Skip to content

Commit

Permalink
feat(store): Add 'createSelector' and 'createFeatureSelector' utils (#10
Browse files Browse the repository at this point in the history
)
  • Loading branch information
MikeRyanDev authored and brandonroberts committed Apr 13, 2017
1 parent 6a2bfe4 commit 41758b1
Show file tree
Hide file tree
Showing 5 changed files with 242 additions and 1 deletion.
Empty file added MIGRATION.md
Empty file.
3 changes: 2 additions & 1 deletion modules/store/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
export { Action, ActionReducer, ActionReducerMap, ActionReducerFactory } from './src/models';
export { Action, ActionReducer, ActionReducerMap, ActionReducerFactory, Selector } from './src/models';
export { StoreModule } from './src/store_module';
export { Store } from './src/store';
export { combineReducers, compose } from './src/utils';
export { ActionsSubject, INIT } from './src/actions_subject';
export { ReducerManager, ReducerObservable, ReducerManagerDispatcher, UPDATE } from './src/reducer_manager';
export { ScannedActionsSubject } from './src/scanned_actions_subject';
export { createSelector, createFeatureSelector, MemoizedSelector } from './src/selector';
export { State, StateObservable, reduceState } from './src/state';
export { INITIAL_STATE, REDUCER_FACTORY, INITIAL_REDUCERS, STORE_FEATURES } from './src/tokens';
export { StoreRootModule, StoreFeatureModule } from './src/store_module';
109 changes: 109 additions & 0 deletions modules/store/spec/selector.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/map';
import { cold } from 'jasmine-marbles';
import { createSelector, createFeatureSelector, MemoizedSelector } from '../';


describe('Selectors', () => {
let countOne: number;
let countTwo: number;
let countThree: number;

let incrementOne: jasmine.Spy;
let incrementTwo: jasmine.Spy;
let incrementThree: jasmine.Spy;

beforeEach(() => {
countOne = 0;
countTwo = 0;
countThree = 0;

incrementOne = jasmine.createSpy('incrementOne').and.callFake(() => {
return ++countOne;
});

incrementTwo = jasmine.createSpy('incrementTwo').and.callFake(() => {
return ++countTwo;
});

incrementThree = jasmine.createSpy('incrementThree').and.callFake(() => {
return ++countThree;
});
});

describe('createSelector', () => {
it('should deliver the value of selectors to the projection function', () => {
const projectFn = jasmine.createSpy('projectionFn');

const selector = createSelector(incrementOne, incrementTwo, projectFn)({ });

expect(projectFn).toHaveBeenCalledWith(countOne, countTwo);
});

it('should memoize the function', () => {
const firstState = { first: 'state' };
const secondState = { second: 'state' };
const projectFn = jasmine.createSpy('projectionFn');
const selector = createSelector(incrementOne, incrementTwo, incrementThree, projectFn);

selector(firstState);
selector(firstState);
selector(firstState);
selector(secondState);

expect(incrementOne).toHaveBeenCalledTimes(2);
expect(incrementTwo).toHaveBeenCalledTimes(2);
expect(incrementThree).toHaveBeenCalledTimes(2);
expect(projectFn).toHaveBeenCalledTimes(2);
});

it('should allow you to release memoized arguments', () => {
const state = { first: 'state' };
const projectFn = jasmine.createSpy('projectionFn');
const selector = createSelector(incrementOne, projectFn);

selector(state);
selector(state);
selector.release();
selector(state);
selector(state);

expect(projectFn).toHaveBeenCalledTimes(2);
});

it('should recursively release ancestor selectors', () => {
const grandparent = createSelector(incrementOne, a => a);
const parent = createSelector(grandparent, a => a);
const child = createSelector(parent, a => a);
spyOn(grandparent, 'release').and.callThrough();
spyOn(parent, 'release').and.callThrough();

child.release();

expect(grandparent.release).toHaveBeenCalled();
expect(parent.release).toHaveBeenCalled();
});
});

describe('createFeatureSelector', () => {
let featureName = '@ngrx/router-store';
let featureSelector: MemoizedSelector<any, number>;

beforeEach(() => {
featureSelector = createFeatureSelector<number>(featureName);
});

it('should memoize the result', () => {
const firstValue = { first: 'value' };
const firstState = { [featureName]: firstValue };
const secondValue = { secondValue: 'value' };
const secondState = { [featureName]: secondValue };

const state$ = cold('--a--a--a--b--', { a: firstState, b: secondState });
const expected$ = cold('--a--------b--', { a: firstValue, b: secondValue });
const featureState$ = state$.map(featureSelector).distinctUntilChanged();

expect(featureState$).toBeObservable(expected$);
});
});
});
4 changes: 4 additions & 0 deletions modules/store/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ export interface StoreFeature<T, V extends Action = Action> {
reducerFactory: ActionReducerFactory<T, V>;
initialState: T | undefined;
}

