2015-09-28 03:52:10 +03:00
|
|
|
import { combineReducers, compose } from 'redux';
|
|
|
|
|
2015-07-15 00:09:54 +03:00
|
|
|
const ActionTypes = {
|
|
|
|
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
|
|
|
};
|
|
|
|
|
|
|
|
const INIT_ACTION = {
|
|
|
|
type: '@@INIT'
|
|
|
|
};
|
|
|
|
|
|
|
|
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.
|
|
|
|
* It's probably a good idea to do this only if the code has changed,
|
|
|
|
* but until we have some tests we'll just do it every time an action fires.
|
|
|
|
*/
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Lifts the app state reducer into a DevTools state reducer.
|
|
|
|
*/
|
2015-09-28 03:52:10 +03:00
|
|
|
function createDevToolsStateReducer(reducer, initialCommittedState) {
|
|
|
|
const initialState = {
|
|
|
|
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
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Manages how the DevTools actions modify the DevTools state.
|
|
|
|
*/
|
2015-09-28 03:52:10 +03:00
|
|
|
return function devToolsState(state = initialState, action) {
|
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 03:52:10 +03:00
|
|
|
} = state;
|
2015-07-15 00:09:54 +03:00
|
|
|
|
2015-09-28 03:52:10 +03:00
|
|
|
switch (action.type) {
|
2015-07-15 00:09:54 +03:00
|
|
|
case ActionTypes.RESET:
|
|
|
|
committedState = initialState;
|
|
|
|
stagedActions = [INIT_ACTION];
|
|
|
|
skippedActions = {};
|
2015-07-15 01:43:09 +03:00
|
|
|
currentStateIndex = 0;
|
2015-09-28 03:52:10 +03:00
|
|
|
timestamps = [action.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 03:52:10 +03:00
|
|
|
timestamps = [action.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 03:52:10 +03:00
|
|
|
timestamps = [action.timestamp];
|
2015-07-15 00:09:54 +03:00
|
|
|
break;
|
|
|
|
case ActionTypes.TOGGLE_ACTION:
|
2015-09-28 03:52:10 +03:00
|
|
|
skippedActions = toggle(skippedActions, action.index);
|
2015-07-15 01:43:09 +03:00
|
|
|
break;
|
|
|
|
case ActionTypes.JUMP_TO_STATE:
|
2015-09-28 03:52:10 +03:00
|
|
|
currentStateIndex = action.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 03:52:10 +03:00
|
|
|
stagedActions = [...stagedActions, action.action];
|
|
|
|
timestamps = [...timestamps, action.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 03:52:10 +03:00
|
|
|
action.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
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Lifts an app action to a DevTools action.
|
|
|
|
*/
|
|
|
|
function liftAction(action) {
|
2015-07-31 07:20:33 +03:00
|
|
|
const liftedAction = {
|
|
|
|
type: ActionTypes.PERFORM_ACTION,
|
|
|
|
action,
|
|
|
|
timestamp: Date.now()
|
|
|
|
};
|
2015-07-15 00:09:54 +03:00
|
|
|
return liftedAction;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Unlifts the DevTools state to the app state.
|
|
|
|
*/
|
|
|
|
function unliftState(liftedState) {
|
2015-09-28 03:52:10 +03:00
|
|
|
const { computedStates, currentStateIndex } = liftedState.devToolsState;
|
2015-07-15 01:43:09 +03:00
|
|
|
const { state } = computedStates[currentStateIndex];
|
2015-07-15 00:09:54 +03:00
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Unlifts the DevTools store to act like the app's store.
|
|
|
|
*/
|
2015-09-28 03:52:10 +03:00
|
|
|
function mapToComputedStateStore(devToolsStore, wrapReducer) {
|
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 03:52:10 +03:00
|
|
|
...devToolsStore,
|
|
|
|
|
|
|
|
devToolsStore,
|
|
|
|
|
2015-07-15 00:09:54 +03:00
|
|
|
dispatch(action) {
|
2015-09-28 03:52:10 +03:00
|
|
|
devToolsStore.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-09-28 03:52:10 +03:00
|
|
|
const state = unliftState(devToolsStore.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 03:52:10 +03:00
|
|
|
devToolsStore.replaceReducer(wrapReducer(nextReducer));
|
2015-07-15 00:09:54 +03:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Action creators to change the DevTools state.
|
|
|
|
*/
|
|
|
|
export const ActionCreators = {
|
|
|
|
reset() {
|
2015-07-31 07:20:33 +03:00
|
|
|
return { type: ActionTypes.RESET, timestamp: Date.now() };
|
2015-07-15 00:09:54 +03:00
|
|
|
},
|
2015-09-28 03:52:10 +03:00
|
|
|
|
2015-07-15 00:09:54 +03:00
|
|
|
rollback() {
|
2015-07-31 07:20:33 +03:00
|
|
|
return { type: ActionTypes.ROLLBACK, timestamp: Date.now() };
|
2015-07-15 00:09:54 +03:00
|
|
|
},
|
2015-09-28 03:52:10 +03:00
|
|
|
|
2015-07-15 00:09:54 +03:00
|
|
|
commit() {
|
2015-07-31 07:20:33 +03:00
|
|
|
return { type: ActionTypes.COMMIT, timestamp: Date.now() };
|
2015-07-15 00:09:54 +03:00
|
|
|
},
|
2015-09-28 03:52:10 +03:00
|
|
|
|
2015-07-15 00:09:54 +03:00
|
|
|
sweep() {
|
|
|
|
return { type: ActionTypes.SWEEP };
|
|
|
|
},
|
2015-09-28 03:52:10 +03:00
|
|
|
|
2015-07-15 00:09:54 +03:00
|
|
|
toggleAction(index) {
|
|
|
|
return { type: ActionTypes.TOGGLE_ACTION, index };
|
2015-07-15 01:43:09 +03:00
|
|
|
},
|
2015-09-28 03:52:10 +03:00
|
|
|
|
2015-07-15 01:43:09 +03:00
|
|
|
jumpToState(index) {
|
|
|
|
return { type: ActionTypes.JUMP_TO_STATE, index };
|
2015-07-15 00:09:54 +03:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2015-09-27 16:34:23 +03:00
|
|
|
* Redux DevTools store enhancer.
|
2015-07-15 00:09:54 +03:00
|
|
|
*/
|
2015-09-28 13:06:05 +03:00
|
|
|
export default function enhance(monitorReducer = () => null) {
|
2015-07-15 00:09:54 +03:00
|
|
|
return next => (reducer, initialState) => {
|
2015-09-28 03:52:10 +03:00
|
|
|
const wrapReducer = (r) => combineReducers({
|
|
|
|
devToolsState: createDevToolsStateReducer(r, initialState),
|
2015-09-28 13:06:05 +03:00
|
|
|
monitorState: monitorReducer
|
2015-09-28 03:52:10 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
const devToolsStore = next(wrapReducer(reducer));
|
|
|
|
return mapToComputedStateStore(devToolsStore, wrapReducer);
|
2015-07-15 00:09:54 +03:00
|
|
|
};
|
|
|
|
}
|