This commit is contained in:
Nathan Bierema 2020-05-17 00:34:49 -04:00
parent 3b9d9443fb
commit 5cc660ff7c
2 changed files with 200 additions and 87 deletions

View File

@ -96,9 +96,9 @@ interface JumpToActionAction {
actionId: number;
}
interface ImportStateAction<S, A extends Action<unknown>> {
interface ImportStateAction<S, A extends Action<unknown>, MonitorState> {
type: typeof ActionTypes.IMPORT_STATE;
nextLiftedState: LiftedState<S, A> | readonly A[];
nextLiftedState: LiftedState<S, A, MonitorState> | readonly A[];
preloadedState?: S;
noRecompute: boolean | undefined;
}
@ -113,7 +113,12 @@ interface PauseRecordingAction {
status: boolean;
}
export type LiftedAction<S, A extends Action<unknown>> =
export type LiftedAction<
S,
A extends Action<unknown>,
MonitorState,
MonitorAction extends Action<unknown>
> =
| PerformAction<A>
| ResetAction
| RollbackAction
@ -124,7 +129,7 @@ export type LiftedAction<S, A extends Action<unknown>> =
| ReorderAction
| JumpToStateAction
| JumpToActionAction
| ImportStateAction<S, A>
| ImportStateAction<S, A, MonitorState>
| LockChangesAction
| PauseRecordingAction;
@ -242,10 +247,10 @@ export const ActionCreators = {
return { type: ActionTypes.JUMP_TO_ACTION, actionId };
},
importState<S, A extends Action<unknown>>(
nextLiftedState: LiftedState<S, A> | readonly A[],
importState<S, A extends Action<unknown>, MonitorState>(
nextLiftedState: LiftedState<S, A, MonitorState> | readonly A[],
noRecompute?: boolean
): ImportStateAction<S, A> {
): ImportStateAction<S, A, MonitorState> {
return { type: ActionTypes.IMPORT_STATE, nextLiftedState, noRecompute };
},
@ -371,7 +376,7 @@ export function liftAction<A extends Action<unknown>>(
traceLimit?: number,
toExcludeFromTrace?: Function
): PerformAction<A> {
return ActionCreators.performAction<A>(
return ActionCreators.performAction(
action,
trace,
traceLimit,
@ -379,8 +384,14 @@ export function liftAction<A extends Action<unknown>>(
);
}
export interface LiftedState<S, A extends Action<unknown>> {
monitorState: unknown;
function isArray<S, A extends Action<unknown>, MonitorState>(
nextLiftedState: LiftedState<S, A, MonitorState> | readonly A[]
): nextLiftedState is readonly A[] {
return Array.isArray(nextLiftedState);
}
export interface LiftedState<S, A extends Action<unknown>, MonitorState> {
monitorState: MonitorState;
nextActionId: number;
actionsById: { [actionId: number]: PerformAction<A> };
stagedActionIds: number[];
@ -392,25 +403,27 @@ export interface LiftedState<S, A extends Action<unknown>> {
isPaused: boolean;
}
function isArray<S, A extends Action<unknown>>(
nextLiftedState: LiftedState<S, A> | readonly A[]
): nextLiftedState is readonly A[] {
return Array.isArray(nextLiftedState);
}
/**
* Creates a history state reducer from an app's reducer.
*/
export function liftReducerWith<S, A extends Action<unknown>>(
export function liftReducerWith<
S,
A extends Action<unknown>,
MonitorState,
MonitorAction extends Action<unknown>
>(
reducer: Reducer<S, A>,
initialCommittedState: PreloadedState<S> | undefined,
monitorReducer: Reducer<unknown, Action<unknown>>,
options: Options<S, A>
): Reducer<LiftedState<S, A>, LiftedAction<S, A>> {
const initialLiftedState: LiftedState<S, A> = {
monitorState: monitorReducer(undefined, {} as Action<unknown>),
monitorReducer: Reducer<MonitorState, MonitorAction>,
options: Options<S, A, MonitorState, MonitorAction>
): Reducer<
LiftedState<S, A, MonitorState>,
LiftedAction<S, A, MonitorState, MonitorAction>
> {
const initialLiftedState: LiftedState<S, A, MonitorState> = {
monitorState: monitorReducer(undefined, {} as MonitorAction),
nextActionId: 1,
actionsById: { 0: liftAction<A>(INIT_ACTION as A) },
actionsById: { 0: liftAction(INIT_ACTION as A) },
stagedActionIds: [0],
skippedActionIds: [],
committedState: initialCommittedState as S,
@ -424,9 +437,9 @@ export function liftReducerWith<S, A extends Action<unknown>>(
* Manages how the history actions modify the history state.
*/
return (
liftedState: LiftedState<S, A> | undefined,
liftedAction: LiftedAction<S, A>
): LiftedState<S, A> => {
liftedState: LiftedState<S, A, MonitorState> | undefined,
liftedAction: LiftedAction<S, A, MonitorState, MonitorAction>
): LiftedState<S, A, MonitorState> => {
let {
monitorState,
actionsById,
@ -471,11 +484,16 @@ export function liftReducerWith<S, A extends Action<unknown>>(
currentStateIndex > excess ? currentStateIndex - excess : 0;
}
function computePausedAction(shouldInit?: boolean): LiftedState<S, A> {
function computePausedAction(
shouldInit?: boolean
): LiftedState<S, A, MonitorState> {
let computedState;
if (shouldInit) {
computedState = computedStates[currentStateIndex];
monitorState = monitorReducer(monitorState, liftedAction);
monitorState = monitorReducer(
monitorState,
liftedAction as MonitorAction
);
} else {
computedState = computeNextEntry(
reducer,
@ -487,7 +505,7 @@ export function liftReducerWith<S, A extends Action<unknown>>(
if (!options.pauseActionType || nextActionId === 1) {
return {
monitorState,
actionsById: { 0: liftAction<A>(INIT_ACTION as A) },
actionsById: { 0: liftAction(INIT_ACTION as A) },
nextActionId: 1,
stagedActionIds: [0],
skippedActionIds: [],
@ -509,7 +527,7 @@ export function liftReducerWith<S, A extends Action<unknown>>(
monitorState,
actionsById: {
...actionsById,
[nextActionId - 1]: liftAction<A>({
[nextActionId - 1]: liftAction({
type: options.pauseActionType
} as A)
},
@ -539,7 +557,7 @@ export function liftReducerWith<S, A extends Action<unknown>>(
if (/^@@redux\/(INIT|REPLACE)/.test(liftedAction.type)) {
if (options.shouldHotReload === false) {
actionsById = { 0: liftAction<A>(INIT_ACTION as A) };
actionsById = { 0: liftAction(INIT_ACTION as A) };
nextActionId = 1;
stagedActionIds = [0];
skippedActionIds = [];
@ -597,7 +615,7 @@ export function liftReducerWith<S, A extends Action<unknown>>(
}
case ActionTypes.RESET: {
// Get back to the state the store was created with.
actionsById = { 0: liftAction<A>(INIT_ACTION as A) };
actionsById = { 0: liftAction(INIT_ACTION as A) };
nextActionId = 1;
stagedActionIds = [0];
skippedActionIds = [];
@ -609,7 +627,7 @@ export function liftReducerWith<S, A extends Action<unknown>>(
case ActionTypes.COMMIT: {
// Consider the last committed state the new starting point.
// Squash any staged actions into a single committed state.
actionsById = { 0: liftAction<A>(INIT_ACTION as A) };
actionsById = { 0: liftAction(INIT_ACTION as A) };
nextActionId = 1;
stagedActionIds = [0];
skippedActionIds = [];
@ -621,7 +639,7 @@ export function liftReducerWith<S, A extends Action<unknown>>(
case ActionTypes.ROLLBACK: {
// Forget about any staged actions.
// Start again from the last committed state.
actionsById = { 0: liftAction<A>(INIT_ACTION as A) };
actionsById = { 0: liftAction(INIT_ACTION as A) };
nextActionId = 1;
stagedActionIds = [0];
skippedActionIds = [];
@ -724,7 +742,7 @@ export function liftReducerWith<S, A extends Action<unknown>>(
case ActionTypes.IMPORT_STATE: {
if (isArray(liftedAction.nextLiftedState)) {
// recompute array of actions
actionsById = { 0: liftAction<A>(INIT_ACTION as A) };
actionsById = { 0: liftAction(INIT_ACTION as A) };
nextActionId = 1;
stagedActionIds = [0];
skippedActionIds = [];
@ -772,7 +790,7 @@ export function liftReducerWith<S, A extends Action<unknown>>(
return computePausedAction(true);
}
// Commit when unpausing
actionsById = { 0: liftAction<A>(INIT_ACTION as A) };
actionsById = { 0: liftAction(INIT_ACTION as A) };
nextActionId = 1;
stagedActionIds = [0];
skippedActionIds = [];
@ -800,7 +818,7 @@ export function liftReducerWith<S, A extends Action<unknown>>(
skippedActionIds,
options.shouldCatchErrors
);
monitorState = monitorReducer(monitorState, liftedAction);
monitorState = monitorReducer(monitorState, liftedAction as MonitorAction);
return {
monitorState,
actionsById,
@ -819,25 +837,55 @@ export function liftReducerWith<S, A extends Action<unknown>>(
/**
* Provides an app's view into the state of the lifted store.
*/
export function unliftState<S, A extends Action<unknown>, NextStateExt>(
liftedState: LiftedState<S, A> & NextStateExt
) {
export function unliftState<
S,
A extends Action<unknown>,
MonitorState,
MonitorAction extends Action<unknown>,
NextStateExt
>(
liftedState: LiftedState<S, A, MonitorState> & NextStateExt
): S & NextStateExt {
const { computedStates, currentStateIndex } = liftedState;
const { state } = computedStates[currentStateIndex];
return state;
return state as S & NextStateExt;
}
export type LiftedStore<S, A extends Action<unknown>> = Store<
LiftedState<S, A>,
LiftedAction<S, A>
export type LiftedReducer<
S,
A extends Action<unknown>,
MonitorState,
MonitorAction extends Action<unknown>
> = Reducer<
LiftedState<S, A, MonitorState>,
LiftedAction<S, A, MonitorState, MonitorAction>
>;
export type InstrumentExt<S, A extends Action<unknown>> = {
liftedStore: LiftedStore<S, A>;
export type LiftedStore<
S,
A extends Action<unknown>,
MonitorState,
MonitorAction extends Action<unknown>
> = Store<
LiftedState<S, A, MonitorState>,
LiftedAction<S, A, MonitorState, MonitorAction>
>;
export type InstrumentExt<
S,
A extends Action<unknown>,
MonitorState,
MonitorAction extends Action<unknown>
> = {
liftedStore: LiftedStore<S, A, MonitorState, MonitorAction>;
};
export type EnhancedStore<S, A extends Action<unknown>> = Store<S, A> &
InstrumentExt<S, A>;
export type EnhancedStore<
S,
A extends Action<unknown>,
MonitorState,
MonitorAction extends Action<unknown>
> = Store<S, A> & InstrumentExt<S, A, MonitorState, MonitorAction>;
/**
* Provides an app's view into the lifted store.
@ -845,26 +893,35 @@ export type EnhancedStore<S, A extends Action<unknown>> = Store<S, A> &
export function unliftStore<
S,
A extends Action<unknown>,
MonitorState,
MonitorAction extends Action<unknown>,
NextExt,
NextStateExt
>(
liftedStore: Store<LiftedState<S, A> & NextStateExt, LiftedAction<S, A>> &
liftedStore: Store<
LiftedState<S, A, MonitorState> & NextStateExt,
LiftedAction<S, A, MonitorState, MonitorAction>
> &
NextExt,
liftReducer: (
r: Reducer<S, A>
) => Reducer<LiftedState<S, A>, LiftedAction<S, A>>,
options: Options<S, A>
) => LiftedReducer<S, A, MonitorState, MonitorAction>,
options: Options<S, A, MonitorState, MonitorAction>
): Store<S & NextStateExt, A> &
NextExt & {
liftedStore: Store<LiftedState<S, A> & NextStateExt, LiftedAction<S, A>>;
liftedStore: Store<
LiftedState<S, A, MonitorState> & NextStateExt,
LiftedAction<S, A, MonitorState, MonitorAction>
>;
} {
let lastDefinedState: S & NextStateExt;
const trace = options.trace || options.shouldIncludeCallstack;
const traceLimit = options.traceLimit || 10;
function getState(): S & NextStateExt {
const state = unliftState<S, A, NextStateExt>(liftedStore.getState()) as S &
NextStateExt;
const state = unliftState<S, A, MonitorState, MonitorAction, NextStateExt>(
liftedStore.getState()
);
if (state !== undefined) {
lastDefinedState = state;
}
@ -890,8 +947,8 @@ export function unliftStore<
(liftReducer(
(nextReducer as unknown) as Reducer<S, A>
) as unknown) as Reducer<
LiftedState<S, A> & NextStateExt,
LiftedAction<S, A>
LiftedState<S, A, MonitorState> & NextStateExt,
LiftedAction<S, A, MonitorState, MonitorAction>
>
);
},
@ -922,16 +979,24 @@ export function unliftStore<
}
} as unknown) as Store<S & NextStateExt, A> &
NextExt & {
liftedStore: Store<LiftedState<S, A> & NextStateExt, LiftedAction<S, A>>;
liftedStore: Store<
LiftedState<S, A, MonitorState> & NextStateExt,
LiftedAction<S, A, MonitorState, MonitorAction>
>;
};
}
export interface Options<S, A extends Action<unknown>> {
export interface Options<
S,
A extends Action<unknown>,
MonitorState,
MonitorAction extends Action<unknown>
> {
maxAge?:
| number
| ((
currentLiftedAction: LiftedAction<S, A>,
previousLiftedState: LiftedState<S, A> | undefined
currentLiftedAction: LiftedAction<S, A, MonitorState, MonitorAction>,
previousLiftedState: LiftedState<S, A, MonitorState> | undefined
) => number);
shouldCatchErrors?: boolean;
shouldRecordChanges?: boolean;
@ -946,10 +1011,40 @@ export interface Options<S, A extends Action<unknown>> {
/**
* Redux instrumentation store enhancer.
*/
export default function instrument<OptionsS, OptionsA extends Action<unknown>>(
monitorReducer: Reducer<unknown, Action<unknown>> = () => null,
options: Options<OptionsS, OptionsA> = {}
): StoreEnhancer<InstrumentExt<any, any>> {
export default function instrument<
no_options_state = never,
no_options_action = never,
no_monitor_state = null,
no_monitor_action = never
>(): StoreEnhancer<InstrumentExt<any, any, any, any>>;
export default function instrument<
OptionsS,
OptionsA extends Action<unknown>,
no_monitor_state = null,
no_monitor_action = never
>(
monitorReducer: undefined,
options: Options<OptionsS, OptionsA, any, any>
): StoreEnhancer<InstrumentExt<any, any, any, any>>;
export default function instrument<
OptionsS,
OptionsA extends Action<unknown>,
MonitorState,
MonitorAction extends Action<unknown>
>(
monitorReducer: Reducer<MonitorState, MonitorAction>,
options: Options<OptionsS, OptionsA, MonitorState, MonitorAction>
): StoreEnhancer<InstrumentExt<any, any, MonitorState, MonitorAction>>;
export default function instrument<
OptionsS,
OptionsA extends Action<unknown>,
MonitorState,
MonitorAction extends Action<unknown>
>(
monitorReducer: Reducer<MonitorState, MonitorAction> = ((() =>
null) as unknown) as Reducer<MonitorState, MonitorAction>,
options: Options<OptionsS, OptionsA, MonitorState, MonitorAction> = {}
): StoreEnhancer<InstrumentExt<any, any, MonitorState, MonitorAction>> {
if (typeof options.maxAge === 'number' && options.maxAge < 2) {
throw new Error(
'DevTools.instrument({ maxAge }) option, if specified, ' +
@ -975,22 +1070,25 @@ export default function instrument<OptionsS, OptionsA extends Action<unknown>>(
}
throw new Error('Expected the reducer to be a function.');
}
return liftReducerWith<S, A>(
return liftReducerWith<S, A, MonitorState, MonitorAction>(
r,
initialState,
monitorReducer,
(options as unknown) as Options<S, A>
(options as unknown) as Options<S, A, MonitorState, MonitorAction>
);
}
const liftedStore = createStore(liftReducer(reducer));
if (
(liftedStore as Store<
LiftedState<S, A> & NextStateExt,
LiftedAction<S, A>
LiftedState<S, A, MonitorState> & NextStateExt,
LiftedAction<S, A, MonitorState, MonitorAction>
> &
NextExt & {
liftedStore: Store<LiftedState<S, A>, LiftedAction<S, A>>;
liftedStore: Store<
LiftedState<S, A, MonitorState>,
LiftedAction<S, A, MonitorState, MonitorAction>
>;
}).liftedStore
) {
throw new Error(
@ -999,10 +1097,17 @@ export default function instrument<OptionsS, OptionsA extends Action<unknown>>(
);
}
return unliftStore<S, A, NextExt, NextStateExt>(
return unliftStore<
S,
A,
MonitorState,
MonitorAction,
NextExt,
NextStateExt
>(
liftedStore,
liftReducer,
(options as unknown) as Options<S, A>
(options as unknown) as Options<S, A, MonitorState, MonitorAction>
);
};
}

View File

@ -90,8 +90,8 @@ function counterWithMultiply(state = 0, action: CounterWithMultiplyAction) {
}
describe('instrument', () => {
let store: EnhancedStore<number, CounterAction>;
let liftedStore: LiftedStore<number, CounterAction>;
let store: EnhancedStore<number, CounterAction, null, never>;
let liftedStore: LiftedStore<number, CounterAction, null, never>;
beforeEach(() => {
store = createStore(counter, instrument());
@ -534,8 +534,8 @@ describe('instrument', () => {
});
describe('maxAge option', () => {
let configuredStore: EnhancedStore<number, CounterAction>;
let configuredLiftedStore: LiftedStore<number, CounterAction>;
let configuredStore: EnhancedStore<number, CounterAction, null, never>;
let configuredLiftedStore: LiftedStore<number, CounterAction, null, never>;
beforeEach(() => {
configuredStore = createStore(
@ -618,7 +618,9 @@ describe('instrument', () => {
);
const liftedStoreWithAnotherBug = (liftedStoreWithBug as unknown) as LiftedStore<
number,
CounterWithAnotherBugAction
CounterWithAnotherBugAction,
null,
never
>;
expect(liftedStoreWithAnotherBug.getState().stagedActionIds).toHaveLength(
5
@ -630,7 +632,9 @@ describe('instrument', () => {
);
const liftedStore = liftedStoreWithBug as LiftedStore<
number,
CounterAction
CounterAction,
null,
never
>;
expect(liftedStore.getState().stagedActionIds).toHaveLength(3);
@ -698,7 +702,9 @@ describe('instrument', () => {
);
const liftedStore = liftedStoreWithBug as LiftedStore<
number,
CounterAction
CounterAction,
null,
never
>;
const liftedStoreState = liftedStore.getState();
const currentComputedState =
@ -730,7 +736,9 @@ describe('instrument', () => {
);
const liftedStore = liftedStoreWithBug as LiftedStore<
number,
CounterAction
CounterAction,
null,
never
>;
const liftedStoreState = liftedStore.getState();
const currentComputedState =
@ -1081,9 +1089,9 @@ describe('instrument', () => {
});
describe('Import State', () => {
let monitoredStore: EnhancedStore<number, CounterAction>;
let monitoredLiftedStore: LiftedStore<number, CounterAction>;
let exportedState: LiftedState<number, CounterAction>;
let monitoredStore: EnhancedStore<number, CounterAction, null, never>;
let monitoredLiftedStore: LiftedStore<number, CounterAction, null, never>;
let exportedState: LiftedState<number, CounterAction, null>;
beforeEach(() => {
monitoredStore = createStore(counter, instrument());
@ -1164,7 +1172,7 @@ describe('instrument', () => {
});
function filterStackAndTimestamps<S, A extends Action<unknown>>(
state: LiftedState<S, A>
state: LiftedState<S, A, null>
) {
state.actionsById = _.mapValues(state.actionsById, action => {
delete action.timestamp;
@ -1175,9 +1183,9 @@ describe('instrument', () => {
}
describe('Import Actions', () => {
let monitoredStore: EnhancedStore<number, CounterAction>;
let monitoredLiftedStore: LiftedStore<number, CounterAction>;
let exportedState: LiftedState<number, CounterAction>;
let monitoredStore: EnhancedStore<number, CounterAction, null, never>;
let monitoredLiftedStore: LiftedStore<number, CounterAction, null, never>;
let exportedState: LiftedState<number, CounterAction, null>;
const savedActions = [
{ type: 'INCREMENT' },
{ type: 'INCREMENT' },
@ -1285,7 +1293,7 @@ describe('instrument', () => {
{ type: 'INCREMENT' }
] as const;
store.liftedStore.dispatch(
ActionCreators.importState<number, CounterAction>(savedActions)
ActionCreators.importState<number, CounterAction, null>(savedActions)
);
expect(store.liftedStore.getState().nextActionId).toBe(3);
expect(store.getState()).toBe(2);