From 600134b5d393807d81c70dfa78574fb58919cbd7 Mon Sep 17 00:00:00 2001 From: Tomas Weiss Date: Fri, 1 Apr 2016 16:06:39 +0200 Subject: [PATCH 1/5] defining behaviour of replaying flag --- test/instrument.spec.js | 82 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/test/instrument.spec.js b/test/instrument.spec.js index 1c0ac858..65235d00 100644 --- a/test/instrument.spec.js +++ b/test/instrument.spec.js @@ -1,4 +1,4 @@ -import expect, { spyOn } from 'expect'; +import expect, { createSpy, spyOn } from 'expect'; import { createStore, compose } from 'redux'; import instrument, { ActionCreators } from '../src/instrument'; @@ -356,4 +356,84 @@ describe('instrument', () => { 'Check your store configuration.' ); }); + + describe('replaying flag', () => { + const TESTING_ACTION = { type: 'TESTING_ACTION' }; + const INIT_ACTION = { type: '@@INIT' }; + const TESTING_APP_STATE = 42; + + let spiedEmptyReducer; + let replayingStore; + let liftedReplayingStore; + + beforeEach(() => { + spiedEmptyReducer = createSpy(function emptyReducer(appState = TESTING_APP_STATE) { + return appState; + }).andCallThrough(); + replayingStore = createStore(spiedEmptyReducer, instrument()); + liftedReplayingStore = replayingStore.liftedStore; + }); + + it('should provide falsy replaying argument when plain action is dispatched', () => { + replayingStore.dispatch(TESTING_ACTION); + expect(spiedEmptyReducer).toHaveBeenCalled(); + expect(spiedEmptyReducer.calls[1].arguments).toEqual([TESTING_APP_STATE, TESTING_ACTION, false]); + }); + + it('should provide falsy replaying argument when PERFORM_ACTION is dispatched', () => { + replayingStore.dispatch(TESTING_ACTION); + liftedReplayingStore.dispatch(ActionCreators.performAction(TESTING_ACTION)); + expect(spiedEmptyReducer.calls[1].arguments).toEqual([TESTING_APP_STATE, TESTING_ACTION, false]); + }); + + it('should provide truthy replaying argument for init action which follows rollback', () => { + replayingStore.dispatch(TESTING_ACTION); + liftedReplayingStore.dispatch(ActionCreators.rollback()); + expect(spiedEmptyReducer.calls[2].arguments).toEqual([undefined, INIT_ACTION, true]); + }); + + it('should provide truthy replaying argument for init action which follows reset', () => { + replayingStore.dispatch(TESTING_ACTION); + liftedReplayingStore.dispatch(ActionCreators.reset()); + expect(spiedEmptyReducer.calls[2].arguments).toEqual([undefined, INIT_ACTION, true]); + }); + + it('should provide truthy replaying argument for init action which follows commit', () => { + replayingStore.dispatch(TESTING_ACTION); + liftedReplayingStore.dispatch(ActionCreators.commit()); + expect(spiedEmptyReducer.calls[2].arguments).toEqual([42, INIT_ACTION, true]); + }); + + it('should provide truthy replaying argument for all the actions after sweeping', () => { + replayingStore.dispatch(TESTING_ACTION); + liftedReplayingStore.dispatch(ActionCreators.sweep()); + expect(spiedEmptyReducer.calls[2].arguments).toEqual([undefined, INIT_ACTION, true]); + expect(spiedEmptyReducer.calls[3].arguments).toEqual([TESTING_APP_STATE, TESTING_ACTION, true]); + }); + + it('after toggling, should provide truthy replaying argument for action which has not been toggled', () => { + const NEXT_TESTING_ACTION = { type: 'NEXT_TESTING_ACTION' }; + + replayingStore.dispatch(TESTING_ACTION); + replayingStore.dispatch(NEXT_TESTING_ACTION); + liftedReplayingStore.dispatch(ActionCreators.toggleAction(1)); + expect(spiedEmptyReducer.calls[3].arguments).toEqual([TESTING_APP_STATE, NEXT_TESTING_ACTION, true]); + }); + + it('should provide truthy replaying argument for all the actions after importing state', () => { + replayingStore.dispatch(TESTING_ACTION); + const exportedState = liftedReplayingStore.getState(); + + const spiedImportStoreReducer = createSpy(function importReducer(appState = TESTING_APP_STATE) { + return appState; + }).andCallThrough(); + + const importStore = createStore(spiedImportStoreReducer, instrument()); + importStore.liftedStore.dispatch(ActionCreators.importState(exportedState)); + + expect(spiedImportStoreReducer.calls[0].arguments).toEqual([undefined, INIT_ACTION, false]); + expect(spiedImportStoreReducer.calls[1].arguments).toEqual([undefined, INIT_ACTION, true]); + expect(spiedImportStoreReducer.calls[2].arguments).toEqual([TESTING_APP_STATE, TESTING_ACTION, true]); + }); + }); }); From 7cd1deb8a0be05e5aaffa339cc9e43ba82914831 Mon Sep 17 00:00:00 2001 From: Tomas Weiss Date: Fri, 1 Apr 2016 16:10:45 +0200 Subject: [PATCH 2/5] implementing replaying flag to mark actions which are actually replayed --- src/instrument.js | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/instrument.js b/src/instrument.js index 3b6d5678..04a4745a 100644 --- a/src/instrument.js +++ b/src/instrument.js @@ -59,7 +59,7 @@ const INIT_ACTION = { type: '@@INIT' }; /** * Computes the next entry in the log by applying an action. */ -function computeNextEntry(reducer, action, state, error) { +function computeNextEntry(reducer, action, state, error, replaying) { if (error) { return { state, @@ -70,7 +70,7 @@ function computeNextEntry(reducer, action, state, error) { let nextState = state; let nextError; try { - nextState = reducer(state, action); + nextState = reducer(state, action, replaying); } catch (err) { nextError = err.toString(); if (typeof window === 'object' && typeof window.chrome !== 'undefined') { @@ -97,7 +97,8 @@ function recomputeStates( committedState, actionsById, stagedActionIds, - skippedActionIds + skippedActionIds, + replaying ) { // Optimization: exit early and return the same reference // if we know nothing could have changed. @@ -120,7 +121,7 @@ function recomputeStates( const shouldSkip = skippedActionIds.indexOf(actionId) > -1; const entry = shouldSkip ? previousEntry : - computeNextEntry(reducer, action, previousState, previousError); + computeNextEntry(reducer, action, previousState, previousError, replaying); nextComputedStates.push(entry); } @@ -170,6 +171,10 @@ function liftReducerWith(reducer, initialCommittedState, monitorReducer) { // value whenever we feel like we don't have to recompute the states. let minInvalidatedStateIndex = 0; + // For now, potentially any action except PERFORM_ACTION is considered + // as replay + let replaying = true; + switch (liftedAction.type) { case ActionTypes.RESET: { // Get back to the state the store was created with. @@ -245,6 +250,10 @@ function liftReducerWith(reducer, initialCommittedState, monitorReducer) { stagedActionIds = [...stagedActionIds, actionId]; // Optimization: we know that only the new action needs computing. minInvalidatedStateIndex = stagedActionIds.length - 1; + + // This is the first time Action is actually performed, therefore + // we don't consider this replay + replaying = false; break; } case ActionTypes.IMPORT_STATE: { @@ -262,6 +271,8 @@ function liftReducerWith(reducer, initialCommittedState, monitorReducer) { break; } case '@@redux/INIT': { + replaying = false; + // Always recompute states on hot reload and init. minInvalidatedStateIndex = 0; break; @@ -281,7 +292,8 @@ function liftReducerWith(reducer, initialCommittedState, monitorReducer) { committedState, actionsById, stagedActionIds, - skippedActionIds + skippedActionIds, + replaying ); monitorState = monitorReducer(monitorState, liftedAction); return { From 9e5ae8249efd8ec892e9ab6368140a1cd1a8d0a3 Mon Sep 17 00:00:00 2001 From: Tomas Weiss Date: Fri, 1 Apr 2016 16:10:54 +0200 Subject: [PATCH 3/5] Docs reference about replaying flag --- docs/Walkthrough.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/Walkthrough.md b/docs/Walkthrough.md index e474e957..27a202fd 100644 --- a/docs/Walkthrough.md +++ b/docs/Walkthrough.md @@ -368,6 +368,8 @@ Note that there are no useful props you can pass to the `DevTools` component oth * **It is important that `DevTools.instrument()` store enhancer should be added to your middleware stack *after* `applyMiddleware` in the `compose`d functions, as `applyMiddleware` is potentially asynchronous.** Otherwise, DevTools won’t see the raw actions emitted by asynchronous middleware such as [redux-promise](https://github.com/acdlite/redux-promise) or [redux-thunk](https://github.com/gaearon/redux-thunk). +* **Are you a library author**? If you're building a Store Enhancer you might find handy to know which actions have been dispatched regularly and which have been replayed. `redux-devtools` is calling reducer with third argument. The argument is a boolean identifying that Action has been replayed. + ### What Next? Now that you see the DevTools, you might want to learn what those buttons mean and how to use them. This usually depends on the monitor. You can begin by exploring the [LogMonitor](https://github.com/gaearon/redux-devtools-log-monitor) and [DockMonitor](https://github.com/gaearon/redux-devtools-dock-monitor) documentation, as they are the default monitors we suggest to use together. When you’re comfortable using them, you may want to create your own monitor for more exotic purposes, such as a [chart](https://github.com/romseguy/redux-devtools-chart-monitor) or a [diff](https://github.com/whetstone/redux-devtools-diff-monitor) monitor. Don’t forget to send a PR to feature your monitor at the front page! From 0382e6c37e93120e2b32e3271736f596b43c4747 Mon Sep 17 00:00:00 2001 From: Tomas Weiss Date: Sat, 2 Apr 2016 13:48:51 +0200 Subject: [PATCH 4/5] replaying flag should not be third argument of reducer but should be held inside the action - unit tests --- test/instrument.spec.js | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/test/instrument.spec.js b/test/instrument.spec.js index 65235d00..72a6711a 100644 --- a/test/instrument.spec.js +++ b/test/instrument.spec.js @@ -362,6 +362,9 @@ describe('instrument', () => { const INIT_ACTION = { type: '@@INIT' }; const TESTING_APP_STATE = 42; + const buildTestingAction = replaying => ({ ...TESTING_ACTION, replaying }); + const buildInitAction = replaying => ({ ...INIT_ACTION, replaying }); + let spiedEmptyReducer; let replayingStore; let liftedReplayingStore; @@ -374,53 +377,53 @@ describe('instrument', () => { liftedReplayingStore = replayingStore.liftedStore; }); - it('should provide falsy replaying argument when plain action is dispatched', () => { + it('should provide falsy replaying flag when plain action is dispatched', () => { replayingStore.dispatch(TESTING_ACTION); expect(spiedEmptyReducer).toHaveBeenCalled(); - expect(spiedEmptyReducer.calls[1].arguments).toEqual([TESTING_APP_STATE, TESTING_ACTION, false]); + expect(spiedEmptyReducer.calls[1].arguments).toEqual([TESTING_APP_STATE, buildTestingAction(false)]); }); - it('should provide falsy replaying argument when PERFORM_ACTION is dispatched', () => { + it('should provide falsy replaying flag when PERFORM_ACTION is dispatched', () => { replayingStore.dispatch(TESTING_ACTION); liftedReplayingStore.dispatch(ActionCreators.performAction(TESTING_ACTION)); - expect(spiedEmptyReducer.calls[1].arguments).toEqual([TESTING_APP_STATE, TESTING_ACTION, false]); + expect(spiedEmptyReducer.calls[1].arguments).toEqual([TESTING_APP_STATE, buildTestingAction(false)]); }); - it('should provide truthy replaying argument for init action which follows rollback', () => { + it('should provide truthy replaying flag for init action which follows rollback', () => { replayingStore.dispatch(TESTING_ACTION); liftedReplayingStore.dispatch(ActionCreators.rollback()); - expect(spiedEmptyReducer.calls[2].arguments).toEqual([undefined, INIT_ACTION, true]); + expect(spiedEmptyReducer.calls[2].arguments).toEqual([undefined, buildInitAction(true)]); }); - it('should provide truthy replaying argument for init action which follows reset', () => { + it('should provide truthy replaying flag for init action which follows reset', () => { replayingStore.dispatch(TESTING_ACTION); liftedReplayingStore.dispatch(ActionCreators.reset()); - expect(spiedEmptyReducer.calls[2].arguments).toEqual([undefined, INIT_ACTION, true]); + expect(spiedEmptyReducer.calls[2].arguments).toEqual([undefined, buildInitAction(true)]); }); - it('should provide truthy replaying argument for init action which follows commit', () => { + it('should provide truthy replaying flag for init action which follows commit', () => { replayingStore.dispatch(TESTING_ACTION); liftedReplayingStore.dispatch(ActionCreators.commit()); - expect(spiedEmptyReducer.calls[2].arguments).toEqual([42, INIT_ACTION, true]); + expect(spiedEmptyReducer.calls[2].arguments).toEqual([42, buildInitAction(true)]); }); - it('should provide truthy replaying argument for all the actions after sweeping', () => { + it('should provide truthy replaying flag for all the actions after sweeping', () => { replayingStore.dispatch(TESTING_ACTION); liftedReplayingStore.dispatch(ActionCreators.sweep()); - expect(spiedEmptyReducer.calls[2].arguments).toEqual([undefined, INIT_ACTION, true]); - expect(spiedEmptyReducer.calls[3].arguments).toEqual([TESTING_APP_STATE, TESTING_ACTION, true]); + expect(spiedEmptyReducer.calls[2].arguments).toEqual([undefined, buildInitAction(true)]); + expect(spiedEmptyReducer.calls[3].arguments).toEqual([TESTING_APP_STATE, buildTestingAction(true)]); }); - it('after toggling, should provide truthy replaying argument for action which has not been toggled', () => { + it('after toggling, should provide truthy replaying flag for action which has not been toggled', () => { const NEXT_TESTING_ACTION = { type: 'NEXT_TESTING_ACTION' }; replayingStore.dispatch(TESTING_ACTION); replayingStore.dispatch(NEXT_TESTING_ACTION); liftedReplayingStore.dispatch(ActionCreators.toggleAction(1)); - expect(spiedEmptyReducer.calls[3].arguments).toEqual([TESTING_APP_STATE, NEXT_TESTING_ACTION, true]); + expect(spiedEmptyReducer.calls[3].arguments).toEqual([TESTING_APP_STATE, { ...NEXT_TESTING_ACTION, replaying: true }]); }); - it('should provide truthy replaying argument for all the actions after importing state', () => { + it('should provide truthy replaying flag for all the actions after importing state', () => { replayingStore.dispatch(TESTING_ACTION); const exportedState = liftedReplayingStore.getState(); @@ -431,9 +434,9 @@ describe('instrument', () => { const importStore = createStore(spiedImportStoreReducer, instrument()); importStore.liftedStore.dispatch(ActionCreators.importState(exportedState)); - expect(spiedImportStoreReducer.calls[0].arguments).toEqual([undefined, INIT_ACTION, false]); - expect(spiedImportStoreReducer.calls[1].arguments).toEqual([undefined, INIT_ACTION, true]); - expect(spiedImportStoreReducer.calls[2].arguments).toEqual([TESTING_APP_STATE, TESTING_ACTION, true]); + expect(spiedImportStoreReducer.calls[0].arguments).toEqual([undefined, buildInitAction(false)]); + expect(spiedImportStoreReducer.calls[1].arguments).toEqual([undefined, buildInitAction(true)]); + expect(spiedImportStoreReducer.calls[2].arguments).toEqual([TESTING_APP_STATE, buildTestingAction(true)]); }); }); }); From 2e25fe2b02abb299eaa15990de06cf7ae39ad725 Mon Sep 17 00:00:00 2001 From: Tomas Weiss Date: Sat, 2 Apr 2016 13:50:22 +0200 Subject: [PATCH 5/5] replaying flag held inside action --- src/instrument.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/instrument.js b/src/instrument.js index 04a4745a..38cdeb5c 100644 --- a/src/instrument.js +++ b/src/instrument.js @@ -70,7 +70,7 @@ function computeNextEntry(reducer, action, state, error, replaying) { let nextState = state; let nextError; try { - nextState = reducer(state, action, replaying); + nextState = reducer(state, { ...action, replaying }); } catch (err) { nextError = err.toString(); if (typeof window === 'object' && typeof window.chrome !== 'undefined') {