Skip to content

Commit

Permalink
fix(StoreDevtools): Add internal support for ActionSanitizer and Stat…
Browse files Browse the repository at this point in the history
…eSanitizer (#795)
  • Loading branch information
kgkma authored and brandonroberts committed Feb 12, 2018
1 parent 4192645 commit a7de2a6
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 39 deletions.
11 changes: 3 additions & 8 deletions modules/store-devtools/spec/extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,7 @@ import { of } from 'rxjs/observable/of';

import { LiftedState } from '../';
import { DevtoolsExtension, ReduxDevtoolsExtension } from '../src/extension';
import {
createConfig,
noActionSanitizer,
noMonitor,
noStateSanitizer,
} from '../src/instrument';
import { createConfig, noMonitor } from '../src/instrument';

describe('DevtoolsExtension', () => {
let reduxDevtoolsExtension: ReduxDevtoolsExtension;
Expand All @@ -32,8 +27,8 @@ describe('DevtoolsExtension', () => {
const defaultOptions = {
maxAge: false,
monitor: noMonitor,
actionSanitizer: noActionSanitizer,
stateSanitizer: noStateSanitizer,
actionSanitizer: undefined,
stateSanitizer: undefined,
name: 'NgRx Store DevTools',
serialize: false,
logOnly: false,
Expand Down
132 changes: 132 additions & 0 deletions modules/store-devtools/spec/store.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,4 +610,136 @@ describe('Store Devtools', () => {
expect(fixture.getLiftedState()).toEqual(exportedState);
});
});

describe('Action and State Sanitizer', () => {
let fixture: Fixture<number>;

const SANITIZED_TOKEN = 'SANITIZED_ACTION';
const SANITIZED_COUNTER = 42;
const testActionSanitizer = (action: Action, id: number) => {
return { type: SANITIZED_TOKEN };
};
const incrementActionSanitizer = (action: Action, id: number) => {
return { type: 'INCREMENT' };
};
const testStateSanitizer = (state: any, index: number) => {
return { state: SANITIZED_COUNTER };
};

afterEach(() => {
fixture.cleanup();
});

it('should function normally with no sanitizers', () => {
fixture = createStore(counter);

fixture.store.dispatch({ type: 'INCREMENT' });

const liftedState = fixture.getLiftedState();
const currentLiftedState =
liftedState.computedStates[liftedState.currentStateIndex];
expect(Object.keys(liftedState.actionsById).length).toBe(
Object.keys(liftedState.sanitizedActionsById).length
);
expect(liftedState.actionsById).toEqual(liftedState.sanitizedActionsById);
expect(currentLiftedState.state).toEqual({ state: 1 });
expect(currentLiftedState.sanitizedState).toBeUndefined();
});

it('should run the action sanitizer on actions', () => {
fixture = createStore(counter, {
actionSanitizer: testActionSanitizer,
});

fixture.store.dispatch({ type: 'INCREMENT' });
fixture.store.dispatch({ type: 'DECREMENT' });

const liftedState = fixture.getLiftedState();
const sanitizedAction =
liftedState.sanitizedActionsById[liftedState.nextActionId - 1];
const sanitizedAction2 =
liftedState.sanitizedActionsById[liftedState.nextActionId - 2];
const action = liftedState.actionsById[liftedState.nextActionId - 1];
const action2 = liftedState.actionsById[liftedState.nextActionId - 2];

expect(liftedState.actionsById).not.toEqual(
liftedState.sanitizedActionsById
);
expect(sanitizedAction.action).toEqual({ type: SANITIZED_TOKEN });
expect(sanitizedAction2.action).toEqual({ type: SANITIZED_TOKEN });
expect(action.action).toEqual({ type: 'DECREMENT' });
expect(action2.action).toEqual({ type: 'INCREMENT' });
});

it('should run the state sanitizer on store state', () => {
fixture = createStore(counter, {
stateSanitizer: testStateSanitizer,
});

let liftedState = fixture.getLiftedState();
let currentLiftedState =
liftedState.computedStates[liftedState.currentStateIndex];
expect(fixture.getState()).toBe(0);
expect(currentLiftedState.state).toEqual({ state: 0 });
expect(currentLiftedState.sanitizedState).toBeDefined();
expect(currentLiftedState.sanitizedState).toEqual({
state: SANITIZED_COUNTER,
});

fixture.store.dispatch({ type: 'INCREMENT' });

liftedState = fixture.getLiftedState();
currentLiftedState =
liftedState.computedStates[liftedState.currentStateIndex];
expect(fixture.getState()).toBe(1);
expect(currentLiftedState.state).toEqual({ state: 1 });
expect(currentLiftedState.sanitizedState).toEqual({
state: SANITIZED_COUNTER,
});
});

it('should run transparently to produce a new lifted store state', () => {
const devtoolsOptions: Partial<StoreDevtoolsConfig> = {
actionSanitizer: testActionSanitizer,
stateSanitizer: testStateSanitizer,
};
fixture = createStore(counter, devtoolsOptions);

fixture.store.dispatch({ type: 'INCREMENT' });

const liftedState = fixture.getLiftedState();
const sanitizedLiftedState = fixture.devtools.getSanitizedState(
liftedState,
devtoolsOptions.stateSanitizer
);
const originalAction =
liftedState.actionsById[liftedState.nextActionId - 1];
const originalState =
liftedState.computedStates[liftedState.currentStateIndex];
const sanitizedAction =
sanitizedLiftedState.actionsById[liftedState.nextActionId - 1];
const sanitizedState =
sanitizedLiftedState.computedStates[liftedState.currentStateIndex];

expect(originalAction.action).toEqual({ type: 'INCREMENT' });
expect(originalState.state).toEqual({ state: 1 });
expect(sanitizedAction.action).toEqual({ type: SANITIZED_TOKEN });
expect(sanitizedState.state).toEqual({ state: SANITIZED_COUNTER });
});

it('sanitized actions should not affect the store state', () => {
fixture = createStore(counter, {
actionSanitizer: incrementActionSanitizer,
});

fixture.store.dispatch({ type: 'DECREMENT' });
fixture.store.dispatch({ type: 'DECREMENT' });

const liftedState = fixture.getLiftedState();
expect(fixture.getState()).toBe(-2);
expect(
liftedState.computedStates[liftedState.currentStateIndex].state
).toEqual({ state: -2 });
});
});
});
7 changes: 5 additions & 2 deletions modules/store-devtools/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { ActionReducer, Action } from '@ngrx/store';
import { InjectionToken, Type } from '@angular/core';

export type ActionSanitizer = (action: Action, id: number) => Action;
export type StateSanitizer = (state: any, index: number) => any;

export class StoreDevtoolsConfig {
maxAge: number | false;
monitor: ActionReducer<any, any>;
actionSanitizer?: <A extends Action>(action: A, id: number) => A;
stateSanitizer?: <S>(state: S, index: number) => S;
actionSanitizer?: ActionSanitizer;
stateSanitizer?: StateSanitizer;
name?: string;
serialize?: boolean;
logOnly?: boolean;
Expand Down
43 changes: 38 additions & 5 deletions modules/store-devtools/src/devtools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,18 @@ import { queue } from 'rxjs/scheduler/queue';

import { DevtoolsExtension } from './extension';
import { liftAction, unliftAction, unliftState, applyOperators } from './utils';
import { liftReducerWith, liftInitialState, LiftedState } from './reducer';
import {
liftReducerWith,
liftInitialState,
LiftedState,
ComputedState,
} from './reducer';
import * as Actions from './actions';
import { StoreDevtoolsConfig, STORE_DEVTOOLS_CONFIG } from './config';
import {
StoreDevtoolsConfig,
STORE_DEVTOOLS_CONFIG,
StateSanitizer,
} from './config';

@Injectable()
export class DevtoolsDispatcher extends ActionsSubject {}
Expand Down Expand Up @@ -68,11 +77,15 @@ export class StoreDevtools implements Observer<any> {
[
scan,
({ state: liftedState }: any, [action, reducer]: any) => {
const state = reducer(liftedState, action);
const reducedLiftedState = reducer(liftedState, action);

extension.notify(action, state);
// Extension should be sent the sanitized lifted state
extension.notify(
action,
this.getSanitizedState(reducedLiftedState, config.stateSanitizer)
);

return { state, action };
return { state: reducedLiftedState, action };
},
{ state: liftedInitialState, action: null },
],
Expand All @@ -97,6 +110,26 @@ export class StoreDevtools implements Observer<any> {
this.state = state$;
}

/**
* Restructures the lifted state passed in to prepare for sending to the
* Redux Devtools Extension
*/
getSanitizedState(state: LiftedState, stateSanitizer?: StateSanitizer) {
const sanitizedComputedStates = stateSanitizer
? state.computedStates.map((entry: ComputedState) => ({
state: entry.sanitizedState,
error: entry.error,
}))
: state.computedStates;

// Replace action and state logs with their sanitized versions
return {
...state,
actionsById: state.sanitizedActionsById,
computedStates: sanitizedComputedStates,
};
}

dispatch(action: Action) {
this.dispatcher.next(action);
}
Expand Down
6 changes: 4 additions & 2 deletions modules/store-devtools/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { takeUntil } from 'rxjs/operator/takeUntil';
import { STORE_DEVTOOLS_CONFIG, StoreDevtoolsConfig } from './config';
import { LiftedState } from './reducer';
import { PerformAction } from './actions';
import { applyOperators } from './utils';
import { applyOperators, unliftState } from './utils';

export const ExtensionActionTypes = {
START: 'START',
Expand All @@ -36,6 +36,7 @@ export interface ReduxDevtoolsExtensionConfig {
name: string | undefined;
instanceId: string;
maxAge?: number;
actionSanitizer?: (action: Action, id: number) => Action;
}

export interface ReduxDevtoolsExtension {
Expand Down Expand Up @@ -86,7 +87,7 @@ export class DevtoolsExtension {
// d) any action that is not a PerformAction to err on the side of
// caution.
if (action instanceof PerformAction) {
const currentState = state.computedStates[state.currentStateIndex].state;
const currentState = unliftState(state);
this.extensionConnection.send(action.action, currentState);
} else {
// Requires full state update;
Expand All @@ -104,6 +105,7 @@ export class DevtoolsExtension {
instanceId: this.instanceId,
name: this.config.name,
features: this.config.features,
actionSanitizer: this.config.actionSanitizer,
};
if (this.config.maxAge !== false /* support === 0 */) {
extensionOptions.maxAge = this.config.maxAge;
Expand Down
12 changes: 2 additions & 10 deletions modules/store-devtools/src/instrument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,6 @@ export function noMonitor(): null {
return null;
}

export function noActionSanitizer(): null {
return null;
}

export function noStateSanitizer(): null {
return null;
}

export const DEFAULT_NAME = 'NgRx Store DevTools';

export function createConfig(
Expand All @@ -80,8 +72,8 @@ export function createConfig(
const DEFAULT_OPTIONS: StoreDevtoolsConfig = {
maxAge: false,
monitor: noMonitor,
actionSanitizer: noActionSanitizer,
stateSanitizer: noStateSanitizer,
actionSanitizer: undefined,
stateSanitizer: undefined,
name: DEFAULT_NAME,
serialize: false,
logOnly: false,
Expand Down
Loading

0 comments on commit a7de2a6

Please sign in to comment.