diff --git a/examples/counter/package.json b/examples/counter/package.json index 98833f56..029efc70 100644 --- a/examples/counter/package.json +++ b/examples/counter/package.json @@ -28,8 +28,8 @@ "node-libs-browser": "^0.5.2", "react-dock": "^0.1.0", "react-hot-loader": "^1.3.0", - "redux-devtools": "^3.0.0-alpha-6", - "redux-devtools-log-monitor": "^1.0.0-alpha-6", + "redux-devtools": "^3.0.0-alpha-7", + "redux-devtools-log-monitor": "^1.0.0-alpha-7", "webpack": "^1.9.11", "webpack-dev-server": "^1.9.0" } diff --git a/examples/counter/src/store/configureStore.dev.js b/examples/counter/src/store/configureStore.dev.js index 58aa355c..d157ef17 100644 --- a/examples/counter/src/store/configureStore.dev.js +++ b/examples/counter/src/store/configureStore.dev.js @@ -6,7 +6,7 @@ import DevTools from '../containers/DevTools'; const finalCreateStore = compose( applyMiddleware(thunk), - DevTools.enhance, + DevTools.instrument(), persistState( window.location.href.match( /[?&]debug_session=([^&]+)\b/ diff --git a/src/bindActionCreatorsRecursively.js b/src/bindActionCreatorsDeep.js similarity index 71% rename from src/bindActionCreatorsRecursively.js rename to src/bindActionCreatorsDeep.js index d543fad4..8c8626a6 100644 --- a/src/bindActionCreatorsRecursively.js +++ b/src/bindActionCreatorsDeep.js @@ -1,13 +1,13 @@ import { bindActionCreators } from 'redux'; -export default function bindActionCreatorsRecursively(actionCreators, dispatch) { +export default function bindActionCreatorsDeep(actionCreators, dispatch) { return Object.keys(actionCreators).reduce((result, key) => { if (!actionCreators[key]) { return result; } switch (typeof actionCreators[key]) { case 'object': - result[key] = bindActionCreatorsRecursively(actionCreators[key], dispatch); + result[key] = bindActionCreatorsDeep(actionCreators[key], dispatch); break; case 'function': result[key] = bindActionCreators(actionCreators[key], dispatch); diff --git a/src/connectMonitor.js b/src/connectMonitor.js index 3d8e5649..517f9adf 100644 --- a/src/connectMonitor.js +++ b/src/connectMonitor.js @@ -1,7 +1,7 @@ import { bindActionCreators } from 'redux'; -import bindActionCreatorsRecursively from './bindActionCreatorsRecursively'; +import bindActionCreatorsDeep from './bindActionCreatorsDeep'; import { connect } from 'react-redux'; -import { ActionCreators as devToolsActionCreators } from './enhance'; +import { ActionCreators as historyActionCreators } from './instrument'; export default function connectMonitor({ component, @@ -10,15 +10,15 @@ export default function connectMonitor({ }) { function mapStateToProps(state) { return { - devToolsState: state.devToolsState, + historyState: state.historyState, monitorState: state.monitorState }; } function mapDispatchToProps(dispatch) { return { - devToolsActions: bindActionCreators(devToolsActionCreators, dispatch), - monitorActions: bindActionCreatorsRecursively(actionCreators, dispatch) + historyActions: bindActionCreators(historyActionCreators, dispatch), + monitorActions: bindActionCreatorsDeep(actionCreators, dispatch) }; } diff --git a/src/createDevTools.js b/src/createDevTools.js index 218e3bae..f3281343 100644 --- a/src/createDevTools.js +++ b/src/createDevTools.js @@ -1,5 +1,5 @@ import React, { Component, PropTypes } from 'react'; -import enhance from './enhance'; +import instrument from './instrument'; import connectMonitor from './connectMonitor'; export default function createDevTools(monitor) { @@ -10,12 +10,12 @@ export default function createDevTools(monitor) { store: PropTypes.object.isRequired }; - static enhance = enhance(Monitor.reducer); + static instrument = () => instrument(Monitor.reducer); render() { return ( + store={this.context.store.instrumentedStore} /> ); } }; diff --git a/src/index.js b/src/index.js index 427073f2..5b3c3a35 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,3 @@ -export { default, ActionCreators, ActionTypes } from './enhance'; +export { default as instrument, ActionTypes } from './instrument'; export { default as persistState } from './persistState'; export { default as createDevTools } from './createDevTools'; diff --git a/src/enhance.js b/src/instrument.js similarity index 68% rename from src/enhance.js rename to src/instrument.js index 6f611a49..af57bd3d 100644 --- a/src/enhance.js +++ b/src/instrument.js @@ -1,6 +1,6 @@ import { combineReducers } from 'redux'; -const ActionTypes = { +export const ActionTypes = { PERFORM_ACTION: 'PERFORM_ACTION', RESET: 'RESET', ROLLBACK: 'ROLLBACK', @@ -10,10 +10,41 @@ const ActionTypes = { JUMP_TO_STATE: 'JUMP_TO_STATE' }; -const INIT_ACTION = { - type: '@@INIT' +/** + * 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 }; + } }; +const INIT_ACTION = { type: '@@INIT' }; + function toggle(obj, key) { const clone = { ...obj }; if (clone[key]) { @@ -52,8 +83,6 @@ function computeNextEntry(reducer, action, state, error) { /** * 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 = []; @@ -77,10 +106,10 @@ function recomputeStates(reducer, committedState, stagedActions, skippedActions) } /** - * Lifts the app state reducer into a DevTools state reducer. + * Creates a history state reducer from an app's reducer. */ -function createDevToolsStateReducer(reducer, initialCommittedState) { - const initialState = { +function createHistoryReducer(reducer, initialCommittedState) { + const initialHistoryState = { committedState: initialCommittedState, stagedActions: [INIT_ACTION], skippedActions: {}, @@ -89,9 +118,9 @@ function createDevToolsStateReducer(reducer, initialCommittedState) { }; /** - * Manages how the DevTools actions modify the DevTools state. + * Manages how the history actions modify the history state. */ - return function devToolsState(state = initialState, action) { + return (historyState = initialHistoryState, historyAction) => { let shouldRecomputeStates = true; let { committedState, @@ -100,34 +129,34 @@ function createDevToolsStateReducer(reducer, initialCommittedState) { computedStates, currentStateIndex, timestamps - } = state; + } = historyState; - switch (action.type) { + switch (historyAction.type) { case ActionTypes.RESET: - committedState = initialState; + committedState = initialCommittedState; stagedActions = [INIT_ACTION]; skippedActions = {}; currentStateIndex = 0; - timestamps = [action.timestamp]; + timestamps = [historyAction.timestamp]; break; case ActionTypes.COMMIT: committedState = computedStates[currentStateIndex].state; stagedActions = [INIT_ACTION]; skippedActions = {}; currentStateIndex = 0; - timestamps = [action.timestamp]; + timestamps = [historyAction.timestamp]; break; case ActionTypes.ROLLBACK: stagedActions = [INIT_ACTION]; skippedActions = {}; currentStateIndex = 0; - timestamps = [action.timestamp]; + timestamps = [historyAction.timestamp]; break; case ActionTypes.TOGGLE_ACTION: - skippedActions = toggle(skippedActions, action.index); + skippedActions = toggle(skippedActions, historyAction.index); break; case ActionTypes.JUMP_TO_STATE: - currentStateIndex = action.index; + currentStateIndex = historyAction.index; // Optimization: we know the history has not changed. shouldRecomputeStates = false; break; @@ -142,8 +171,8 @@ function createDevToolsStateReducer(reducer, initialCommittedState) { currentStateIndex++; } - stagedActions = [...stagedActions, action.action]; - timestamps = [...timestamps, action.timestamp]; + stagedActions = [...stagedActions, historyAction.action]; + timestamps = [...timestamps, historyAction.timestamp]; // Optimization: we know that the past has not changed. shouldRecomputeStates = false; @@ -151,7 +180,7 @@ function createDevToolsStateReducer(reducer, initialCommittedState) { const previousEntry = computedStates[computedStates.length - 1]; const nextEntry = computeNextEntry( reducer, - action.action, + historyAction.action, previousEntry.state, previousEntry.error ); @@ -182,44 +211,32 @@ function createDevToolsStateReducer(reducer, initialCommittedState) { } /** - * Lifts an app action to a DevTools action. + * Provides a view into the History state that matches the current app state. */ -function liftAction(action) { - const liftedAction = { - type: ActionTypes.PERFORM_ACTION, - action, - timestamp: Date.now() - }; - return liftedAction; -} - -/** - * Unlifts the DevTools state to the app state. - */ -function unliftState(liftedState) { - const { computedStates, currentStateIndex } = liftedState.devToolsState; +function selectAppState(instrumentedState) { + const { computedStates, currentStateIndex } = instrumentedState.historyState; const { state } = computedStates[currentStateIndex]; return state; } /** - * Unlifts the DevTools store to act like the app's store. + * Deinstruments the History store to act like the app's store. */ -function mapToComputedStateStore(devToolsStore, wrapReducer) { +function selectAppStore(instrumentedStore, instrumentReducer) { let lastDefinedState; return { - ...devToolsStore, + ...instrumentedStore, - devToolsStore, + instrumentedStore, dispatch(action) { - devToolsStore.dispatch(liftAction(action)); + instrumentedStore.dispatch(ActionCreators.performAction(action)); return action; }, getState() { - const state = unliftState(devToolsStore.getState()); + const state = selectAppState(instrumentedStore.getState()); if (state !== undefined) { lastDefinedState = state; } @@ -227,51 +244,25 @@ function mapToComputedStateStore(devToolsStore, wrapReducer) { }, replaceReducer(nextReducer) { - devToolsStore.replaceReducer(wrapReducer(nextReducer)); + instrumentedStore.replaceReducer(instrumentReducer(nextReducer)); } }; } /** - * Action creators to change the DevTools state. + * Redux History store enhancer. */ -export const ActionCreators = { - reset() { - return { type: ActionTypes.RESET, timestamp: Date.now() }; - }, +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) + }); + } - 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 }; - } -}; - -/** - * Redux DevTools store enhancer. - */ -export default function enhance(monitorReducer = () => null) { - return next => (reducer, initialState) => { - const wrapReducer = (r) => combineReducers({ - devToolsState: createDevToolsStateReducer(r, initialState), - monitorState: monitorReducer - }); - - const devToolsStore = next(wrapReducer(reducer)); - return mapToComputedStateStore(devToolsStore, wrapReducer); + const instrumentedStore = createStore(instrumentReducer(reducer)); + return selectAppStore(instrumentedStore, instrumentReducer); }; } diff --git a/src/persistState.js b/src/persistState.js index c12e49ce..0aa61e89 100644 --- a/src/persistState.js +++ b/src/persistState.js @@ -1,51 +1,34 @@ -export default function persistState(sessionId, stateDeserializer = null, actionDeserializer = null) { +const identity = _ => _; +export default function persistState(sessionId, deserializeState = identity, deserializeAction = identity) { if (!sessionId) { return next => (...args) => next(...args); } - function deserializeState(fullState) { + function deserialize({ historyState, ...rest }) { return { - ...fullState, - committedState: stateDeserializer(fullState.committedState), - computedStates: fullState.computedStates.map((computedState) => { - return { + ...rest, + historyState: { + ...historyState, + stagedActions: historyState.stagedActions.map(deserializeAction), + committedState: deserializeState(historyState.committedState), + computedStates: historyState.computedStates.map(computedState => ({ ...computedState, - state: stateDeserializer(computedState.state) - }; - }) + state: deserializeState(computedState.state) + })) + } }; } - function deserializeActions(fullState) { - return { - ...fullState, - stagedActions: fullState.stagedActions.map((action) => { - return actionDeserializer(action); - }) - }; - } - - function deserialize(fullState) { - if (!fullState) { - return fullState; - } - let deserializedState = fullState; - if (typeof stateDeserializer === 'function') { - deserializedState = deserializeState(deserializedState); - } - if (typeof actionDeserializer === 'function') { - deserializedState = deserializeActions(deserializedState); - } - return deserializedState; - } - return next => (reducer, initialState) => { const key = `redux-dev-session-${sessionId}`; let finalInitialState; try { - finalInitialState = deserialize(JSON.parse(localStorage.getItem(key))) || initialState; - next(reducer, initialState); + const json = localStorage.getItem(key); + if (json) { + finalInitialState = deserialize(JSON.parse(json)) || initialState; + next(reducer, initialState); + } } catch (e) { console.warn('Could not read debug session from localStorage:', e); try { diff --git a/test/devTools.spec.js b/test/instrument.spec.js similarity index 71% rename from test/devTools.spec.js rename to test/instrument.spec.js index d7540741..542dd1c5 100644 --- a/test/devTools.spec.js +++ b/test/instrument.spec.js @@ -1,6 +1,6 @@ import expect, { spyOn } from 'expect'; import { createStore } from 'redux'; -import devTools, { ActionCreators } from '../src/devTools'; +import instrument, { ActionCreators } from '../src/instrument'; function counter(state = 0, action) { switch (action.type) { @@ -27,13 +27,13 @@ function doubleCounter(state = 0, action) { } } -describe('devTools', () => { +describe('instrument', () => { let store; - let devToolsStore; + let instrumentedStore; beforeEach(() => { - store = devTools()(createStore)(counter); - devToolsStore = store.devToolsStore; + store = instrument()(createStore)(counter); + instrumentedStore = store.instrumentedStore; }); it('should perform actions', () => { @@ -49,20 +49,20 @@ describe('devTools', () => { store.dispatch({ type: 'INCREMENT' }); expect(store.getState()).toBe(2); - devToolsStore.dispatch(ActionCreators.commit()); + instrumentedStore.dispatch(ActionCreators.commit()); expect(store.getState()).toBe(2); store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'INCREMENT' }); expect(store.getState()).toBe(4); - devToolsStore.dispatch(ActionCreators.rollback()); + instrumentedStore.dispatch(ActionCreators.rollback()); expect(store.getState()).toBe(2); store.dispatch({ type: 'DECREMENT' }); expect(store.getState()).toBe(1); - devToolsStore.dispatch(ActionCreators.rollback()); + instrumentedStore.dispatch(ActionCreators.rollback()); expect(store.getState()).toBe(2); }); @@ -70,19 +70,19 @@ describe('devTools', () => { store.dispatch({ type: 'INCREMENT' }); expect(store.getState()).toBe(1); - devToolsStore.dispatch(ActionCreators.commit()); + instrumentedStore.dispatch(ActionCreators.commit()); expect(store.getState()).toBe(1); store.dispatch({ type: 'INCREMENT' }); expect(store.getState()).toBe(2); - devToolsStore.dispatch(ActionCreators.rollback()); + instrumentedStore.dispatch(ActionCreators.rollback()); expect(store.getState()).toBe(1); store.dispatch({ type: 'INCREMENT' }); expect(store.getState()).toBe(2); - devToolsStore.dispatch(ActionCreators.reset()); + instrumentedStore.dispatch(ActionCreators.reset()); expect(store.getState()).toBe(0); }); @@ -93,10 +93,10 @@ describe('devTools', () => { store.dispatch({ type: 'INCREMENT' }); expect(store.getState()).toBe(1); - devToolsStore.dispatch(ActionCreators.toggleAction(2)); + instrumentedStore.dispatch(ActionCreators.toggleAction(2)); expect(store.getState()).toBe(2); - devToolsStore.dispatch(ActionCreators.toggleAction(2)); + instrumentedStore.dispatch(ActionCreators.toggleAction(2)); expect(store.getState()).toBe(1); }); @@ -108,16 +108,16 @@ describe('devTools', () => { store.dispatch({ type: 'INCREMENT' }); expect(store.getState()).toBe(2); - devToolsStore.dispatch(ActionCreators.toggleAction(2)); + instrumentedStore.dispatch(ActionCreators.toggleAction(2)); expect(store.getState()).toBe(3); - devToolsStore.dispatch(ActionCreators.sweep()); + instrumentedStore.dispatch(ActionCreators.sweep()); expect(store.getState()).toBe(3); - devToolsStore.dispatch(ActionCreators.toggleAction(2)); + instrumentedStore.dispatch(ActionCreators.toggleAction(2)); expect(store.getState()).toBe(2); - devToolsStore.dispatch(ActionCreators.sweep()); + instrumentedStore.dispatch(ActionCreators.sweep()); expect(store.getState()).toBe(2); }); @@ -127,19 +127,19 @@ describe('devTools', () => { store.dispatch({ type: 'INCREMENT' }); expect(store.getState()).toBe(1); - devToolsStore.dispatch(ActionCreators.jumpToState(0)); + instrumentedStore.dispatch(ActionCreators.jumpToState(0)); expect(store.getState()).toBe(0); - devToolsStore.dispatch(ActionCreators.jumpToState(1)); + instrumentedStore.dispatch(ActionCreators.jumpToState(1)); expect(store.getState()).toBe(1); - devToolsStore.dispatch(ActionCreators.jumpToState(2)); + instrumentedStore.dispatch(ActionCreators.jumpToState(2)); expect(store.getState()).toBe(0); store.dispatch({ type: 'INCREMENT' }); expect(store.getState()).toBe(0); - devToolsStore.dispatch(ActionCreators.jumpToState(4)); + instrumentedStore.dispatch(ActionCreators.jumpToState(4)); expect(store.getState()).toBe(2); }); @@ -155,17 +155,17 @@ describe('devTools', () => { it('should catch and record errors', () => { let spy = spyOn(console, 'error'); - let storeWithBug = devTools()(createStore)(counterWithBug); + let storeWithBug = instrument()(createStore)(counterWithBug); storeWithBug.dispatch({ type: 'INCREMENT' }); storeWithBug.dispatch({ type: 'DECREMENT' }); storeWithBug.dispatch({ type: 'INCREMENT' }); - let devStoreState = storeWithBug.devToolsStore.getState(); - expect(devStoreState.computedStates[2].error).toMatch( + let historyState = storeWithBug.instrumentedStore.getState().historyState; + expect(historyState.computedStates[2].error).toMatch( /ReferenceError/ ); - expect(devStoreState.computedStates[3].error).toMatch( + expect(historyState.computedStates[3].error).toMatch( /Interrupted by an error up the chain/ ); expect(spy.calls[0].arguments[0]).toMatch( @@ -176,7 +176,7 @@ describe('devTools', () => { }); it('should return the last non-undefined state from getState', () => { - let storeWithBug = devTools()(createStore)(counterWithBug); + let storeWithBug = instrument()(createStore)(counterWithBug); storeWithBug.dispatch({ type: 'INCREMENT' }); storeWithBug.dispatch({ type: 'INCREMENT' }); expect(storeWithBug.getState()).toBe(2); @@ -187,7 +187,7 @@ describe('devTools', () => { it('should not recompute states on every action', () => { let reducerCalls = 0; - let monitoredStore = devTools()(createStore)(() => reducerCalls++); + let monitoredStore = instrument()(createStore)(() => reducerCalls++); expect(reducerCalls).toBe(1); monitoredStore.dispatch({ type: 'INCREMENT' }); monitoredStore.dispatch({ type: 'INCREMENT' }); @@ -197,8 +197,8 @@ describe('devTools', () => { it('should not recompute states when jumping to state', () => { let reducerCalls = 0; - let monitoredStore = devTools()(createStore)(() => reducerCalls++); - let monitoredDevToolsStore = monitoredStore.devToolsStore; + let monitoredStore = instrument()(createStore)(() => reducerCalls++); + let monitoredInstrumentedStore = monitoredStore.instrumentedStore; expect(reducerCalls).toBe(1); monitoredStore.dispatch({ type: 'INCREMENT' }); @@ -206,13 +206,13 @@ describe('devTools', () => { monitoredStore.dispatch({ type: 'INCREMENT' }); expect(reducerCalls).toBe(4); - monitoredDevToolsStore.dispatch(ActionCreators.jumpToState(0)); + monitoredInstrumentedStore.dispatch(ActionCreators.jumpToState(0)); expect(reducerCalls).toBe(4); - monitoredDevToolsStore.dispatch(ActionCreators.jumpToState(1)); + monitoredInstrumentedStore.dispatch(ActionCreators.jumpToState(1)); expect(reducerCalls).toBe(4); - monitoredDevToolsStore.dispatch(ActionCreators.jumpToState(3)); + monitoredInstrumentedStore.dispatch(ActionCreators.jumpToState(3)); expect(reducerCalls).toBe(4); }); }); diff --git a/test/persistState.spec.js b/test/persistState.spec.js index 4cafe1c5..c00c1c19 100644 --- a/test/persistState.spec.js +++ b/test/persistState.spec.js @@ -1,6 +1,5 @@ import expect from 'expect'; -import devTools from '../src/devTools'; -import persistState from '../src/persistState'; +import { instrument, persistState } from '../src'; import { compose, createStore } from 'redux'; describe('persistState', () => { @@ -40,7 +39,7 @@ describe('persistState', () => { }; it('should persist state', () => { - const finalCreateStore = compose(devTools(), persistState('id'))(createStore); + const finalCreateStore = compose(instrument(), persistState('id'))(createStore); const store = finalCreateStore(reducer); expect(store.getState()).toBe(0); @@ -53,7 +52,7 @@ describe('persistState', () => { }); it('should not persist state if no session id', () => { - const finalCreateStore = compose(devTools(), persistState())(createStore); + const finalCreateStore = compose(instrument(), persistState())(createStore); const store = finalCreateStore(reducer); expect(store.getState()).toBe(0); @@ -67,7 +66,7 @@ describe('persistState', () => { it('should run with a custom state deserializer', () => { const oneLess = state => state === undefined ? -1 : state - 1; - const finalCreateStore = compose(devTools(), persistState('id', oneLess))(createStore); + const finalCreateStore = compose(instrument(), persistState('id', oneLess))(createStore); const store = finalCreateStore(reducer); expect(store.getState()).toBe(0); @@ -81,7 +80,7 @@ describe('persistState', () => { it('should run with a custom action deserializer', () => { const incToDec = action => action.type === 'INCREMENT' ? { type: 'DECREMENT' } : action; - const finalCreateStore = compose(devTools(), persistState('id', null, incToDec))(createStore); + const finalCreateStore = compose(instrument(), persistState('id', undefined, incToDec))(createStore); const store = finalCreateStore(reducer); expect(store.getState()).toBe(0); @@ -95,7 +94,7 @@ describe('persistState', () => { it('should warn if read from localStorage fails', () => { const spy = expect.spyOn(console, 'warn'); - const finalCreateStore = compose(devTools(), persistState('id'))(createStore); + const finalCreateStore = compose(instrument(), persistState('id'))(createStore); delete global.localStorage.getItem; finalCreateStore(reducer); @@ -108,7 +107,7 @@ describe('persistState', () => { }); it('should warn if write to localStorage fails', () => { const spy = expect.spyOn(console, 'warn'); - const finalCreateStore = compose(devTools(), persistState('id'))(createStore); + const finalCreateStore = compose(instrument(), persistState('id'))(createStore); delete global.localStorage.setItem; const store = finalCreateStore(reducer);