Skip to content

Commit cb0d185

Browse files
brandonrobertsMikeRyanDev
authored andcommitted
feat(Store): Add support for generating custom createSelector functions (#734)
Closes #478, #724
1 parent b82c35d commit cb0d185

File tree

3 files changed

+136
-31
lines changed

3 files changed

+136
-31
lines changed

modules/store/spec/selector.spec.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import 'rxjs/add/operator/distinctUntilChanged';
22
import 'rxjs/add/operator/map';
33
import { cold } from 'jasmine-marbles';
4-
import { createSelector, createFeatureSelector } from '../';
4+
import {
5+
createSelector,
6+
createFeatureSelector,
7+
defaultMemoize,
8+
createSelectorFactory,
9+
} from '../';
510

611
describe('Selectors', () => {
712
let countOne: number;
@@ -229,4 +234,53 @@ describe('Selectors', () => {
229234
expect(featureState$).toBeObservable(expected$);
230235
});
231236
});
237+
238+
describe('createSelectorFactory', () => {
239+
it('should return a selector creator function', () => {
240+
const projectFn = jasmine.createSpy('projectionFn');
241+
const selectorFunc = createSelectorFactory(defaultMemoize);
242+
243+
const selector = selectorFunc(incrementOne, incrementTwo, projectFn)({});
244+
245+
expect(projectFn).toHaveBeenCalledWith(countOne, countTwo);
246+
});
247+
248+
it('should allow a custom memoization function', () => {
249+
const projectFn = jasmine.createSpy('projectionFn');
250+
const anyFn = jasmine.createSpy('t').and.callFake(() => true);
251+
const equalFn = jasmine.createSpy('isEqual').and.callFake(() => true);
252+
const customMemoizer = (aFn: any = anyFn, eFn: any = equalFn) =>
253+
defaultMemoize(anyFn, equalFn);
254+
const customSelector = createSelectorFactory(customMemoizer);
255+
256+
const selector = customSelector(incrementOne, incrementTwo, projectFn);
257+
selector(1);
258+
selector(2);
259+
260+
expect(anyFn.calls.count()).toEqual(1);
261+
});
262+
263+
it('should allow a custom state memoization function', () => {
264+
const projectFn = jasmine.createSpy('projectionFn');
265+
const stateFn = jasmine.createSpy('stateFn');
266+
const selectorFunc = createSelectorFactory(defaultMemoize, { stateFn });
267+
268+
const selector = selectorFunc(incrementOne, incrementTwo, projectFn)({});
269+
270+
expect(stateFn).toHaveBeenCalled();
271+
});
272+
});
273+
274+
describe('defaultMemoize', () => {
275+
it('should allow a custom equality function', () => {
276+
const anyFn = jasmine.createSpy('t').and.callFake(() => true);
277+
const equalFn = jasmine.createSpy('isEqual').and.callFake(() => true);
278+
const memoizer = defaultMemoize(anyFn, equalFn);
279+
280+
memoizer.memoized(1, 2, 3);
281+
memoizer.memoized(1, 2);
282+
283+
expect(anyFn.calls.count()).toEqual(1);
284+
});
285+
});
232286
});

