Skip to content

Commit 41758b1

Browse files
MikeRyanDevbrandonroberts
authored andcommitted
feat(store): Add 'createSelector' and 'createFeatureSelector' utils (#10)
1 parent 6a2bfe4 commit 41758b1

File tree

5 files changed

+242
-1
lines changed

5 files changed

+242
-1
lines changed

MIGRATION.md

Whitespace-only changes.

modules/store/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
export { Action, ActionReducer, ActionReducerMap, ActionReducerFactory } from './src/models';
1+
export { Action, ActionReducer, ActionReducerMap, ActionReducerFactory, Selector } from './src/models';
22
export { StoreModule } from './src/store_module';
33
export { Store } from './src/store';
44
export { combineReducers, compose } from './src/utils';
55
export { ActionsSubject, INIT } from './src/actions_subject';
66
export { ReducerManager, ReducerObservable, ReducerManagerDispatcher, UPDATE } from './src/reducer_manager';
77
export { ScannedActionsSubject } from './src/scanned_actions_subject';
8+
export { createSelector, createFeatureSelector, MemoizedSelector } from './src/selector';
89
export { State, StateObservable, reduceState } from './src/state';
910
export { INITIAL_STATE, REDUCER_FACTORY, INITIAL_REDUCERS, STORE_FEATURES } from './src/tokens';
1011
export { StoreRootModule, StoreFeatureModule } from './src/store_module';
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import 'rxjs/add/operator/distinctUntilChanged';
2+
import 'rxjs/add/operator/map';
3+
import { cold } from 'jasmine-marbles';
4+
import { createSelector, createFeatureSelector, MemoizedSelector } from '../';
5+
6+
7+
describe('Selectors', () => {
8+
let countOne: number;
9+
let countTwo: number;
10+
let countThree: number;
11+
12+
let incrementOne: jasmine.Spy;
13+
let incrementTwo: jasmine.Spy;
14+
let incrementThree: jasmine.Spy;
15+
16+
beforeEach(() => {
17+
countOne = 0;
18+
countTwo = 0;
19+
countThree = 0;
20+
21+
incrementOne = jasmine.createSpy('incrementOne').and.callFake(() => {
22+
return ++countOne;
23+
});
24+
25+
incrementTwo = jasmine.createSpy('incrementTwo').and.callFake(() => {
26+
return ++countTwo;
27+
});
28+
29+
incrementThree = jasmine.createSpy('incrementThree').and.callFake(() => {
30+
return ++countThree;
31+
});
32+
});
33+
34+
describe('createSelector', () => {
35+
it('should deliver the value of selectors to the projection function', () => {
36+
const projectFn = jasmine.createSpy('projectionFn');
37+
38+
const selector = createSelector(incrementOne, incrementTwo, projectFn)({ });
39+
40+
expect(projectFn).toHaveBeenCalledWith(countOne, countTwo);
41+
});
42+
43+
it('should memoize the function', () => {
44+
const firstState = { first: 'state' };
45+
const secondState = { second: 'state' };
46+
const projectFn = jasmine.createSpy('projectionFn');
47+
const selector = createSelector(incrementOne, incrementTwo, incrementThree, projectFn);
48+
49+
selector(firstState);
50+
selector(firstState);
51+
selector(firstState);
52+
selector(secondState);
53+
54+
expect(incrementOne).toHaveBeenCalledTimes(2);
55+
expect(incrementTwo).toHaveBeenCalledTimes(2);
56+
expect(incrementThree).toHaveBeenCalledTimes(2);
57+
expect(projectFn).toHaveBeenCalledTimes(2);
58+
});
59+
60+
it('should allow you to release memoized arguments', () => {
61+
const state = { first: 'state' };
62+
const projectFn = jasmine.createSpy('projectionFn');
63+
const selector = createSelector(incrementOne, projectFn);
64+
65+
selector(state);
66+
selector(state);
67+
selector.release();
68+
selector(state);
69+
selector(state);
70+
71+
expect(projectFn).toHaveBeenCalledTimes(2);
72+
});
73+
74+
it('should recursively release ancestor selectors', () => {
75+
const grandparent = createSelector(incrementOne, a => a);
76+
const parent = createSelector(grandparent, a => a);
77+
const child = createSelector(parent, a => a);
78+
spyOn(grandparent, 'release').and.callThrough();
79+
spyOn(parent, 'release').and.callThrough();
80+
81+
child.release();
82+
83+
expect(grandparent.release).toHaveBeenCalled();
84+
expect(parent.release).toHaveBeenCalled();
85+
});
86+
});
87+
88+
describe('createFeatureSelector', () => {
89+
let featureName = '@ngrx/router-store';
90+
let featureSelector: MemoizedSelector<any, number>;
91+
92+
beforeEach(() => {
93+
featureSelector = createFeatureSelector<number>(featureName);
94+
});
95+
96+
it('should memoize the result', () => {
97+
const firstValue = { first: 'value' };
98+
const firstState = { [featureName]: firstValue };
99+
const secondValue = { secondValue: 'value' };
100+
const secondState = { [featureName]: secondValue };
101+
102+
const state$ = cold('--a--a--a--b--', { a: firstState, b: secondState });
103+
const expected$ = cold('--a--------b--', { a: firstValue, b: secondValue });
104+
const featureState$ = state$.map(featureSelector).distinctUntilChanged();
105+
106+
expect(featureState$).toBeObservable(expected$);
107+
});
108+
});
109+
});

modules/store/src/models.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,7 @@ export interface StoreFeature<T, V extends Action = Action> {
2020
reducerFactory: ActionReducerFactory<T, V>;
2121
initialState: T | undefined;
2222
}
23+
24+
export interface Selector<T, V> {
25+
(state: T): V;
26+
}

modules/store/src/selector.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { Selector } from './models';
2+
3+
4+
export interface MemoizedSelector<State, Result> extends Selector<State, Result> {
5+
release(): void;
6+
}
7+
8+
export type AnyFn = (...args: any[]) => any;
9+
10+
export function memoize(t: AnyFn): { memoized: AnyFn, reset: () => void } {
11+
let lastArguments: null | IArguments = null;
12+
let lastResult: any = null;
13+
14+
function reset() {
15+
lastArguments = null;
16+
lastResult = null;
17+
}
18+
19+
function memoized(): any {
20+
if (!lastArguments) {
21+
lastResult = t.apply(null, arguments);
22+
lastArguments = arguments;
23+
24+
return lastResult;
25+
}
26+
for (let i = 0; i < arguments.length; i++) {
27+
if (arguments[i] !== lastArguments[i]) {
28+
lastResult = t.apply(null, arguments);
29+
lastArguments = arguments;
30+
31+
return lastResult;
32+
}
33+
}
34+
35+
return lastResult;
36+
}
37+
38+
return { memoized, reset };
39+
}
40+
41+
export function createSelector<State, S1, Result>(
42+
s1: Selector<State, S1>,
43+
projector: (S1: S1) => Result
44+
): MemoizedSelector<State, Result>;
45+
export function createSelector<State, S1, S2, Result>(
46+
s1: Selector<State, S1>,
47+
s2: Selector<State, S2>,
48+
projector: (s1: S1, s2: S2) => Result,
49+
): MemoizedSelector<State, Result>;
50+
export function createSelector<State, S1, S2, S3, Result>(
51+
s1: Selector<State, S1>,
52+
s2: Selector<State, S2>,
53+
s3: Selector<State, S3>,
54+
projector: (s1: S1, s2: S2, s3: S3) => Result,
55+
): MemoizedSelector<State, Result>;
56+
export function createSelector<State, S1, S2, S3, S4, Result>(
57+
s1: Selector<State, S1>,
58+
s2: Selector<State, S2>,
59+
s3: Selector<State, S3>,
60+
s4: Selector<State, S4>,
61+
projector: (s1: S1, s2: S2, s3: S3) => Result,
62+
): MemoizedSelector<State, Result>;
63+
export function createSelector<State, S1, S2, S3, S4, S5, Result>(
64+
s1: Selector<State, S1>,
65+
s2: Selector<State, S2>,
66+
s3: Selector<State, S3>,
67+
s4: Selector<State, S4>,
68+
s5: Selector<State, S5>,
69+
projector: (s1: S1, s2: S2, s3: S3, s4: S4, s5: S5) => Result,
70+
): MemoizedSelector<State, Result>;
71+
export function createSelector<State, S1, S2, S3, S4, S5, S6, Result>(
72+
s1: Selector<State, S1>,
73+
s2: Selector<State, S2>,
74+
s3: Selector<State, S3>,
75+
s4: Selector<State, S4>,
76+
s5: Selector<State, S5>,
77+
s6: Selector<State, S6>,
78+
projector: (s1: S1, s2: S2, s3: S3, s4: S4, s5: S5, s6: S6) => Result,
79+
): MemoizedSelector<State, Result>;
80+
export function createSelector<State, S1, S2, S3, S4, S5, S6, S7, Result>(
81+
s1: Selector<State, S1>,
82+
s2: Selector<State, S2>,
83+
s3: Selector<State, S3>,
84+
s4: Selector<State, S4>,
85+
s5: Selector<State, S5>,
86+
s6: Selector<State, S6>,
87+
s7: Selector<State, S7>,
88+
projector: (s1: S1, s2: S2, s3: S3, s4: S4, s5: S5, s6: S6, s7: S7) => Result,
89+
): MemoizedSelector<State, Result>;
90+
export function createSelector<State, S1, S2, S3, S4, S5, S6, S7, S8, Result>(
91+
s1: Selector<State, S1>,
92+
s2: Selector<State, S2>,
93+
s3: Selector<State, S3>,
94+
s4: Selector<State, S4>,
95+
s5: Selector<State, S5>,
96+
s6: Selector<State, S6>,
97+
s7: Selector<State, S7>,
98+
s8: Selector<State, S8>,
99+
projector: (s1: S1, s2: S2, s3: S3, s4: S4, s5: S5, s6: S6, s7: S7, s8: S8) => Result,
100+
): MemoizedSelector<State, Result>;
101+
export function createSelector(...args: any[]): Selector<any, any> {
102+
const selectors = args.slice(0, args.length - 1);
103+
const projector = args[args.length - 1];
104+
const memoizedSelectors = selectors.filter((selector: any) => selector.release && typeof selector.release === 'function');
105+
106+
const { memoized, reset } = memoize(function (state: any) {
107+
const args = selectors.map(fn => fn(state));
108+
109+
return projector.apply(null, args);
110+
});
111+
112+
function release() {
113+
reset();
114+
115+
memoizedSelectors.forEach(selector => selector.release());
116+
}
117+
118+
return Object.assign(memoized, { release });
119+
}
120+
121+
export function createFeatureSelector<T>(featureName: string): MemoizedSelector<object, T> {
122+
const { memoized, reset } = memoize(function (state: any): any {
123+
return state[featureName];
124+
});
125+
126+
return Object.assign(memoized, { release: reset });
127+
}

0 commit comments

Comments
 (0)