2015-10-17 03:53:43 +03:00
|
|
|
import difference from 'lodash/array/difference';
|
|
|
|
|
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-10-17 02:34:46 +03:00
|
|
|
JUMP_TO_STATE: 'JUMP_TO_STATE',
|
|
|
|
IMPORT_STATE: 'IMPORT_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 };
|
|
|
|
},
|
|
|
|
|
2015-10-17 03:53:43 +03:00
|
|
|
toggleAction(id) {
|
|
|
|
return { type: ActionTypes.TOGGLE_ACTION, id };
|
2015-09-28 18:38:33 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
jumpToState(index) {
|
|
|
|
return { type: ActionTypes.JUMP_TO_STATE, index };
|
2015-10-17 02:34:46 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
importState(nextLiftedState) {
|
|
|
|
return { type: ActionTypes.IMPORT_STATE, nextLiftedState };
|
2015-09-28 18:38:33 +03:00
|
|
|
}
|
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
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2015-10-17 03:53:43 +03:00
|
|
|
function recomputeStates(reducer, committedState, actionsById, stagedActionIds, skippedActionIds) {
|
2015-07-15 00:09:54 +03:00
|
|
|
const computedStates = [];
|
2015-10-17 03:53:43 +03:00
|
|
|
for (let i = 0; i < stagedActionIds.length; i++) {
|
|
|
|
const actionId = stagedActionIds[i];
|
|
|
|
const action = actionsById[actionId].action;
|
2015-07-15 00:09:54 +03:00
|
|
|
|
|
|
|
const previousEntry = computedStates[i - 1];
|
|
|
|
const previousState = previousEntry ? previousEntry.state : committedState;
|
|
|
|
const previousError = previousEntry ? previousEntry.error : undefined;
|
|
|
|
|
2015-10-17 03:53:43 +03:00
|
|
|
const shouldSkip = skippedActionIds.indexOf(actionId) > -1;
|
2015-07-15 00:09:54 +03:00
|
|
|
const entry = shouldSkip ?
|
|
|
|
previousEntry :
|
|
|
|
computeNextEntry(reducer, action, previousState, previousError);
|
|
|
|
|
|
|
|
computedStates.push(entry);
|
|
|
|
}
|
|
|
|
|
|
|
|
return computedStates;
|
|
|
|
}
|
|
|
|
|
2015-10-17 03:53:43 +03:00
|
|
|
/**
|
|
|
|
* Lifts an app's action into an action on the lifted store.
|
|
|
|
*/
|
|
|
|
function liftAction(action) {
|
|
|
|
return ActionCreators.performAction(action);
|
|
|
|
}
|
|
|
|
|
2015-07-15 00:09:54 +03:00
|
|
|
/**
|
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-10-17 02:21:07 +03:00
|
|
|
function liftReducerWith(reducer, initialCommittedState, monitorReducer) {
|
|
|
|
const initialLiftedState = {
|
2015-10-17 03:53:43 +03:00
|
|
|
monitorState: monitorReducer(undefined, {}),
|
|
|
|
nextActionId: 1,
|
|
|
|
actionsById: {
|
|
|
|
0: liftAction(INIT_ACTION)
|
|
|
|
},
|
|
|
|
stagedActionIds: [0],
|
|
|
|
skippedActionIds: [],
|
2015-09-28 03:52:10 +03:00
|
|
|
committedState: initialCommittedState,
|
2015-07-19 18:21:10 +03:00
|
|
|
currentStateIndex: 0,
|
2015-10-17 03:53:43 +03:00
|
|
|
computedStates: undefined
|
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-10-17 02:21:07 +03:00
|
|
|
return (liftedState = initialLiftedState, liftedAction) => {
|
2015-09-27 17:02:32 +03:00
|
|
|
let shouldRecomputeStates = true;
|
2015-07-15 00:09:54 +03:00
|
|
|
let {
|
2015-10-17 02:21:07 +03:00
|
|
|
monitorState,
|
2015-10-17 03:53:43 +03:00
|
|
|
actionsById,
|
|
|
|
nextActionId,
|
|
|
|
stagedActionIds,
|
|
|
|
skippedActionIds,
|
2015-07-15 00:09:54 +03:00
|
|
|
committedState,
|
2015-07-19 18:21:10 +03:00
|
|
|
currentStateIndex,
|
2015-10-17 03:53:43 +03:00
|
|
|
computedStates
|
2015-10-17 02:21:07 +03:00
|
|
|
} = liftedState;
|
2015-07-15 00:09:54 +03:00
|
|
|
|
2015-10-17 02:21:07 +03:00
|
|
|
switch (liftedAction.type) {
|
2015-07-15 00:09:54 +03:00
|
|
|
case ActionTypes.RESET:
|
2015-10-17 03:53:43 +03:00
|
|
|
actionsById = {
|
|
|
|
0: liftAction(INIT_ACTION)
|
|
|
|
};
|
|
|
|
nextActionId = 1;
|
|
|
|
stagedActionIds = [0];
|
|
|
|
skippedActionIds = [];
|
2015-09-28 18:38:33 +03:00
|
|
|
committedState = initialCommittedState;
|
2015-07-15 01:43:09 +03:00
|
|
|
currentStateIndex = 0;
|
2015-07-15 00:09:54 +03:00
|
|
|
break;
|
|
|
|
case ActionTypes.COMMIT:
|
2015-10-17 03:53:43 +03:00
|
|
|
actionsById = {
|
|
|
|
0: liftAction(INIT_ACTION)
|
|
|
|
};
|
|
|
|
nextActionId = 1;
|
|
|
|
stagedActionIds = [0];
|
|
|
|
skippedActionIds = [];
|
2015-07-15 01:43:09 +03:00
|
|
|
committedState = computedStates[currentStateIndex].state;
|
|
|
|
currentStateIndex = 0;
|
2015-07-15 00:09:54 +03:00
|
|
|
break;
|
|
|
|
case ActionTypes.ROLLBACK:
|
2015-10-17 03:53:43 +03:00
|
|
|
actionsById = {
|
|
|
|
0: liftAction(INIT_ACTION)
|
|
|
|
};
|
|
|
|
nextActionId = 1;
|
|
|
|
stagedActionIds = [0];
|
|
|
|
skippedActionIds = [];
|
2015-07-15 01:43:09 +03:00
|
|
|
currentStateIndex = 0;
|
2015-07-15 00:09:54 +03:00
|
|
|
break;
|
|
|
|
case ActionTypes.TOGGLE_ACTION:
|
2015-10-17 03:53:43 +03:00
|
|
|
const index = skippedActionIds.indexOf(liftedAction.id);
|
|
|
|
if (index === -1) {
|
|
|
|
skippedActionIds = [
|
|
|
|
liftedAction.id,
|
|
|
|
...skippedActionIds
|
|
|
|
];
|
|
|
|
} else {
|
|
|
|
skippedActionIds = [
|
|
|
|
...skippedActionIds.slice(0, index),
|
|
|
|
...skippedActionIds.slice(index + 1)
|
|
|
|
];
|
|
|
|
}
|
2015-07-15 01:43:09 +03:00
|
|
|
break;
|
|
|
|
case ActionTypes.JUMP_TO_STATE:
|
2015-10-17 02:21:07 +03:00
|
|
|
currentStateIndex = liftedAction.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:
|
2015-10-17 03:53:43 +03:00
|
|
|
stagedActionIds = difference(stagedActionIds, skippedActionIds);
|
|
|
|
skippedActionIds = [];
|
|
|
|
currentStateIndex = Math.min(currentStateIndex, stagedActionIds.length - 1);
|
2015-07-15 00:09:54 +03:00
|
|
|
break;
|
|
|
|
case ActionTypes.PERFORM_ACTION:
|
2015-10-17 03:53:43 +03:00
|
|
|
if (currentStateIndex === stagedActionIds.length - 1) {
|
2015-07-15 01:43:09 +03:00
|
|
|
currentStateIndex++;
|
|
|
|
}
|
2015-09-27 17:02:32 +03:00
|
|
|
|
2015-10-17 03:53:43 +03:00
|
|
|
const actionId = nextActionId++;
|
|
|
|
// Mutation! This is the hottest path, and we optimize on purpose.
|
|
|
|
// It is safe because we set a new key in a cache dictionary.
|
|
|
|
actionsById[actionId] = liftedAction;
|
|
|
|
stagedActionIds = [...stagedActionIds, actionId];
|
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-10-17 02:21:07 +03:00
|
|
|
liftedAction.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;
|
2015-10-17 02:34:46 +03:00
|
|
|
case ActionTypes.IMPORT_STATE:
|
|
|
|
({
|
2015-10-17 03:53:43 +03:00
|
|
|
monitorState,
|
|
|
|
actionsById,
|
|
|
|
nextActionId,
|
|
|
|
stagedActionIds,
|
|
|
|
skippedActionIds,
|
|
|
|
committedState,
|
2015-10-17 02:34:46 +03:00
|
|
|
currentStateIndex,
|
2015-10-17 03:53:43 +03:00
|
|
|
computedStates
|
2015-10-17 02:34:46 +03:00
|
|
|
} = liftedAction.nextLiftedState);
|
|
|
|
break;
|
2015-10-17 02:21:07 +03:00
|
|
|
case '@@redux/INIT':
|
|
|
|
// Always recompute states on hot reload and init.
|
|
|
|
shouldRecomputeStates = true;
|
|
|
|
break;
|
2015-07-15 00:09:54 +03:00
|
|
|
default:
|
2015-10-17 02:21:07 +03:00
|
|
|
// Optimization: a monitor action can't change history.
|
|
|
|
shouldRecomputeStates = false;
|
2015-07-15 00:09:54 +03:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2015-09-27 17:02:32 +03:00
|
|
|
if (shouldRecomputeStates) {
|
|
|
|
computedStates = recomputeStates(
|
|
|
|
reducer,
|
|
|
|
committedState,
|
2015-10-17 03:53:43 +03:00
|
|
|
actionsById,
|
|
|
|
stagedActionIds,
|
|
|
|
skippedActionIds
|
2015-09-27 17:02:32 +03:00
|
|
|
);
|
|
|
|
}
|
2015-07-15 00:09:54 +03:00
|
|
|
|
2015-10-17 02:21:07 +03:00
|
|
|
monitorState = monitorReducer(monitorState, liftedAction);
|
|
|
|
|
2015-07-15 00:09:54 +03:00
|
|
|
return {
|
2015-10-17 03:53:43 +03:00
|
|
|
monitorState,
|
|
|
|
actionsById,
|
|
|
|
nextActionId,
|
|
|
|
stagedActionIds,
|
|
|
|
skippedActionIds,
|
2015-07-15 00:09:54 +03:00
|
|
|
committedState,
|
2015-07-19 18:21:10 +03:00
|
|
|
currentStateIndex,
|
2015-10-17 03:53:43 +03:00
|
|
|
computedStates
|
2015-07-15 00:09:54 +03:00
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2015-10-17 02:21:07 +03:00
|
|
|
* Provides an app's view into the state of the lifted store.
|
2015-07-15 00:09:54 +03:00
|
|
|
*/
|
2015-10-17 02:21:07 +03:00
|
|
|
function unliftState(liftedState) {
|
|
|
|
const { computedStates, currentStateIndex } = liftedState;
|
2015-07-15 01:43:09 +03:00
|
|
|
const { state } = computedStates[currentStateIndex];
|
2015-07-15 00:09:54 +03:00
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
2015-10-17 02:21:07 +03:00
|
|
|
/**
|
|
|
|
* Provides an app's view into the lifted store.
|
2015-07-15 00:09:54 +03:00
|
|
|
*/
|
2015-10-17 02:21:07 +03:00
|
|
|
function unliftStore(liftedStore, liftReducer) {
|
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-10-17 02:21:07 +03:00
|
|
|
...liftedStore,
|
2015-09-28 03:52:10 +03:00
|
|
|
|
2015-10-17 02:21:07 +03:00
|
|
|
liftedStore,
|
2015-09-28 03:52:10 +03:00
|
|
|
|
2015-07-15 00:09:54 +03:00
|
|
|
dispatch(action) {
|
2015-10-17 02:21:07 +03:00
|
|
|
liftedStore.dispatch(liftAction(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-10-17 02:21:07 +03:00
|
|
|
const state = unliftState(liftedStore.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-10-17 02:21:07 +03:00
|
|
|
liftedStore.replaceReducer(liftReducer(nextReducer));
|
2015-07-15 00:09:54 +03:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2015-10-17 02:34:46 +03:00
|
|
|
* Redux instrumentation 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) => {
|
2015-10-17 02:21:07 +03:00
|
|
|
function liftReducer(r) {
|
|
|
|
return liftReducerWith(r, initialState, monitorReducer);
|
2015-09-28 18:38:33 +03:00
|
|
|
}
|
2015-07-15 00:09:54 +03:00
|
|
|
|
2015-10-17 02:21:07 +03:00
|
|
|
const liftedStore = createStore(liftReducer(reducer));
|
|
|
|
return unliftStore(liftedStore, liftReducer);
|
2015-07-15 00:09:54 +03:00
|
|
|
};
|
|
|
|
}
|