Skip to content

Commit a7de2a6

Browse files
kgkmabrandonroberts
authored andcommitted
fix(StoreDevtools): Add internal support for ActionSanitizer and StateSanitizer (#795)
1 parent 4192645 commit a7de2a6

File tree

7 files changed

+230
-39
lines changed

7 files changed

+230
-39
lines changed

modules/store-devtools/spec/extension.spec.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,7 @@ import { of } from 'rxjs/observable/of';
33

44
import { LiftedState } from '../';
55
import { DevtoolsExtension, ReduxDevtoolsExtension } from '../src/extension';
6-
import {
7-
createConfig,
8-
noActionSanitizer,
9-
noMonitor,
10-
noStateSanitizer,
11-
} from '../src/instrument';
6+
import { createConfig, noMonitor } from '../src/instrument';
127

138
describe('DevtoolsExtension', () => {
149
let reduxDevtoolsExtension: ReduxDevtoolsExtension;
@@ -32,8 +27,8 @@ describe('DevtoolsExtension', () => {
3227
const defaultOptions = {
3328
maxAge: false,
3429
monitor: noMonitor,
35-
actionSanitizer: noActionSanitizer,
36-
stateSanitizer: noStateSanitizer,
30+
actionSanitizer: undefined,
31+
stateSanitizer: undefined,
3732
name: 'NgRx Store DevTools',
3833
serialize: false,
3934
logOnly: false,

modules/store-devtools/spec/store.spec.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,4 +610,136 @@ describe('Store Devtools', () => {
610610
expect(fixture.getLiftedState()).toEqual(exportedState);
611611
});
612612
});
613+
614+
describe('Action and State Sanitizer', () => {
615+
let fixture: Fixture<number>;
616+
617+
const SANITIZED_TOKEN = 'SANITIZED_ACTION';
618+
const SANITIZED_COUNTER = 42;
619+
const testActionSanitizer = (action: Action, id: number) => {
620+
return { type: SANITIZED_TOKEN };
621+
};
622+
const incrementActionSanitizer = (action: Action, id: number) => {
623+
return { type: 'INCREMENT' };
624+
};
625+
const testStateSanitizer = (state: any, index: number) => {
626+
return { state: SANITIZED_COUNTER };
627+
};
628+
629+
afterEach(() => {
630+
fixture.cleanup();
631+
});
632+
633+
it('should function normally with no sanitizers', () => {
634+
fixture = createStore(counter);
635+
636+
fixture.store.dispatch({ type: 'INCREMENT' });
637+
638+
const liftedState = fixture.getLiftedState();
639+
const currentLiftedState =
640+
liftedState.computedStates[liftedState.currentStateIndex];
641+
expect(Object.keys(liftedState.actionsById).length).toBe(
642+
Object.keys(liftedState.sanitizedActionsById).length
643+
);
644+
expect(liftedState.actionsById).toEqual(liftedState.sanitizedActionsById);
645+
expect(currentLiftedState.state).toEqual({ state: 1 });
646+
expect(currentLiftedState.sanitizedState).toBeUndefined();
647+
});
648+
649+
it('should run the action sanitizer on actions', () => {
650+
fixture = createStore(counter, {
651+
actionSanitizer: testActionSanitizer,
652+
});
653+
654+
fixture.store.dispatch({ type: 'INCREMENT' });
655+
fixture.store.dispatch({ type: 'DECREMENT' });
656+
657+
const liftedState = fixture.getLiftedState();
658+
const sanitizedAction =
659+
liftedState.sanitizedActionsById[liftedState.nextActionId - 1];
660+
const sanitizedAction2 =
661+
liftedState.sanitizedActionsById[liftedState.nextActionId - 2];
662+
const action = liftedState.actionsById[liftedState.nextActionId - 1];
663+
const action2 = liftedState.actionsById[liftedState.nextActionId - 2];
664+
665+
expect(liftedState.actionsById).not.toEqual(
666+
liftedState.sanitizedActionsById
667+
);
668+
expect(sanitizedAction.action).toEqual({ type: SANITIZED_TOKEN });
669+
expect(sanitizedAction2.action).toEqual({ type: SANITIZED_TOKEN });
670+
expect(action.action).toEqual({ type: 'DECREMENT' });
671+
expect(action2.action).toEqual({ type: 'INCREMENT' });
672+
});
673+
674+
it('should run the state sanitizer on store state', () => {
675+
fixture = createStore(counter, {
676+
stateSanitizer: testStateSanitizer,
677+
});
678+
679+
let liftedState = fixture.getLiftedState();
680+
let currentLiftedState =
681+
liftedState.computedStates[liftedState.currentStateIndex];
682+
expect(fixture.getState()).toBe(0);
683+
expect(currentLiftedState.state).toEqual({ state: 0 });
684+
expect(currentLiftedState.sanitizedState).toBeDefined();
685+
expect(currentLiftedState.sanitizedState).toEqual({
686+
state: SANITIZED_COUNTER,
687+
});
688+
689+
fixture.store.dispatch({ type: 'INCREMENT' });
690+
691+
liftedState = fixture.getLiftedState();
692+
currentLiftedState =
693+
liftedState.computedStates[liftedState.currentStateIndex];
694+
expect(fixture.getState()).toBe(1);
695+
expect(currentLiftedState.state).toEqual({ state: 1 });
696+
expect(currentLiftedState.sanitizedState).toEqual({
697+
state: SANITIZED_COUNTER,
698+
});
699+
});
700+
701+
it('should run transparently to produce a new lifted store state', () => {
702+
const devtoolsOptions: Partial<StoreDevtoolsConfig> = {
703+
actionSanitizer: testActionSanitizer,
704+
stateSanitizer: testStateSanitizer,
705+
};
706+
fixture = createStore(counter, devtoolsOptions);
707+
708+
fixture.store.dispatch({ type: 'INCREMENT' });
709+
710+
const liftedState = fixture.getLiftedState();
711+
const sanitizedLiftedState = fixture.devtools.getSanitizedState(
712+
liftedState,
713+
devtoolsOptions.stateSanitizer
714+
);
715+
const originalAction =
716+
liftedState.actionsById[liftedState.nextActionId - 1];
717+
const originalState =
718+
liftedState.computedStates[liftedState.currentStateIndex];
719+
const sanitizedAction =
720+
sanitizedLiftedState.actionsById[liftedState.nextActionId - 1];
721+
const sanitizedState =
722+
sanitizedLiftedState.computedStates[liftedState.currentStateIndex];
723+
724+
expect(originalAction.action).toEqual({ type: 'INCREMENT' });
725+
expect(originalState.state).toEqual({ state: 1 });
726+
expect(sanitizedAction.action).toEqual({ type: SANITIZED_TOKEN });
727+
expect(sanitizedState.state).toEqual({ state: SANITIZED_COUNTER });
728+
});
729+
730+
it('sanitized actions should not affect the store state', () => {
731+
fixture = createStore(counter, {
732+
actionSanitizer: incrementActionSanitizer,
733+
});
734+
735+
fixture.store.dispatch({ type: 'DECREMENT' });
736+
fixture.store.dispatch({ type: 'DECREMENT' });
737+
738+
const liftedState = fixture.getLiftedState();
739+
expect(fixture.getState()).toBe(-2);
740+
expect(
741+
liftedState.computedStates[liftedState.currentStateIndex].state
742+
).toEqual({ state: -2 });
743+
});
744+
});
613745
});

modules/store-devtools/src/config.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { ActionReducer, Action } from '@ngrx/store';
22
import { InjectionToken, Type } from '@angular/core';
33

4+
export type ActionSanitizer = (action: Action, id: number) => Action;
5+
export type StateSanitizer = (state: any, index: number) => any;
6+
47
export class StoreDevtoolsConfig {
58
maxAge: number | false;
69
monitor: ActionReducer<any, any>;
7-
actionSanitizer?: <A extends Action>(action: A, id: number) => A;
8-
stateSanitizer?: <S>(state: S, index: number) => S;
10+
actionSanitizer?: ActionSanitizer;
11+
stateSanitizer?: StateSanitizer;
912
name?: string;
1013
serialize?: boolean;
1114
logOnly?: boolean;

modules/store-devtools/src/devtools.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,18 @@ import { queue } from 'rxjs/scheduler/queue';
2121

2222
import { DevtoolsExtension } from './extension';
2323
import { liftAction, unliftAction, unliftState, applyOperators } from './utils';
24-
import { liftReducerWith, liftInitialState, LiftedState } from './reducer';
24+
import {
25+
liftReducerWith,
26+
liftInitialState,
27+
LiftedState,
28+
ComputedState,
29+
} from './reducer';
2530
import * as Actions from './actions';
26-
import { StoreDevtoolsConfig, STORE_DEVTOOLS_CONFIG } from './config';
31+
import {
32+
StoreDevtoolsConfig,
33+
STORE_DEVTOOLS_CONFIG,
34+
StateSanitizer,
35+
} from './config';
2736

2837
@Injectable()
2938
export class DevtoolsDispatcher extends ActionsSubject {}
@@ -68,11 +77,15 @@ export class StoreDevtools implements Observer<any> {
6877
[
6978
scan,
7079
({ state: liftedState }: any, [action, reducer]: any) => {
71-
const state = reducer(liftedState, action);
80+
const reducedLiftedState = reducer(liftedState, action);
7281

73-
extension.notify(action, state);
82+
// Extension should be sent the sanitized lifted state
83+
extension.notify(
84+
action,
85+
this.getSanitizedState(reducedLiftedState, config.stateSanitizer)
86+
);
7487

75-
return { state, action };
88+
return { state: reducedLiftedState, action };
7689
},
7790
{ state: liftedInitialState, action: null },
7891
],
@@ -97,6 +110,26 @@ export class StoreDevtools implements Observer<any> {
97110
this.state = state$;
98111
}
99112

113+
/**
114+
* Restructures the lifted state passed in to prepare for sending to the
115+
* Redux Devtools Extension
116+
*/
117+
getSanitizedState(state: LiftedState, stateSanitizer?: StateSanitizer) {
118+
const sanitizedComputedStates = stateSanitizer
119+
? state.computedStates.map((entry: ComputedState) => ({
120+
state: entry.sanitizedState,
121+
error: entry.error,
122+
}))
123+
: state.computedStates;
124+
125+
// Replace action and state logs with their sanitized versions
126+
return {
127+
...state,
128+
actionsById: state.sanitizedActionsById,
129+
computedStates: sanitizedComputedStates,
130+
};
131+
}
132+
100133
dispatch(action: Action) {
101134
this.dispatcher.next(action);
102135
}

modules/store-devtools/src/extension.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { takeUntil } from 'rxjs/operator/takeUntil';
1111
import { STORE_DEVTOOLS_CONFIG, StoreDevtoolsConfig } from './config';
1212
import { LiftedState } from './reducer';
1313
import { PerformAction } from './actions';
14-
import { applyOperators } from './utils';
14+
import { applyOperators, unliftState } from './utils';
1515

1616
export const ExtensionActionTypes = {
1717
START: 'START',
@@ -36,6 +36,7 @@ export interface ReduxDevtoolsExtensionConfig {
3636
name: string | undefined;
3737
instanceId: string;
3838
maxAge?: number;
39+
actionSanitizer?: (action: Action, id: number) => Action;
3940
}
4041

4142
export interface ReduxDevtoolsExtension {
@@ -86,7 +87,7 @@ export class DevtoolsExtension {
8687
// d) any action that is not a PerformAction to err on the side of
8788
// caution.
8889
if (action instanceof PerformAction) {
89-
const currentState = state.computedStates[state.currentStateIndex].state;
90+
const currentState = unliftState(state);
9091
this.extensionConnection.send(action.action, currentState);
9192
} else {
9293
// Requires full state update;
@@ -104,6 +105,7 @@ export class DevtoolsExtension {
104105
instanceId: this.instanceId,
105106
name: this.config.name,
106107
features: this.config.features,
108+
actionSanitizer: this.config.actionSanitizer,
107109
};
108110
if (this.config.maxAge !== false /* support === 0 */) {
109111
extensionOptions.maxAge = this.config.maxAge;

modules/store-devtools/src/instrument.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,6 @@ export function noMonitor(): null {
6464
return null;
6565
}
6666

67-
export function noActionSanitizer(): null {
68-
return null;
69-
}
70-
71-
export function noStateSanitizer(): null {
72-
return null;
73-
}
74-
7567
export const DEFAULT_NAME = 'NgRx Store DevTools';
7668

7769
export function createConfig(
@@ -80,8 +72,8 @@ export function createConfig(
8072
const DEFAULT_OPTIONS: StoreDevtoolsConfig = {
8173
maxAge: false,
8274
monitor: noMonitor,
83-
actionSanitizer: noActionSanitizer,
84-
stateSanitizer: noStateSanitizer,
75+
actionSanitizer: undefined,
76+
stateSanitizer: undefined,
8577
name: DEFAULT_NAME,
8678
serialize: false,
8779
logOnly: false,

0 commit comments

Comments
 (0)