-
-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(store): Add 'createSelector' and 'createFeatureSelector' utils (#10
- Loading branch information
1 parent
6a2bfe4
commit 41758b1
Showing
5 changed files
with
242 additions
and
1 deletion.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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$); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); | ||
} |