Resolved dependency cycle between actions and web utils

This commit is contained in:
Filip Barl
2019-08-02 20:02:30 +02:00
parent c2fa42f514
commit 478e325755
19 changed files with 644 additions and 607 deletions
+6 -217
View File
@@ -4,24 +4,18 @@ import { defaults } from 'lodash';
import { Map as makeMap, List } from 'immutable';
import {
closeWebsocket, openWebsocket, receiveError,
receiveApiDetails, receiveNodesDelta, receiveNodeDetails,
receiveControlPipeStatus, receiveTopologies, receiveNotFound,
receiveNodesForTopology, receiveNodes,
receiveError,
receiveNodeDetails,
receiveNotFound, receiveNodesForTopology, receiveNodes,
} from '../actions/app-actions';
import { getCurrentTopologyUrl } from './topology-utils';
import { layersTopologyIdsSelector } from '../selectors/resource-view/layout';
import { activeTopologyOptionsSelector } from '../selectors/topology';
import { isPausedSelector } from '../selectors/time-travel';
import { API_REFRESH_INTERVAL, TOPOLOGY_REFRESH_INTERVAL } from '../constants/timer';
const log = debug('scope:web-api-utils');
const reconnectTimerInterval = 5000;
const updateFrequency = '5s';
const FIRST_RENDER_TOO_LONG_THRESHOLD = 100; // ms
const csrfToken = (() => {
// Check for token at window level or parent level (for iframe);
/* eslint-disable no-underscore-dangle */
@@ -37,15 +31,6 @@ const csrfToken = (() => {
return token;
})();
let socket;
let reconnectTimer = 0;
let topologyTimer = 0;
let apiDetailsTimer = 0;
let currentUrl = null;
let createWebsocketAt = null;
let firstMessageOnWebsocketAt = null;
let continuePolling = true;
export function buildUrlQuery(params = makeMap(), state = null) {
// Attach the time travel timestamp to every request to the backend.
if (state) {
@@ -103,7 +88,7 @@ export function getReportUrl(timestamp) {
return `${getApiPath()}/api/report?${buildUrlQuery(makeMap({ timestamp }))}`;
}
function topologiesUrl(state) {
export function topologiesUrl(state) {
const activeTopologyOptions = activeTopologyOptionsSelector(state);
const optionsQuery = buildUrlQuery(activeTopologyOptions, state);
return `${getApiPath()}/api/topology?${optionsQuery}`;
@@ -114,68 +99,12 @@ export function getWebsocketUrl(host = window.location.host, pathname = window.l
return `${wsProto}://${host}${getApiPath(pathname)}`;
}
function buildWebsocketUrl(topologyUrl, topologyOptions = makeMap(), state) {
export function buildWebsocketUrl(topologyUrl, topologyOptions = makeMap(), state) {
topologyOptions = topologyOptions.set('t', updateFrequency);
const optionsQuery = buildUrlQuery(topologyOptions, state);
return `${getWebsocketUrl()}${topologyUrl}/ws?${optionsQuery}`;
}
function createWebsocket(websocketUrl, getState, dispatch) {
if (socket) {
socket.onclose = null;
socket.onerror = null;
socket.close();
// onclose() is not called, but that's fine since we're opening a new one
// right away
}
// profiling
createWebsocketAt = new Date();
firstMessageOnWebsocketAt = null;
socket = new WebSocket(websocketUrl);
socket.onopen = () => {
log(`Opening websocket to ${websocketUrl}`);
dispatch(openWebsocket());
};
socket.onclose = () => {
clearTimeout(reconnectTimer);
log(`Closing websocket to ${websocketUrl}`, socket.readyState);
socket = null;
dispatch(closeWebsocket());
if (continuePolling && !isPausedSelector(getState())) {
reconnectTimer = setTimeout(() => {
createWebsocket(websocketUrl, getState, dispatch);
}, reconnectTimerInterval);
}
};
socket.onerror = () => {
log(`Error in websocket to ${websocketUrl}`);
dispatch(receiveError(websocketUrl));
};
socket.onmessage = (event) => {
const msg = JSON.parse(event.data);
dispatch(receiveNodesDelta(msg));
// profiling (receiveNodesDelta triggers synchronous render)
if (!firstMessageOnWebsocketAt) {
firstMessageOnWebsocketAt = new Date();
const timeToFirstMessage = firstMessageOnWebsocketAt - createWebsocketAt;
if (timeToFirstMessage > FIRST_RENDER_TOO_LONG_THRESHOLD) {
log(
'Time (ms) to first nodes render after websocket was created',
firstMessageOnWebsocketAt - createWebsocketAt
);
}
}
};
}
/**
* XHR wrapper. Applies a CSRF token (if it exists) and content-type to all requests.
* Any opts that get passed in will override the defaults.
@@ -210,7 +139,7 @@ function getNodesForTopologies(state, dispatch, topologyIds, topologyOptions = m
);
}
function getNodesOnce(getState, dispatch) {
export function getNodesOnce(getState, dispatch) {
const state = getState();
const topologyUrl = getCurrentTopologyUrl(state);
const topologyOptions = activeTopologyOptionsSelector(state);
@@ -246,63 +175,6 @@ export function getResourceViewNodesSnapshot(state, dispatch) {
getNodesForTopologies(state, dispatch, topologyIds);
}
function pollTopologies(getState, dispatch, initialPoll = false) {
// Used to resume polling when navigating between pages in Weave Cloud.
continuePolling = initialPoll === true ? true : continuePolling;
clearTimeout(topologyTimer);
// NOTE: getState is called every time to make sure the up-to-date state is used.
const url = topologiesUrl(getState());
doRequest({
error: (req) => {
log(`Error in topology request: ${req.responseText}`);
dispatch(receiveError(url));
// Only retry in stand-alone mode
if (continuePolling && !isPausedSelector(getState())) {
topologyTimer = setTimeout(() => {
pollTopologies(getState, dispatch);
}, TOPOLOGY_REFRESH_INTERVAL);
}
},
success: (res) => {
if (continuePolling && !isPausedSelector(getState())) {
dispatch(receiveTopologies(res));
topologyTimer = setTimeout(() => {
pollTopologies(getState, dispatch);
}, TOPOLOGY_REFRESH_INTERVAL);
}
},
url
});
}
function getTopologiesOnce(getState, dispatch) {
const url = topologiesUrl(getState());
doRequest({
error: (req) => {
log(`Error in topology request: ${req.responseText}`);
dispatch(receiveError(url));
},
success: (res) => {
dispatch(receiveTopologies(res));
},
url
});
}
function updateWebsocketChannel(getState, dispatch, forceRequest) {
const topologyUrl = getCurrentTopologyUrl(getState());
const topologyOptions = activeTopologyOptionsSelector(getState());
const websocketUrl = buildWebsocketUrl(topologyUrl, topologyOptions, getState());
// Only recreate websocket if url changed or if forced (weave cloud instance reload);
const isNewUrl = websocketUrl !== currentUrl;
// `topologyUrl` can be undefined initially, so only create a socket if it is truthy
// and no socket exists, or if we get a new url.
if (topologyUrl && (!socket || isNewUrl || forceRequest)) {
createWebsocket(websocketUrl, getState, dispatch);
currentUrl = websocketUrl;
}
}
export function getNodeDetails(getState, dispatch) {
const state = getState();
const nodeMap = state.get('nodeDetails');
@@ -349,48 +221,6 @@ export function getNodeDetails(getState, dispatch) {
}
}
export function getTopologies(getState, dispatch, forceRequest) {
if (isPausedSelector(getState())) {
getTopologiesOnce(getState, dispatch);
} else {
pollTopologies(getState, dispatch, forceRequest);
}
}
export function getNodes(getState, dispatch, forceRequest = false) {
if (isPausedSelector(getState())) {
getNodesOnce(getState, dispatch);
} else {
updateWebsocketChannel(getState, dispatch, forceRequest);
}
getNodeDetails(getState, dispatch);
}
export function getApiDetails(dispatch) {
clearTimeout(apiDetailsTimer);
const url = `${getApiPath()}/api`;
doRequest({
error: (req) => {
log(`Error in api details request: ${req.responseText}`);
receiveError(url);
if (continuePolling) {
apiDetailsTimer = setTimeout(() => {
getApiDetails(dispatch);
}, API_REFRESH_INTERVAL / 2);
}
},
success: (res) => {
dispatch(receiveApiDetails(res));
if (continuePolling) {
apiDetailsTimer = setTimeout(() => {
getApiDetails(dispatch);
}, API_REFRESH_INTERVAL);
}
},
url
});
}
export function doResizeTty(pipeId, control, cols, rows) {
const url = `${getApiPath()}/api/control/${encodeURIComponent(control.probeId)}/`
+ `${encodeURIComponent(control.nodeId)}/${control.id}`;
@@ -420,44 +250,3 @@ export function deletePipe(pipeId, dispatch) {
url
});
}
export function getPipeStatus(pipeId, dispatch) {
const url = `${getApiPath()}/api/pipe/${encodeURIComponent(pipeId)}/check`;
doRequest({
complete: (res) => {
const status = {
204: 'PIPE_ALIVE',
404: 'PIPE_DELETED'
}[res.status];
if (!status) {
log('Unexpected pipe status:', res.status);
return;
}
dispatch(receiveControlPipeStatus(pipeId, status));
},
method: 'GET',
url
});
}
export function stopPolling() {
clearTimeout(apiDetailsTimer);
clearTimeout(topologyTimer);
continuePolling = false;
}
export function teardownWebsockets() {
clearTimeout(reconnectTimer);
if (socket) {
socket.onerror = null;
socket.onclose = null;
socket.onmessage = null;
socket.onopen = null;
socket.close();
socket = null;
currentUrl = null;
}
}