export interface Selector<T, V> {
(state: T): V;
}
127 changes: 127 additions & 0 deletions modules/store/src/selector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { Selector } from './models';


export interface MemoizedSelector<State, Result> extends Selector<State, Result> {
release(): void;
}

export type AnyFn = (...args: any[]) => any;

export function memoize(t: AnyFn): { memoized: AnyFn, reset: () => void } {
let lastArguments: null | IArguments = null;
let lastResult: any = null;

function reset() {
lastArguments = null;
lastResult = null;
}

function memoized(): any {
if (!lastArguments) {
lastResult = t.apply(null, arguments);
lastArguments = arguments;

return lastResult;
}
for (let i = 0; i < arguments.length; i++) {
if (arguments[i] !== lastArguments[i]) {
lastResult = t.apply(null, arguments);
lastArguments = arguments;

return lastResult;
}
}

return lastResult;
}

return { memoized, reset };
}

export function createSelector<State, S1, Result>(
s1: Selector<State, S1>,
projector: (S1: S1) => Result
): MemoizedSelector<State, Result>;
export function createSelector<State, S1, S2, Result>(
s1: Selector<State, S1>,
s2: Selector<State, S2>,
projector: (s1: S1, s2: S2) => Result,
): MemoizedSelector<State, Result>;
export function createSelector<State, S1, S2, S3, Result>(
s1: Selector<State, S1>,
s2: Selector<State, S2>,
s3: Selector<State, S3>,
projector: (s1: S1, s2: S2, s3: S3) => Result,
): MemoizedSelector<State, Result>;
export function createSelector<State, S1, S2, S3, S4, Result>(
s1: Selector<State, S1>,
s2: Selector<State, S2>,
s3: Selector<State, S3>,
s4: Selector<State, S4>,
projector: (s1: S1, s2: S2, s3: S3) => Result,
): MemoizedSelector<State, Result>;
export function createSelector<State, S1, S2, S3, S4, S5, Result>(
s1: Selector<State, S1>,
s2: Selector<State, S2>,
s3: Selector<State, S3>,
s4: Selector<State, S4>,
s5: Selector<State, S5>,
projector: (s1: S1, s2: S2, s3: S3, s4: S4, s5: S5) => Result,
): MemoizedSelector<State, Result>;
export function createSelector<State, S1, S2, S3, S4, S5, S6, Result>(
s1: Selector<State, S1>,
s2: Selector<State, S2>,
s3: Selector<State, S3>,
s4: Selector<State, S4>,
s5: Selector<State, S5>,
s6: Selector<State, S6>,
projector: (s1: S1, s2: S2, s3: S3, s4: S4, s5: S5, s6: S6) => Result,
): MemoizedSelector<State, Result>;
export function createSelector<State, S1, S2, S3, S4, S5, S6, S7, Result>(
s1: Selector<State, S1>,
s2: Selector<State, S2>,
s3: Selector<State, S3>,
s4: Selector<State, S4>,
s5: Selector<State, S5>,
s6: Selector<State, S6>,
s7: Selector<State, S7>,
projector: (s1: S1, s2: S2, s3: S3, s4: S4, s5: S5, s6: S6, s7: S7) => Result,
): MemoizedSelector<State, Result>;
export function createSelector<State, S1, S2, S3, S4, S5, S6, S7, S8, Result>(
s1: Selector<State, S1>,
s2: Selector<State, S2>,
s3: Selector<State, S3>,
s4: Selector<State, S4>,
s5: Selector<State, S5>,
s6: Selector<State, S6>,
s7: Selector<State, S7>,
s8: Selector<State, S8>,
projector: (s1: S1, s2: S2, s3: S3, s4: S4, s5: S5, s6: S6, s7: S7, s8: S8) => Result,
): MemoizedSelector<State, Result>;
export function createSelector(...args: any[]): Selector<any, any> {
const selectors = args.slice(0, args.length - 1);
const projector = args[args.length - 1];
const memoizedSelectors = selectors.filter((selector: any) => selector.release && typeof selector.release === 'function');

const { memoized, reset } = memoize(function (state: any) {
const args = selectors.map(fn => fn(state));

return projector.apply(null, args);
});

function release() {
reset();

memoizedSelectors.forEach(selector => selector.release());
}

return Object.assign(memoized, { release });
}

export function createFeatureSelector<T>(featureName: string): MemoizedSelector<object, T> {
const { memoized, reset } = memoize(function (state: any): any {
return state[featureName];
});

return Object.assign(memoized, { release: reset });
}

0 comments on commit 41758b1

Please sign in to comment.