diff --git a/modules/store-devtools/spec/store.spec.ts b/modules/store-devtools/spec/store.spec.ts index cd14192096..f7a5f010b3 100644 --- a/modules/store-devtools/spec/store.spec.ts +++ b/modules/store-devtools/spec/store.spec.ts @@ -259,7 +259,7 @@ describe('Store Devtools', () => { expect(getState()).toBe(2); }); - it('should replace the reducer', () => { + it('should replace the reducer and preserve previous states', () => { store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'DECREMENT' }); store.dispatch({ type: 'INCREMENT' }); @@ -268,7 +268,21 @@ describe('Store Devtools', () => { fixture.replaceReducer(doubleCounter); - expect(getState()).toBe(2); + expect(getState()).toBe(1); + }); + + it('should replace the reducer and compute new state with latest reducer', () => { + store.dispatch({ type: 'INCREMENT' }); + store.dispatch({ type: 'DECREMENT' }); + store.dispatch({ type: 'INCREMENT' }); + + expect(getState()).toBe(1); + + fixture.replaceReducer(doubleCounter); + + store.dispatch({ type: 'INCREMENT' }); + + expect(getState()).toBe(3); }); it('should catch and record errors', () => { @@ -280,8 +294,8 @@ describe('Store Devtools', () => { store.dispatch({ type: 'INCREMENT' }); let { computedStates } = fixture.getLiftedState(); - expect(computedStates[2].error).toMatch(/ReferenceError/); - expect(computedStates[3].error).toMatch( + expect(computedStates[3].error).toMatch(/ReferenceError/); + expect(computedStates[4].error).toMatch( /Interrupted by an error up the chain/ ); diff --git a/modules/store-devtools/src/reducer.ts b/modules/store-devtools/src/reducer.ts index 54cc77b75e..76b899831d 100644 --- a/modules/store-devtools/src/reducer.ts +++ b/modules/store-devtools/src/reducer.ts @@ -10,6 +10,7 @@ import { import { difference, liftAction } from './utils'; import * as Actions from './actions'; import { StoreDevtoolsConfig } from './config'; +import { PerformAction } from './actions'; export type InitAction = { readonly type: typeof INIT; @@ -301,7 +302,6 @@ export function liftReducerWith( } = liftedAction.nextLiftedState); break; } - case UPDATE: case INIT: { // Always recompute states on hot reload and init. minInvalidatedStateIndex = 0; @@ -326,6 +326,66 @@ export function liftReducerWith( break; } + case UPDATE: { + const stateHasErrors = + computedStates.filter(state => state.error).length > 0; + + if (stateHasErrors) { + // Recompute all states + minInvalidatedStateIndex = 0; + + if (options.maxAge && stagedActionIds.length > options.maxAge) { + // States must be recomputed before committing excess. + computedStates = recomputeStates( + computedStates, + minInvalidatedStateIndex, + reducer, + committedState, + actionsById, + stagedActionIds, + skippedActionIds + ); + + commitExcessActions(stagedActionIds.length - options.maxAge); + + // Avoid double computation. + minInvalidatedStateIndex = Infinity; + } + } else { + if (currentStateIndex === stagedActionIds.length - 1) { + currentStateIndex++; + } + + // Add a new action to only recompute state + const actionId = nextActionId++; + actionsById[actionId] = new PerformAction(liftedAction); + stagedActionIds = [...stagedActionIds, actionId]; + + minInvalidatedStateIndex = stagedActionIds.length - 1; + + // States must be recomputed before committing excess. + computedStates = recomputeStates( + computedStates, + minInvalidatedStateIndex, + reducer, + committedState, + actionsById, + stagedActionIds, + skippedActionIds + ); + + currentStateIndex = minInvalidatedStateIndex; + + if (options.maxAge && stagedActionIds.length > options.maxAge) { + commitExcessActions(stagedActionIds.length - options.maxAge); + } + + // Avoid double computation. + minInvalidatedStateIndex = Infinity; + } + + break; + } default: { // If the action is not recognized, it's a monitor action. // Optimization: a monitor action can't change history.