redux-devtools/packages/redux-devtools-app/src/middlewares/api.ts

308 lines
7.7 KiB
TypeScript
Raw Normal View History

2019-01-03 17:14:25 +03:00
import {
DispatchAction,
2019-01-10 21:51:14 +03:00
GET_REPORT_ERROR,
GET_REPORT_REQUEST,
GET_REPORT_SUCCESS,
CLEAR_INSTANCES,
getActiveInstance,
importState,
LIFTED_ACTION,
LiftedActionAction,
REMOVE_INSTANCE,
Request,
showNotification,
UPDATE_REPORTS,
UPDATE_STATE,
UpdateReportsRequest,
} from '@redux-devtools/app-core';
import socketClusterClient, { AGClientSocket } from 'socketcluster-client';
import { stringify } from 'jsan';
import { Dispatch, Middleware, MiddlewareAPI } from 'redux';
import * as actions from '../constants/socketActionTypes';
2019-01-03 17:14:25 +03:00
import { nonReduxDispatch } from '../utils/monitorActions';
import { EmitAction, StoreAction } from '../actions';
import { StoreState } from '../reducers';
2019-01-03 17:14:25 +03:00
let socket: AGClientSocket;
let store: MiddlewareAPI<Dispatch<StoreAction>, StoreState>;
2019-01-03 17:14:25 +03:00
function emit({ message: type, id, instanceId, action, state }: EmitAction) {
void socket.transmit(id ? `sc-${id}` : 'respond', {
type,
action,
state,
instanceId,
});
2019-01-03 17:14:25 +03:00
}
function startMonitoring(channel: string) {
2019-01-03 17:14:25 +03:00
if (channel !== store.getState().socket.baseChannel) return;
store.dispatch({ type: actions.EMIT, message: 'START' });
}
function dispatchRemoteAction({
message,
action,
state,
toAll,
}: LiftedActionAction) {
2019-01-03 17:14:25 +03:00
const instances = store.getState().instances;
const instanceId = getActiveInstance(instances);
const id = !toAll && instances.options[instanceId].connectionId;
store.dispatch({
type: actions.EMIT,
message,
action,
2019-01-10 21:51:14 +03:00
state: nonReduxDispatch(
store,
message,
instanceId,
action as DispatchAction,
2019-01-10 21:51:14 +03:00
state,
instances,
2019-01-10 21:51:14 +03:00
),
2019-01-03 17:14:25 +03:00
instanceId,
id,
2019-01-03 17:14:25 +03:00
});
}
interface RequestBase {
id?: string;
instanceId?: string;
}
interface DisconnectedAction extends RequestBase {
type: 'DISCONNECTED';
id: string;
}
interface StartAction extends RequestBase {
type: 'START';
id: string;
}
interface ErrorAction extends RequestBase {
type: 'ERROR';
payload: string;
}
interface RequestWithData extends RequestBase {
data: Request;
}
type MonitoringRequest =
| DisconnectedAction
| StartAction
| ErrorAction
| Request;
function monitoring(request: MonitoringRequest) {
2019-01-03 17:14:25 +03:00
if (request.type === 'DISCONNECTED') {
store.dispatch({
type: REMOVE_INSTANCE,
id: request.id,
2019-01-03 17:14:25 +03:00
});
return;
}
if (request.type === 'START') {
store.dispatch({ type: actions.EMIT, message: 'START', id: request.id });
return;
}
if (request.type === 'ERROR') {
store.dispatch(showNotification(request.payload));
return;
}
store.dispatch({
type: UPDATE_STATE,
request: (request as unknown as RequestWithData).data
? { ...(request as unknown as RequestWithData).data, id: request.id }
: request,
2019-01-03 17:14:25 +03:00
});
const instances = store.getState().instances;
const instanceId = request.instanceId || request.id;
if (
2019-01-10 21:51:14 +03:00
instances.sync &&
instanceId === instances.selected &&
2019-01-03 17:14:25 +03:00
(request.type === 'ACTION' || request.type === 'STATE')
) {
void socket.transmit('respond', {
2019-01-03 17:14:25 +03:00
type: 'SYNC',
state: stringify(instances.states[instanceId]),
id: request.id,
instanceId,
2019-01-03 17:14:25 +03:00
});
}
}
function subscribe(
channelName: string,
subscription: typeof UPDATE_STATE | typeof UPDATE_REPORTS,
) {
2019-01-03 17:14:25 +03:00
const channel = socket.subscribe(channelName);
if (subscription === UPDATE_STATE) {
void (async () => {
for await (const data of channel) {
monitoring(data as MonitoringRequest);
}
})();
} else {
const watcher = (request: UpdateReportsRequest) => {
2019-01-03 17:14:25 +03:00
store.dispatch({ type: subscription, request });
};
void (async () => {
for await (const data of channel) {
watcher(data as UpdateReportsRequest);
}
})();
2019-01-03 17:14:25 +03:00
}
}
function handleConnection() {
void (async () => {
for await (const data of socket.listener('connect')) {
store.dispatch({
type: actions.CONNECT_SUCCESS,
payload: {
id: data.id,
authState: socket.authState,
socketState: socket.state,
},
// @ts-expect-error Is this legitimate?
error: data.authError,
});
if (socket.authState !== actions.AUTHENTICATED) {
store.dispatch({ type: actions.AUTH_REQUEST });
}
2019-01-03 17:14:25 +03:00
}
})();
void (async () => {
for await (const data of socket.listener('disconnect')) {
store.dispatch({ type: actions.DISCONNECTED, code: data.code });
store.dispatch({ type: CLEAR_INSTANCES });
}
})();
2019-01-03 17:14:25 +03:00
void (async () => {
for await (const data of socket.listener('subscribe')) {
store.dispatch({
type: actions.SUBSCRIBE_SUCCESS,
channel: data.channel,
});
}
})();
void (async () => {
for await (const data of socket.listener('unsubscribe')) {
void socket.unsubscribe(data.channel);
store.dispatch({ type: actions.UNSUBSCRIBE, channel: data.channel });
}
})();
void (async () => {
for await (const data of socket.listener('subscribeFail')) {
store.dispatch({
type: actions.SUBSCRIBE_ERROR,
error: data.error,
status: 'subscribeFail',
});
}
})();
2019-01-03 17:14:25 +03:00
void (async () => {
for await (const data of socket.listener('error')) {
store.dispatch({ type: actions.CONNECT_ERROR, error: data.error });
}
})();
2019-01-03 17:14:25 +03:00
}
function connect() {
if (process.env.NODE_ENV === 'test') return;
const connection = store.getState().connection;
try {
socket = socketClusterClient.create(connection.options);
handleConnection();
2019-01-03 17:14:25 +03:00
} catch (error) {
store.dispatch({ type: actions.CONNECT_ERROR, error: error as Error });
store.dispatch(
showNotification((error as Error).message || (error as string)),
);
2019-01-03 17:14:25 +03:00
}
}
function disconnect() {
if (socket) {
socket.disconnect();
}
2019-01-03 17:14:25 +03:00
}
function login() {
void (async () => {
try {
const baseChannel = (await socket.invoke('login', {})) as string;
store.dispatch({ type: actions.AUTH_SUCCESS, baseChannel });
store.dispatch({
type: actions.SUBSCRIBE_REQUEST,
channel: baseChannel,
subscription: UPDATE_STATE,
});
store.dispatch({
type: actions.SUBSCRIBE_REQUEST,
channel: 'report',
subscription: UPDATE_REPORTS,
});
} catch (error) {
store.dispatch({ type: actions.AUTH_ERROR, error: error as Error });
2019-01-03 17:14:25 +03:00
}
})();
2019-01-03 17:14:25 +03:00
}
function getReport(reportId: unknown) {
void (async () => {
try {
const data = (await socket.invoke('getReport', reportId)) as {
payload: string;
};
store.dispatch({ type: GET_REPORT_SUCCESS, data });
store.dispatch(importState(data.payload));
} catch (error) {
store.dispatch({ type: GET_REPORT_ERROR, error: error as Error });
2019-01-03 17:14:25 +03:00
}
})();
2019-01-03 17:14:25 +03:00
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export const api: Middleware<{}, StoreState, Dispatch<StoreAction>> = (
inStore,
) => {
2019-01-03 17:14:25 +03:00
store = inStore;
return (next) => (untypedAction) => {
const result = next(untypedAction);
const action = untypedAction as StoreAction;
switch (action.type) {
2019-01-10 21:51:14 +03:00
case actions.CONNECT_REQUEST:
connect();
break;
2019-01-03 17:14:25 +03:00
case actions.RECONNECT:
disconnect();
if (action.options.type !== 'disabled') connect();
break;
2019-01-10 21:51:14 +03:00
case actions.AUTH_REQUEST:
login();
break;
case actions.SUBSCRIBE_REQUEST:
subscribe(action.channel, action.subscription);
break;
case actions.SUBSCRIBE_SUCCESS:
startMonitoring(action.channel);
break;
case actions.EMIT:
if (socket) emit(action);
break;
case LIFTED_ACTION:
dispatchRemoteAction(action);
break;
case GET_REPORT_REQUEST:
getReport(action.report);
break;
2019-01-03 17:14:25 +03:00
}
return result;
};
};