modules/store/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ export {
1818
export { ScannedActionsSubject } from './scanned_actions_subject';
1919
export {
2020
createSelector,
21+
createSelectorFactory,
2122
createFeatureSelector,
23+
defaultMemoize,
24+
defaultStateFn,
25+
MemoizeFn,
26+
MemoizedProjection,
2227
MemoizedSelector,
2328
} from './selector';
2429
export { State, StateObservable, reduceState } from './state';

modules/store/src/selector.ts

Lines changed: 76 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,24 @@ import { Selector } from './models';
22

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

5+
export type MemoizedProjection = { memoized: AnyFn; reset: () => void };
6+
7+
export type MemoizeFn = (t: AnyFn) => MemoizedProjection;
8+
59
export interface MemoizedSelector<State, Result>
610
extends Selector<State, Result> {
711
release(): void;
812
projector: AnyFn;
913
}
1014

11-
export function memoize(t: AnyFn): { memoized: AnyFn; reset: () => void } {
15+
export function isEqualCheck(a: any, b: any): boolean {
16+
return a === b;
17+
}
18+
19+
export function defaultMemoize(
20+
t: AnyFn,
21+
isEqual = isEqualCheck
22+
): MemoizedProjection {
1223
let lastArguments: null | IArguments = null;
1324
let lastResult: any = null;
1425

@@ -24,8 +35,9 @@ export function memoize(t: AnyFn): { memoized: AnyFn; reset: () => void } {
2435

2536
return lastResult;
2637
}
38+
2739
for (let i = 0; i < arguments.length; i++) {
28-
if (arguments[i] !== lastArguments[i]) {
40+
if (!isEqual(arguments[i], lastArguments[i])) {
2941
lastResult = t.apply(null, arguments);
3042
lastArguments = arguments;
3143

@@ -184,41 +196,75 @@ export function createSelector<State, S1, S2, S3, S4, S5, S6, S7, S8, Result>(
184196
s8: S8
185197
) => Result
186198
): MemoizedSelector<State, Result>;
187-
export function createSelector(...input: any[]): Selector<any, any> {
188-
let args = input;
189-
if (Array.isArray(args[0])) {
190-
const [head, ...tail] = args;
191-
args = [...head, ...tail];
192-
}
199+
export function createSelector(...input: any[]) {
200+
return createSelectorFactory(defaultMemoize)(...input);
201+
}
193202

194-
const selectors = args.slice(0, args.length - 1);
195-
const projector = args[args.length - 1];
196-
const memoizedSelectors = selectors.filter(
197-
(selector: any) =>
198-
selector.release && typeof selector.release === 'function'
199-
);
203+
export function defaultStateFn(
204+
state: any,
205+
selectors: Selector<any, any>[],
206+
memoizedProjector: MemoizedProjection
207+
): any {
208+
const args = selectors.map(fn => fn(state));
200209

201-
const memoizedProjector = memoize(function(...selectors: any[]) {
202-
return projector.apply(null, selectors);
203-
});
210+
return memoizedProjector.memoized.apply(null, args);
211+
}
204212

205-
const memoizedState = memoize(function(state: any) {
206-
const args = selectors.map(fn => fn(state));
213+
export type SelectorFactoryConfig<T = any, V = any> = {
214+
stateFn: (
215+
state: T,
216+
selectors: Selector<any, any>[],
217+
memoizedProjector: MemoizedProjection
218+
) => V;
219+
};
207220

208-
return memoizedProjector.memoized.apply(null, args);
209-
});
221+
export function createSelectorFactory<T = any, V = any>(
222+
memoize: MemoizeFn
223+
): (...input: any[]) => Selector<T, V>;
224+
export function createSelectorFactory<T = any, V = any>(
225+
memoize: MemoizeFn,
226+
options: SelectorFactoryConfig<T, V>
227+
): (...input: any[]) => Selector<T, V>;
228+
export function createSelectorFactory(
229+
memoize: MemoizeFn,
230+
options: SelectorFactoryConfig<any, any> = {
231+
stateFn: defaultStateFn,
232+
}
233+
) {
234+
return function(...input: any[]): Selector<any, any> {
235+
let args = input;
236+
if (Array.isArray(args[0])) {
237+
const [head, ...tail] = args;
238+
args = [...head, ...tail];
239+
}
210240

211-
function release() {
212-
memoizedState.reset();
213-
memoizedProjector.reset();
241+
const selectors = args.slice(0, args.length - 1);
242+
const projector = args[args.length - 1];
243+
const memoizedSelectors = selectors.filter(
244+
(selector: any) =>
245+
selector.release && typeof selector.release === 'function'
246+
);
214247

215-
memoizedSelectors.forEach(selector => selector.release());
216-
}
248+
const memoizedProjector = memoize(function(...selectors: any[]) {
249+
return projector.apply(null, selectors);
250+
});
251+
252+
const memoizedState = defaultMemoize(function(state: any) {
253+
return options.stateFn.apply(null, [state, selectors, memoizedProjector]);
254+
});
255+
256+
function release() {
257+
memoizedState.reset();
258+
memoizedProjector.reset();
259+
260+
memoizedSelectors.forEach(selector => selector.release());
261+
}
217262

218-
return Object.assign(memoizedState.memoized, {
219-
release,
220-
projector: memoizedProjector.memoized,
221-
});
263+
return Object.assign(memoizedState.memoized, {
264+
release,
265+
projector: memoizedProjector.memoized,
266+
});
267+
};
222268
}
223269

224270
export function createFeatureSelector<T>(

0 commit comments

Comments
 (0)