Skip to content

Commit b7f0b26

Browse files
committed
Merge pull request #241 from echenley/master
add maxAge option
2 parents 0fd4f24 + 3706c3b commit b7f0b26

File tree

3 files changed

+225
-7
lines changed

3 files changed

+225
-7
lines changed

src/createDevTools.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@ export default function createDevTools(children) {
77
const monitorProps = monitorElement.props;
88
const Monitor = monitorElement.type;
99
const ConnectedMonitor = connect(state => state)(Monitor);
10-
const enhancer = instrument((state, action) =>
11-
Monitor.update(monitorProps, state, action)
12-
);
1310

1411
return class DevTools extends Component {
1512
static contextTypes = {
@@ -20,7 +17,10 @@ export default function createDevTools(children) {
2017
store: PropTypes.object
2118
};
2219

23-
static instrument = () => enhancer;
20+
static instrument = (options) => instrument(
21+
(state, action) => Monitor.update(monitorProps, state, action),
22+
options
23+
);
2424

2525
constructor(props, context) {
2626
super(props, context);

src/instrument.js

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ function liftAction(action) {
138138
/**
139139
* Creates a history state reducer from an app's reducer.
140140
*/
141-
function liftReducerWith(reducer, initialCommittedState, monitorReducer) {
141+
function liftReducerWith(reducer, initialCommittedState, monitorReducer, options) {
142142
const initialLiftedState = {
143143
monitorState: monitorReducer(undefined, {}),
144144
nextActionId: 1,
@@ -165,6 +165,31 @@ function liftReducerWith(reducer, initialCommittedState, monitorReducer) {
165165
computedStates
166166
} = liftedState;
167167

168+
function commitExcessActions(n) {
169+
// Auto-commits n-number of excess actions.
170+
let excess = n;
171+
let idsToDelete = stagedActionIds.slice(1, excess + 1);
172+
173+
for (let i = 0; i < idsToDelete.length; i++) {
174+
if (computedStates[i + 1].error) {
175+
// Stop if error is found. Commit actions up to error.
176+
excess = i;
177+
idsToDelete = stagedActionIds.slice(1, excess + 1);
178+
break;
179+
} else {
180+
delete actionsById[idsToDelete[i]];
181+
}
182+
}
183+
184+
skippedActionIds = skippedActionIds.filter(id => idsToDelete.indexOf(id) === -1);
185+
stagedActionIds = [0, ...stagedActionIds.slice(excess + 1)];
186+
committedState = computedStates[excess].state;
187+
computedStates = computedStates.slice(excess);
188+
currentStateIndex = currentStateIndex > excess
189+
? currentStateIndex - excess
190+
: 0;
191+
}
192+
168193
// By default, agressively recompute every state whatever happens.
169194
// This has O(n) performance, so we'll override this to a sensible
170195
// value whenever we feel like we don't have to recompute the states.
@@ -235,6 +260,11 @@ function liftReducerWith(reducer, initialCommittedState, monitorReducer) {
235260
break;
236261
}
237262
case ActionTypes.PERFORM_ACTION: {
263+
// Auto-commit as new actions come in.
264+
if (options.maxAge && stagedActionIds.length === options.maxAge) {
265+
commitExcessActions(1);
266+
}
267+
238268
if (currentStateIndex === stagedActionIds.length - 1) {
239269
currentStateIndex++;
240270
}
@@ -264,6 +294,25 @@ function liftReducerWith(reducer, initialCommittedState, monitorReducer) {
264294
case '@@redux/INIT': {
265295
// Always recompute states on hot reload and init.
266296
minInvalidatedStateIndex = 0;
297+
298+
if (options.maxAge && stagedActionIds.length > options.maxAge) {
299+
// States must be recomputed before committing excess.
300+
computedStates = recomputeStates(
301+
computedStates,
302+
minInvalidatedStateIndex,
303+
reducer,
304+
committedState,
305+
actionsById,
306+
stagedActionIds,
307+
skippedActionIds
308+
);
309+
310+
commitExcessActions(stagedActionIds.length - options.maxAge);
311+
312+
// Avoid double computation.
313+
minInvalidatedStateIndex = Infinity;
314+
}
315+
267316
break;
268317
}
269318
default: {
@@ -339,7 +388,7 @@ function unliftStore(liftedStore, liftReducer) {
339388
/**
340389
* Redux instrumentation store enhancer.
341390
*/
342-
export default function instrument(monitorReducer = () => null) {
391+
export default function instrument(monitorReducer = () => null, options = {}) {
343392
return createStore => (reducer, initialState, enhancer) => {
344393

345394
function liftReducer(r) {
@@ -354,7 +403,7 @@ export default function instrument(monitorReducer = () => null) {
354403
}
355404
throw new Error('Expected the reducer to be a function.');
356405
}
357-
return liftReducerWith(r, initialState, monitorReducer);
406+
return liftReducerWith(r, initialState, monitorReducer, options);
358407
}
359408

360409
const liftedStore = createStore(liftReducer(reducer), enhancer);

test/instrument.spec.js

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ function counterWithBug(state = 0, action) {
1919
}
2020
}
2121

22+
function counterWithAnotherBug(state = 0, action) {
23+
switch (action.type) {
24+
case 'INCREMENT': return mistake + 1; // eslint-disable-line no-undef
25+
case 'DECREMENT': return state - 1;
26+
case 'SET_UNDEFINED': return undefined;
27+
default: return state;
28+
}
29+
}
30+
2231
function doubleCounter(state = 0, action) {
2332
switch (action.type) {
2433
case 'INCREMENT': return state + 2;
@@ -295,6 +304,166 @@ describe('instrument', () => {
295304
expect(monitoredLiftedStore.getState().computedStates).toBe(savedComputedStates);
296305
});
297306

307+
describe('maxAge option', () => {
308+
let configuredStore;
309+
let configuredLiftedStore;
310+
311+
beforeEach(() => {
312+
configuredStore = createStore(counter, instrument(undefined, { maxAge: 3 }));
313+
configuredLiftedStore = configuredStore.liftedStore;
314+
});
315+
316+
it('should auto-commit earliest non-@@INIT action when maxAge is reached', () => {
317+
configuredStore.dispatch({ type: 'INCREMENT' });
318+
configuredStore.dispatch({ type: 'INCREMENT' });
319+
let liftedStoreState = configuredLiftedStore.getState();
320+
321+
expect(configuredStore.getState()).toBe(2);
322+
expect(Object.keys(liftedStoreState.actionsById).length).toBe(3);
323+
expect(liftedStoreState.committedState).toBe(undefined);
324+
expect(liftedStoreState.stagedActionIds).toInclude(1);
325+
326+
// Trigger auto-commit.
327+
configuredStore.dispatch({ type: 'INCREMENT' });
328+
liftedStoreState = configuredLiftedStore.getState();
329+
330+
expect(configuredStore.getState()).toBe(3);
331+
expect(Object.keys(liftedStoreState.actionsById).length).toBe(3);
332+
expect(liftedStoreState.stagedActionIds).toExclude(1);
333+
expect(liftedStoreState.computedStates[0].state).toBe(1);
334+
expect(liftedStoreState.committedState).toBe(1);
335+
expect(liftedStoreState.currentStateIndex).toBe(2);
336+
});
337+
338+
it('should remove skipped actions once committed', () => {
339+
configuredStore.dispatch({ type: 'INCREMENT' });
340+
configuredLiftedStore.dispatch(ActionCreators.toggleAction(1));
341+
configuredStore.dispatch({ type: 'INCREMENT' });
342+
expect(configuredLiftedStore.getState().skippedActionIds).toInclude(1);
343+
configuredStore.dispatch({ type: 'INCREMENT' });
344+
expect(configuredLiftedStore.getState().skippedActionIds).toExclude(1);
345+
});
346+
347+
it('should not auto-commit errors', () => {
348+
let spy = spyOn(console, 'error');
349+
350+
let storeWithBug = createStore(counterWithBug, instrument(undefined, { maxAge: 3 }));
351+
let liftedStoreWithBug = storeWithBug.liftedStore;
352+
storeWithBug.dispatch({ type: 'DECREMENT' });
353+
storeWithBug.dispatch({ type: 'INCREMENT' });
354+
expect(liftedStoreWithBug.getState().stagedActionIds.length).toBe(3);
355+
356+
storeWithBug.dispatch({ type: 'INCREMENT' });
357+
expect(liftedStoreWithBug.getState().stagedActionIds.length).toBe(4);
358+
359+
spy.restore();
360+
});
361+
362+
it('should auto-commit actions after hot reload fixes error', () => {
363+
let spy = spyOn(console, 'error');
364+
365+
let storeWithBug = createStore(counterWithBug, instrument(undefined, { maxAge: 3 }));
366+
let liftedStoreWithBug = storeWithBug.liftedStore;
367+
storeWithBug.dispatch({ type: 'DECREMENT' });
368+
storeWithBug.dispatch({ type: 'DECREMENT' });
369+
storeWithBug.dispatch({ type: 'INCREMENT' });
370+
storeWithBug.dispatch({ type: 'DECREMENT' });
371+
storeWithBug.dispatch({ type: 'DECREMENT' });
372+
storeWithBug.dispatch({ type: 'DECREMENT' });
373+
expect(liftedStoreWithBug.getState().stagedActionIds.length).toBe(7);
374+
375+
// Auto-commit 2 actions by "fixing" reducer bug, but introducing another.
376+
storeWithBug.replaceReducer(counterWithAnotherBug);
377+
expect(liftedStoreWithBug.getState().stagedActionIds.length).toBe(5);
378+
379+
// Auto-commit 2 more actions by "fixing" other reducer bug.
380+
storeWithBug.replaceReducer(counter);
381+
expect(liftedStoreWithBug.getState().stagedActionIds.length).toBe(3);
382+
383+
spy.restore();
384+
});
385+
386+
it('should update currentStateIndex when auto-committing', () => {
387+
let liftedStoreState;
388+
let currentComputedState;
389+
390+
configuredStore.dispatch({ type: 'INCREMENT' });
391+
configuredStore.dispatch({ type: 'INCREMENT' });
392+
liftedStoreState = configuredLiftedStore.getState();
393+
expect(liftedStoreState.currentStateIndex).toBe(2);
394+
395+
// currentStateIndex should stay at 2 as actions are committed.
396+
configuredStore.dispatch({ type: 'INCREMENT' });
397+
liftedStoreState = configuredLiftedStore.getState();
398+
currentComputedState = liftedStoreState.computedStates[liftedStoreState.currentStateIndex];
399+
expect(liftedStoreState.currentStateIndex).toBe(2);
400+
expect(currentComputedState.state).toBe(3);
401+
});
402+
403+
it('should continue to increment currentStateIndex while error blocks commit', () => {
404+
let spy = spyOn(console, 'error');
405+
406+
let storeWithBug = createStore(counterWithBug, instrument(undefined, { maxAge: 3 }));
407+
let liftedStoreWithBug = storeWithBug.liftedStore;
408+
409+
storeWithBug.dispatch({ type: 'DECREMENT' });
410+
storeWithBug.dispatch({ type: 'DECREMENT' });
411+
storeWithBug.dispatch({ type: 'DECREMENT' });
412+
storeWithBug.dispatch({ type: 'DECREMENT' });
413+
414+
let liftedStoreState = liftedStoreWithBug.getState();
415+
let currentComputedState = liftedStoreState.computedStates[liftedStoreState.currentStateIndex];
416+
expect(liftedStoreState.currentStateIndex).toBe(4);
417+
expect(currentComputedState.state).toBe(0);
418+
expect(currentComputedState.error).toExist();
419+
420+
spy.restore();
421+
});
422+
423+
it('should adjust currentStateIndex correctly when multiple actions are committed', () => {
424+
let spy = spyOn(console, 'error');
425+
426+
let storeWithBug = createStore(counterWithBug, instrument(undefined, { maxAge: 3 }));
427+
let liftedStoreWithBug = storeWithBug.liftedStore;
428+
429+
storeWithBug.dispatch({ type: 'DECREMENT' });
430+
storeWithBug.dispatch({ type: 'DECREMENT' });
431+
storeWithBug.dispatch({ type: 'DECREMENT' });
432+
storeWithBug.dispatch({ type: 'DECREMENT' });
433+
434+
// Auto-commit 2 actions by "fixing" reducer bug.
435+
storeWithBug.replaceReducer(counter);
436+
let liftedStoreState = liftedStoreWithBug.getState();
437+
let currentComputedState = liftedStoreState.computedStates[liftedStoreState.currentStateIndex];
438+
expect(liftedStoreState.currentStateIndex).toBe(2);
439+
expect(currentComputedState.state).toBe(-4);
440+
441+
spy.restore();
442+
});
443+
444+
it('should not allow currentStateIndex to drop below 0', () => {
445+
let spy = spyOn(console, 'error');
446+
447+
let storeWithBug = createStore(counterWithBug, instrument(undefined, { maxAge: 3 }));
448+
let liftedStoreWithBug = storeWithBug.liftedStore;
449+
450+
storeWithBug.dispatch({ type: 'DECREMENT' });
451+
storeWithBug.dispatch({ type: 'DECREMENT' });
452+
storeWithBug.dispatch({ type: 'DECREMENT' });
453+
storeWithBug.dispatch({ type: 'DECREMENT' });
454+
liftedStoreWithBug.dispatch(ActionCreators.jumpToState(1));
455+
456+
// Auto-commit 2 actions by "fixing" reducer bug.
457+
storeWithBug.replaceReducer(counter);
458+
let liftedStoreState = liftedStoreWithBug.getState();
459+
let currentComputedState = liftedStoreState.computedStates[liftedStoreState.currentStateIndex];
460+
expect(liftedStoreState.currentStateIndex).toBe(0);
461+
expect(currentComputedState.state).toBe(-2);
462+
463+
spy.restore();
464+
});
465+
});
466+
298467
describe('Import State', () => {
299468
let monitoredStore;
300469
let monitoredLiftedStore;

0 commit comments

Comments
 (0)