redux-devtools/packages/redux-devtools-app/src/reducers/instances.ts
Nathan Bierema a418284a4a
chore(extension): convert to TypeScript (#756)
* Start work

* More work

* stash

* stash

* stash

* Eliminate relay

* Fix

* Define page script to content script messages

* Define ContentScriptToPageScriptMessage

* Not required

* Fill out more types

* More work on types

* More type fixes

* Add type

* More improvements to types

* More work on types

* Fix more type errors

* More changes

* More work

* Work

* Fix build

* Fix lint

* Fix more lint

* Fix bug

* Fix tests
2021-08-25 00:22:54 -04:00

383 lines
10 KiB
TypeScript

import { PerformAction } from '@redux-devtools/core';
import { Action } from 'redux';
import {
UPDATE_STATE,
SET_STATE,
LIFTED_ACTION,
SELECT_INSTANCE,
REMOVE_INSTANCE,
TOGGLE_PERSIST,
TOGGLE_SYNC,
} from '../constants/actionTypes';
import { DISCONNECTED } from '../constants/socketActionTypes';
import parseJSON from '../utils/parseJSON';
import { recompute } from '../utils/updateState';
import {
ActionCreator,
LiftedActionDispatchAction,
Request,
StoreAction,
} from '../actions';
export interface Features {
lock?: boolean;
export?: string | boolean;
import?: string | boolean;
persist?: boolean;
pause?: boolean;
reorder?: boolean;
jump?: boolean;
skip?: boolean;
dispatch?: boolean;
sync?: boolean;
test?: boolean;
}
export interface Options {
name?: string | number;
connectionId?: string | number;
explicitLib?: string;
lib?: string;
actionCreators?: ActionCreator[];
features: Features;
serialize?: boolean;
}
export interface State {
actionsById: { [actionId: number]: PerformAction<Action<unknown>> };
computedStates: { state: unknown; error?: string }[];
currentStateIndex: number;
nextActionId: number;
skippedActionIds: number[];
stagedActionIds: number[];
committedState?: unknown;
isLocked?: boolean;
isPaused?: boolean;
}
export interface InstancesState {
selected: string | number | null;
current: string | number;
sync: boolean;
connections: { [id: string]: (string | number)[] };
options: { [id: string]: Options };
states: { [id: string]: State };
persisted?: boolean;
}
export const initialState: InstancesState = {
selected: null,
current: 'default',
sync: false,
connections: {},
options: { default: { features: {} } },
states: {
default: {
actionsById: {},
computedStates: [],
currentStateIndex: -1,
nextActionId: 0,
skippedActionIds: [],
stagedActionIds: [],
},
},
};
function updateState(
state: { [id: string]: State },
request: Request,
id: string | number,
serialize: boolean | undefined
) {
let payload: State = request.payload as State;
const actionsById = request.actionsById;
if (actionsById) {
payload = {
// eslint-disable-next-line @typescript-eslint/ban-types
...payload,
actionsById: parseJSON(actionsById, serialize),
computedStates: parseJSON(request.computedStates, serialize),
} as State;
if (request.type === 'STATE' && request.committedState) {
payload.committedState = payload.computedStates[0].state;
}
} else {
payload = parseJSON(payload as unknown as string, serialize) as State;
}
let newState;
const liftedState = state[id] || state.default;
const action = ((request.action && parseJSON(request.action, serialize)) ||
{}) as PerformAction<Action<unknown>>;
switch (request.type) {
case 'INIT':
newState = recompute(state.default, payload, {
action: { type: '@@INIT' },
timestamp: (action as { timestamp?: number }).timestamp || Date.now(),
});
break;
case 'ACTION': {
const isExcess = request.isExcess;
const nextActionId = request.nextActionId || liftedState.nextActionId + 1;
const maxAge = request.maxAge;
if (Array.isArray(action)) {
// Batched actions
newState = liftedState;
for (let i = 0; i < action.length; i++) {
newState = recompute(
newState,
request.batched ? payload : (payload as unknown as State[])[i],
action[i],
newState.nextActionId + 1,
maxAge,
isExcess
);
}
} else {
newState = recompute(
liftedState,
payload,
action,
nextActionId,
maxAge,
isExcess
);
}
break;
}
case 'STATE':
newState = payload;
if (newState.computedStates.length <= newState.currentStateIndex) {
newState.currentStateIndex = newState.computedStates.length - 1;
}
break;
case 'PARTIAL_STATE': {
const maxAge = request.maxAge;
const nextActionId = payload.nextActionId;
const stagedActionIds = payload.stagedActionIds;
let computedStates = payload.computedStates;
let oldActionsById;
let oldComputedStates;
let committedState;
if (nextActionId > maxAge) {
const oldStagedActionIds = liftedState.stagedActionIds;
const excess = oldStagedActionIds.indexOf(stagedActionIds[1]);
let key;
if (excess > 0) {
oldComputedStates = liftedState.computedStates.slice(excess - 1);
oldActionsById = { ...liftedState.actionsById };
for (let i = 1; i < excess; i++) {
key = oldStagedActionIds[i];
if (key) delete oldActionsById[key];
}
committedState = computedStates[0].state;
} else {
oldActionsById = liftedState.actionsById;
oldComputedStates = liftedState.computedStates;
committedState = liftedState.committedState;
}
} else {
oldActionsById = liftedState.actionsById;
oldComputedStates = liftedState.computedStates;
committedState = liftedState.committedState;
}
computedStates = [...oldComputedStates, ...computedStates];
const statesCount = computedStates.length;
let currentStateIndex = payload.currentStateIndex;
if (statesCount <= currentStateIndex) currentStateIndex = statesCount - 1;
newState = {
...liftedState,
actionsById: { ...oldActionsById, ...payload.actionsById },
computedStates,
currentStateIndex,
nextActionId,
stagedActionIds,
committedState,
};
break;
}
case 'LIFTED':
newState = liftedState;
break;
default:
return state;
}
if (request.liftedState) newState = { ...newState, ...request.liftedState };
return { ...state, [id]: newState };
}
export function dispatchAction(
state: InstancesState,
{ action }: LiftedActionDispatchAction
) {
if (action.type === 'JUMP_TO_STATE' || action.type === 'JUMP_TO_ACTION') {
const id = state.selected || state.current;
const liftedState = state.states[id];
let currentStateIndex = action.index;
if (typeof currentStateIndex === 'undefined' && action.actionId) {
currentStateIndex = liftedState.stagedActionIds.indexOf(action.actionId);
}
return {
...state,
states: {
...state.states,
[id]: { ...liftedState, currentStateIndex },
},
};
}
return state;
}
function removeState(state: InstancesState, connectionId: string | number) {
const instanceIds = state.connections[connectionId];
if (!instanceIds) return state;
const connections = { ...state.connections };
const options = { ...state.options };
const states = { ...state.states };
let selected = state.selected;
let current = state.current;
let sync = state.sync;
delete connections[connectionId];
instanceIds.forEach((id) => {
if (id === selected) {
selected = null;
sync = false;
}
if (id === current) {
const inst = Object.keys(connections)[0];
if (inst) current = connections[inst][0];
else current = 'default';
}
delete options[id];
delete states[id];
});
return {
selected,
current,
sync,
connections,
options,
states,
};
}
function init(
{ type, action, name, libConfig = {} }: Request,
connectionId: string | number,
current: string | number
): Options {
let lib;
let actionCreators;
let creators = libConfig.actionCreators || action;
if (typeof creators === 'string') creators = JSON.parse(creators);
if (Array.isArray(creators)) actionCreators = creators;
if (type === 'STATE') lib = 'redux';
return {
name: libConfig.name || name || current,
connectionId,
explicitLib: libConfig.type,
lib,
actionCreators,
features: libConfig.features
? libConfig.features
: {
lock: lib === 'redux',
export: libConfig.type === 'redux' ? 'custom' : true,
import: 'custom',
persist: true,
pause: true,
reorder: true,
jump: true,
skip: true,
dispatch: true,
sync: true,
test: true,
},
serialize: libConfig.serialize,
};
}
export default function instances(
state = initialState,
action: StoreAction
): InstancesState {
switch (action.type) {
case UPDATE_STATE: {
const { request } = action;
if (!request) return state;
const connectionId = (action.id || request.id)!;
const current = request.instanceId || connectionId;
let connections = state.connections;
let options = state.options;
if (typeof state.options[current] === 'undefined') {
connections = {
...state.connections,
[connectionId]: [...(connections[connectionId] || []), current],
};
options = {
...options,
[current]: init(request, connectionId, current),
};
}
return {
...state,
current,
connections,
options,
states: updateState(
state.states,
request,
current,
options[current].serialize
),
};
}
case SET_STATE:
return {
...state,
states: {
...state.states,
[getActiveInstance(state)]: action.newState,
},
};
case TOGGLE_PERSIST:
return { ...state, persisted: !state.persisted };
case TOGGLE_SYNC:
return { ...state, sync: !state.sync };
case SELECT_INSTANCE:
return { ...state, selected: action.selected, sync: false };
case REMOVE_INSTANCE:
return removeState(state, action.id);
case LIFTED_ACTION: {
if (action.message === 'DISPATCH') return dispatchAction(state, action);
if (action.message === 'IMPORT') {
const id = state.selected || state.current;
if (state.options[id].features.import === true) {
return {
...state,
states: {
...state.states,
[id]: parseJSON(action.state) as State,
},
};
}
}
return state;
}
case DISCONNECTED:
return initialState;
default:
return state;
}
}
export const getActiveInstance = (instances: InstancesState) =>
instances.selected || instances.current;