2015-09-28 18:38:33 +03:00
|
|
|
export const ActionTypes = {
|
2015-07-15 00:09:54 +03:00
|
|
|
PERFORM_ACTION: 'PERFORM_ACTION',
|
|
|
|
RESET: 'RESET',
|
|
|
|
ROLLBACK: 'ROLLBACK',
|
|
|
|
COMMIT: 'COMMIT',
|
|
|
|
SWEEP: 'SWEEP',
|
2015-07-15 01:43:09 +03:00
|
|
|
TOGGLE_ACTION: 'TOGGLE_ACTION',
|
2015-09-27 16:55:57 +03:00
|
|
|
JUMP_TO_STATE: 'JUMP_TO_STATE'
|
2015-07-15 00:09:54 +03:00
|
|
|
};
|
|
|
|
|
2015-09-28 18:38:33 +03:00
|
|
|
/**
|
|
|
|
* Action creators to change the History state.
|
|
|
|
*/
|
|
|
|
export const ActionCreators = {
|
|
|
|
performAction(action) {
|
|
|
|
return { type: ActionTypes.PERFORM_ACTION, action, timestamp: Date.now() };
|
|
|
|
},
|
|
|
|
|
|
|
|
reset() {
|
|
|
|
return { type: ActionTypes.RESET, timestamp: Date.now() };
|
|
|
|
},
|
|
|
|
|
|
|
|
rollback() {
|
|
|
|
return { type: ActionTypes.ROLLBACK, timestamp: Date.now() };
|
|
|
|
},
|
|
|
|
|
|
|
|
commit() {
|
|
|
|
return { type: ActionTypes.COMMIT, timestamp: Date.now() };
|
|
|
|
},
|
|
|
|
|
|
|
|
sweep() {
|
|
|
|
return { type: ActionTypes.SWEEP };
|
|
|
|
},
|
|
|
|
|
|
|
|
toggleAction(index) {
|
|
|
|
return { type: ActionTypes.TOGGLE_ACTION, index };
|
|
|
|
},
|
|
|
|
|
|
|
|
jumpToState(index) {
|
|
|
|
return { type: ActionTypes.JUMP_TO_STATE, index };
|
|
|
|
}
|
2015-07-15 00:09:54 +03:00
|
|
|
};
|
|
|
|
|
2015-09-28 18:38:33 +03:00
|
|
|
const INIT_ACTION = { type: '@@INIT' };
|
|
|
|
|
2015-07-15 00:09:54 +03:00
|
|
|
function toggle(obj, key) {
|
|
|
|
const clone = { ...obj };
|
|
|
|
if (clone[key]) {
|
|
|
|
delete clone[key];
|
|
|
|
} else {
|
|
|
|
clone[key] = true;
|
|
|
|
}
|
|
|
|
return clone;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Computes the next entry in the log by applying an action.
|
|
|
|
*/
|
|
|
|
function computeNextEntry(reducer, action, state, error) {
|
|
|
|
if (error) {
|
|
|
|
return {
|
|
|
|
state,
|
|
|
|
error: 'Interrupted by an error up the chain'
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
let nextState = state;
|
|
|
|
let nextError;
|
|
|
|
try {
|
|
|
|
nextState = reducer(state, action);
|
|
|
|
} catch (err) {
|
|
|
|
nextError = err.toString();
|
2015-07-27 21:10:13 +03:00
|
|
|
console.error(err.stack || err);
|
2015-07-15 00:09:54 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
state: nextState,
|
|
|
|
error: nextError
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Runs the reducer on all actions to get a fresh computation log.
|
|
|
|
*/
|
|
|
|
function recomputeStates(reducer, committedState, stagedActions, skippedActions) {
|
|
|
|
const computedStates = [];
|
|
|
|
|
|
|
|
for (let i = 0; i < stagedActions.length; i++) {
|
|
|
|
const action = stagedActions[i];
|
|
|
|
|
|
|
|
const previousEntry = computedStates[i - 1];
|
|
|
|
const previousState = previousEntry ? previousEntry.state : committedState;
|
|
|
|
const previousError = previousEntry ? previousEntry.error : undefined;
|
|
|
|
|
|
|
|
const shouldSkip = Boolean(skippedActions[i]);
|
|
|
|
const entry = shouldSkip ?
|
|
|
|
previousEntry :
|
|
|
|
computeNextEntry(reducer, action, previousState, previousError);
|
|
|
|
|
|
|
|
computedStates.push(entry);
|
|
|
|
}
|
|
|
|
|
|
|
|
return computedStates;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2015-09-28 18:38:33 +03:00
|
|
|
* Creates a history state reducer from an app's reducer.
|
2015-07-15 00:09:54 +03:00
|
|
|
*/
|
2015-09-28 18:38:33 +03:00
|
|
|
function createHistoryReducer(reducer, initialCommittedState) {
|
|
|
|
const initialHistoryState = {
|
2015-09-28 03:52:10 +03:00
|
|
|
committedState: initialCommittedState,
|
2015-07-15 00:09:54 +03:00
|
|
|
stagedActions: [INIT_ACTION],
|
2015-07-15 01:43:09 +03:00
|
|
|
skippedActions: {},
|
2015-07-19 18:21:10 +03:00
|
|
|
currentStateIndex: 0,
|
2015-07-31 07:20:33 +03:00
|
|
|
timestamps: [Date.now()]
|
2015-07-15 00:09:54 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2015-09-28 18:38:33 +03:00
|
|
|
* Manages how the history actions modify the history state.
|
2015-07-15 00:09:54 +03:00
|
|
|
*/
|
2015-09-28 18:38:33 +03:00
|
|
|
return (historyState = initialHistoryState, historyAction) => {
|
2015-09-27 17:02:32 +03:00
|
|
|
let shouldRecomputeStates = true;
|
2015-07-15 00:09:54 +03:00
|
|
|
let {
|
|
|
|
committedState,
|
|
|
|
stagedActions,
|
|
|
|
skippedActions,
|
2015-07-15 01:43:09 +03:00
|
|
|
computedStates,
|
2015-07-19 18:21:10 +03:00
|
|
|
currentStateIndex,
|
2015-07-31 07:20:33 +03:00
|
|
|
timestamps
|
2015-09-28 18:38:33 +03:00
|
|
|
} = historyState;
|
2015-07-15 00:09:54 +03:00
|
|
|
|
2015-09-28 18:38:33 +03:00
|
|
|
switch (historyAction.type) {
|
2015-07-15 00:09:54 +03:00
|
|
|
case ActionTypes.RESET:
|
2015-09-28 18:38:33 +03:00
|
|
|
committedState = initialCommittedState;
|
2015-07-15 00:09:54 +03:00
|
|
|
stagedActions = [INIT_ACTION];
|
|
|
|
skippedActions = {};
|
2015-07-15 01:43:09 +03:00
|
|
|
currentStateIndex = 0;
|
2015-09-28 18:38:33 +03:00
|
|
|
timestamps = [historyAction.timestamp];
|
2015-07-15 00:09:54 +03:00
|
|
|
break;
|
|
|
|
case ActionTypes.COMMIT:
|
2015-07-15 01:43:09 +03:00
|
|
|
committedState = computedStates[currentStateIndex].state;
|
2015-07-15 00:09:54 +03:00
|
|
|
stagedActions = [INIT_ACTION];
|
|
|
|
skippedActions = {};
|
2015-07-15 01:43:09 +03:00
|
|
|
currentStateIndex = 0;
|
2015-09-28 18:38:33 +03:00
|
|
|
timestamps = [historyAction.timestamp];
|
2015-07-15 00:09:54 +03:00
|
|
|
break;
|
|
|
|
case ActionTypes.ROLLBACK:
|
|
|
|
stagedActions = [INIT_ACTION];
|
|
|
|
skippedActions = {};
|
2015-07-15 01:43:09 +03:00
|
|
|
currentStateIndex = 0;
|
2015-09-28 18:38:33 +03:00
|
|
|
timestamps = [historyAction.timestamp];
|
2015-07-15 00:09:54 +03:00
|
|
|
break;
|
|
|
|
case ActionTypes.TOGGLE_ACTION:
|
2015-09-28 18:38:33 +03:00
|
|
|
skippedActions = toggle(skippedActions, historyAction.index);
|
2015-07-15 01:43:09 +03:00
|
|
|
break;
|
|
|
|
case ActionTypes.JUMP_TO_STATE:
|
2015-09-28 18:38:33 +03:00
|
|
|
currentStateIndex = historyAction.index;
|
2015-09-27 17:02:32 +03:00
|
|
|
// Optimization: we know the history has not changed.
|
|
|
|
shouldRecomputeStates = false;
|
2015-07-15 00:09:54 +03:00
|
|
|
break;
|
|
|
|
case ActionTypes.SWEEP:
|
|
|
|
stagedActions = stagedActions.filter((_, i) => !skippedActions[i]);
|
2015-07-31 07:20:33 +03:00
|
|
|
timestamps = timestamps.filter((_, i) => !skippedActions[i]);
|
2015-07-15 00:09:54 +03:00
|
|
|
skippedActions = {};
|
2015-07-23 07:33:31 +03:00
|
|
|
currentStateIndex = Math.min(currentStateIndex, stagedActions.length - 1);
|
2015-07-15 00:09:54 +03:00
|
|
|
break;
|
|
|
|
case ActionTypes.PERFORM_ACTION:
|
2015-07-15 01:43:09 +03:00
|
|
|
if (currentStateIndex === stagedActions.length - 1) {
|
|
|
|
currentStateIndex++;
|
|
|
|
}
|
2015-09-27 17:02:32 +03:00
|
|
|
|
2015-09-28 18:38:33 +03:00
|
|
|
stagedActions = [...stagedActions, historyAction.action];
|
|
|
|
timestamps = [...timestamps, historyAction.timestamp];
|
2015-09-27 17:02:32 +03:00
|
|
|
|
|
|
|
// Optimization: we know that the past has not changed.
|
|
|
|
shouldRecomputeStates = false;
|
|
|
|
// Instead of recomputing the states, append the next one.
|
|
|
|
const previousEntry = computedStates[computedStates.length - 1];
|
|
|
|
const nextEntry = computeNextEntry(
|
|
|
|
reducer,
|
2015-09-28 18:38:33 +03:00
|
|
|
historyAction.action,
|
2015-09-27 17:02:32 +03:00
|
|
|
previousEntry.state,
|
|
|
|
previousEntry.error
|
|
|
|
);
|
|
|
|
computedStates = [...computedStates, nextEntry];
|
2015-07-15 00:09:54 +03:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2015-09-27 17:02:32 +03:00
|
|
|
if (shouldRecomputeStates) {
|
|
|
|
computedStates = recomputeStates(
|
|
|
|
reducer,
|
|
|
|
committedState,
|
|
|
|
stagedActions,
|
|
|
|
skippedActions
|
|
|
|
);
|
|
|
|
}
|
2015-07-15 00:09:54 +03:00
|
|
|
|
|
|
|
return {
|
|
|
|
committedState,
|
|
|
|
stagedActions,
|
|
|
|
skippedActions,
|
2015-07-15 01:43:09 +03:00
|
|
|
computedStates,
|
2015-07-19 18:21:10 +03:00
|
|
|
currentStateIndex,
|
2015-07-31 07:20:33 +03:00
|
|
|
timestamps
|
2015-07-15 00:09:54 +03:00
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2015-09-28 18:38:33 +03:00
|
|
|
* Provides a view into the History state that matches the current app state.
|
2015-07-15 00:09:54 +03:00
|
|
|
*/
|
2015-09-28 18:38:33 +03:00
|
|
|
function selectAppState(instrumentedState) {
|
|
|
|
const { computedStates, currentStateIndex } = instrumentedState.historyState;
|
2015-07-15 01:43:09 +03:00
|
|
|
const { state } = computedStates[currentStateIndex];
|
2015-07-15 00:09:54 +03:00
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2015-09-28 18:38:33 +03:00
|
|
|
* Deinstruments the History store to act like the app's store.
|
2015-07-15 00:09:54 +03:00
|
|
|
*/
|
2015-09-28 18:38:33 +03:00
|
|
|
function selectAppStore(instrumentedStore, instrumentReducer) {
|
2015-09-20 13:49:37 +03:00
|
|
|
let lastDefinedState;
|
2015-09-28 03:52:10 +03:00
|
|
|
|
2015-07-15 00:09:54 +03:00
|
|
|
return {
|
2015-09-28 18:38:33 +03:00
|
|
|
...instrumentedStore,
|
2015-09-28 03:52:10 +03:00
|
|
|
|
2015-09-28 18:38:33 +03:00
|
|
|
instrumentedStore,
|
2015-09-28 03:52:10 +03:00
|
|
|
|
2015-07-15 00:09:54 +03:00
|
|
|
dispatch(action) {
|
2015-09-28 18:38:33 +03:00
|
|
|
instrumentedStore.dispatch(ActionCreators.performAction(action));
|
2015-07-15 00:09:54 +03:00
|
|
|
return action;
|
|
|
|
},
|
2015-09-28 03:52:10 +03:00
|
|
|
|
2015-07-15 00:09:54 +03:00
|
|
|
getState() {
|
2015-09-28 18:38:33 +03:00
|
|
|
const state = selectAppState(instrumentedStore.getState());
|
2015-09-20 13:49:37 +03:00
|
|
|
if (state !== undefined) {
|
|
|
|
lastDefinedState = state;
|
|
|
|
}
|
|
|
|
return lastDefinedState;
|
2015-07-20 22:42:47 +03:00
|
|
|
},
|
2015-09-28 03:52:10 +03:00
|
|
|
|
2015-07-20 22:42:47 +03:00
|
|
|
replaceReducer(nextReducer) {
|
2015-09-28 18:38:33 +03:00
|
|
|
instrumentedStore.replaceReducer(instrumentReducer(nextReducer));
|
2015-07-15 00:09:54 +03:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2015-09-28 18:38:33 +03:00
|
|
|
* Redux History store enhancer.
|
2015-07-15 00:09:54 +03:00
|
|
|
*/
|
2015-09-28 18:38:33 +03:00
|
|
|
export default function instrument(monitorReducer = () => null) {
|
|
|
|
return createStore => (reducer, initialState) => {
|
|
|
|
function instrumentReducer(r) {
|
|
|
|
const historyReducer = createHistoryReducer(r, initialState);
|
|
|
|
return ({ historyState, monitorState } = {}, action) => ({
|
|
|
|
historyState: historyReducer(historyState, action),
|
|
|
|
monitorState: monitorReducer(monitorState, action)
|
|
|
|
});
|
|
|
|
}
|
2015-07-15 00:09:54 +03:00
|
|
|
|
2015-09-28 18:38:33 +03:00
|
|
|
const instrumentedStore = createStore(instrumentReducer(reducer));
|
|
|
|
return selectAppStore(instrumentedStore, instrumentReducer);
|
2015-07-15 00:09:54 +03:00
|
|
|
};
|
|
|
|
}
|