Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Store): Add support for generating custom createSelector functions #734

Merged
merged 1 commit into from
Jan 22, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion modules/store/spec/selector.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/map';
import { cold } from 'jasmine-marbles';
import { createSelector, createFeatureSelector } from '../';
import {
createSelector,
createFeatureSelector,
defaultMemoize,
createSelectorFactory,
} from '../';

describe('Selectors', () => {
let countOne: number;
Expand Down Expand Up @@ -229,4 +234,53 @@ describe('Selectors', () => {
expect(featureState$).toBeObservable(expected$);
});
});

describe('createSelectorFactory', () => {
it('should return a selector creator function', () => {
const projectFn = jasmine.createSpy('projectionFn');
const selectorFunc = createSelectorFactory(defaultMemoize);

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

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

it('should allow a custom memoization function', () => {
const projectFn = jasmine.createSpy('projectionFn');
const anyFn = jasmine.createSpy('t').and.callFake(() => true);
const equalFn = jasmine.createSpy('isEqual').and.callFake(() => true);
const customMemoizer = (aFn: any = anyFn, eFn: any = equalFn) =>
defaultMemoize(anyFn, equalFn);
const customSelector = createSelectorFactory(customMemoizer);

const selector = customSelector(incrementOne, incrementTwo, projectFn);
selector(1);
selector(2);

expect(anyFn.calls.count()).toEqual(1);
});

it('should allow a custom state memoization function', () => {
const projectFn = jasmine.createSpy('projectionFn');
const stateFn = jasmine.createSpy('stateFn');
const selectorFunc = createSelectorFactory(defaultMemoize, { stateFn });

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

expect(stateFn).toHaveBeenCalled();
});
});

describe('defaultMemoize', () => {
it('should allow a custom equality function', () => {
const anyFn = jasmine.createSpy('t').and.callFake(() => true);
const equalFn = jasmine.createSpy('isEqual').and.callFake(() => true);
const memoizer = defaultMemoize(anyFn, equalFn);

memoizer.memoized(1, 2, 3);
memoizer.memoized(1, 2);

expect(anyFn.calls.count()).toEqual(1);
});
});
});
5 changes: 5 additions & 0 deletions modules/store/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ export {
export { ScannedActionsSubject } from './scanned_actions_subject';
export {
createSelector,
createSelectorFactory,
createFeatureSelector,
defaultMemoize,
defaultStateFn,
MemoizeFn,
MemoizedProjection,
MemoizedSelector,
} from './selector';
export { State, StateObservable, reduceState } from './state';
Expand Down
106 changes: 76 additions & 30 deletions modules/store/src/selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,24 @@ import { Selector } from './models';

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

export type MemoizedProjection = { memoized: AnyFn; reset: () => void };

export type MemoizeFn = (t: AnyFn) => MemoizedProjection;

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

export function memoize(t: AnyFn): { memoized: AnyFn; reset: () => void } {
export function isEqualCheck(a: any, b: any): boolean {
return a === b;
}

export function defaultMemoize(
t: AnyFn,
isEqual = isEqualCheck
): MemoizedProjection {
let lastArguments: null | IArguments = null;
let lastResult: any = null;

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

return lastResult;
}

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

Expand Down Expand Up @@ -184,41 +196,75 @@ export function createSelector<State, S1, S2, S3, S4, S5, S6, S7, S8, Result>(
s8: S8
) => Result
): MemoizedSelector<State, Result>;
export function createSelector(...input: any[]): Selector<any, any> {
let args = input;
if (Array.isArray(args[0])) {
const [head, ...tail] = args;
args = [...head, ...tail];
}
export function createSelector(...input: any[]) {
return createSelectorFactory(defaultMemoize)(...input);
}

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'
);
export function defaultStateFn(
state: any,
selectors: Selector<any, any>[],
memoizedProjector: MemoizedProjection
): any {
const args = selectors.map(fn => fn(state));

const memoizedProjector = memoize(function(...selectors: any[]) {
return projector.apply(null, selectors);
});
return memoizedProjector.memoized.apply(null, args);
}

const memoizedState = memoize(function(state: any) {
const args = selectors.map(fn => fn(state));
export type SelectorFactoryConfig<T = any, V = any> = {
stateFn: (
state: T,
selectors: Selector<any, any>[],
memoizedProjector: MemoizedProjection
) => V;
};

return memoizedProjector.memoized.apply(null, args);
});
export function createSelectorFactory<T = any, V = any>(
memoize: MemoizeFn
): (...input: any[]) => Selector<T, V>;
export function createSelectorFactory<T = any, V = any>(
memoize: MemoizeFn,
options: SelectorFactoryConfig<T, V>
): (...input: any[]) => Selector<T, V>;
export function createSelectorFactory(
memoize: MemoizeFn,
options: SelectorFactoryConfig<any, any> = {
stateFn: defaultStateFn,
}
) {
return function(...input: any[]): Selector<any, any> {
let args = input;
if (Array.isArray(args[0])) {
const [head, ...tail] = args;
args = [...head, ...tail];
}

function release() {
memoizedState.reset();
memoizedProjector.reset();
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'
);

memoizedSelectors.forEach(selector => selector.release());
}
const memoizedProjector = memoize(function(...selectors: any[]) {
return projector.apply(null, selectors);
});

const memoizedState = defaultMemoize(function(state: any) {
return options.stateFn.apply(null, [state, selectors, memoizedProjector]);
});

function release() {
memoizedState.reset();
memoizedProjector.reset();

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

return Object.assign(memoizedState.memoized, {
release,
projector: memoizedProjector.memoized,
});
return Object.assign(memoizedState.memoized, {
release,
projector: memoizedProjector.memoized,
});
};
}

export function createFeatureSelector<T>(
Expand Down