mirror of https://github.com/abpframework/abp.git
committed by
GitHub
4 changed files with 154 additions and 0 deletions
@ -1,4 +1,13 @@ |
|||
import { TemplateRef, Type } from '@angular/core'; |
|||
|
|||
export type DeepPartial<T> = { |
|||
[P in keyof T]?: T[P] extends Serializable ? DeepPartial<T[P]> : T[P]; |
|||
}; |
|||
|
|||
type Serializable = Record< |
|||
string | number | symbol, |
|||
string | number | boolean | Record<string | number | symbol, any> |
|||
>; |
|||
|
|||
export type InferredInstanceOf<T> = T extends Type<infer U> ? U : never; |
|||
export type InferredContextOf<T> = T extends TemplateRef<infer U> ? U : never; |
|||
|
|||
@ -0,0 +1,108 @@ |
|||
import clone from 'just-clone'; |
|||
import { take } from 'rxjs/operators'; |
|||
import { DeepPartial } from '../models'; |
|||
import { InternalStore } from '../utils'; |
|||
|
|||
const mockInitialState = { |
|||
foo: { |
|||
bar: { |
|||
baz: [() => {}], |
|||
qux: null as Promise<any>, |
|||
}, |
|||
n: 0, |
|||
}, |
|||
x: '', |
|||
a: false, |
|||
}; |
|||
|
|||
type MockState = typeof mockInitialState; |
|||
|
|||
const patch1: DeepPartial<MockState> = { foo: { bar: { baz: [() => {}] } } }; |
|||
const expected1: MockState = clone(mockInitialState); |
|||
expected1.foo.bar.baz = patch1.foo.bar.baz; |
|||
|
|||
const patch2: DeepPartial<MockState> = { foo: { bar: { qux: Promise.resolve() } } }; |
|||
const expected2: MockState = clone(mockInitialState); |
|||
expected2.foo.bar.qux = patch2.foo.bar.qux; |
|||
|
|||
const patch3: DeepPartial<MockState> = { foo: { n: 1 } }; |
|||
const expected3: MockState = clone(mockInitialState); |
|||
expected3.foo.n = patch3.foo.n; |
|||
|
|||
const patch4: DeepPartial<MockState> = { x: 'X' }; |
|||
const expected4: MockState = clone(mockInitialState); |
|||
expected4.x = patch4.x; |
|||
|
|||
const patch5: DeepPartial<MockState> = { a: true }; |
|||
const expected5: MockState = clone(mockInitialState); |
|||
expected5.a = patch5.a; |
|||
|
|||
describe('Internal Store', () => { |
|||
describe('sliceState', () => { |
|||
test.each` |
|||
selector | expected |
|||
${(state: MockState) => state.a} | ${mockInitialState.a} |
|||
${(state: MockState) => state.x} | ${mockInitialState.x} |
|||
${(state: MockState) => state.foo.n} | ${mockInitialState.foo.n} |
|||
${(state: MockState) => state.foo.bar} | ${mockInitialState.foo.bar} |
|||
${(state: MockState) => state.foo.bar.baz} | ${mockInitialState.foo.bar.baz} |
|||
${(state: MockState) => state.foo.bar.qux} | ${mockInitialState.foo.bar.qux} |
|||
`(
|
|||
'should return observable $expected when selector is $selector', |
|||
async ({ selector, expected }) => { |
|||
const store = new InternalStore(mockInitialState); |
|||
|
|||
const value = await store |
|||
.sliceState(selector) |
|||
.pipe(take(1)) |
|||
.toPromise(); |
|||
|
|||
expect(value).toEqual(expected); |
|||
}, |
|||
); |
|||
}); |
|||
|
|||
describe('patchState', () => { |
|||
test.each` |
|||
patch | expected |
|||
${patch1} | ${expected1} |
|||
${patch2} | ${expected2} |
|||
${patch3} | ${expected3} |
|||
${patch4} | ${expected4} |
|||
${patch5} | ${expected5} |
|||
`('should set state as $expected when patch is $patch', ({ patch, expected }) => {
|
|||
const store = new InternalStore(mockInitialState); |
|||
|
|||
store.patch(patch); |
|||
|
|||
expect(store.state).toEqual(expected); |
|||
}); |
|||
}); |
|||
|
|||
describe('sliceUpdate', () => { |
|||
it('should return slice of update$ based on selector', done => { |
|||
const store = new InternalStore(mockInitialState); |
|||
|
|||
const onQux$ = store.sliceUpdate(state => state.foo.bar.qux); |
|||
|
|||
onQux$.pipe(take(1)).subscribe(value => { |
|||
expect(value).toEqual(patch2.foo.bar.qux); |
|||
done(); |
|||
}); |
|||
|
|||
store.patch(patch1); |
|||
store.patch(patch2); |
|||
}); |
|||
}); |
|||
|
|||
describe('reset', () => { |
|||
it('should reset state to initialState', () => { |
|||
const store = new InternalStore(mockInitialState); |
|||
|
|||
store.patch(patch1); |
|||
store.reset(); |
|||
|
|||
expect(store.state).toEqual(mockInitialState); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,36 @@ |
|||
import compare from 'just-compare'; |
|||
import { BehaviorSubject, Subject } from 'rxjs'; |
|||
import { distinctUntilChanged, filter, map } from 'rxjs/operators'; |
|||
import { DeepPartial } from '../models'; |
|||
import { deepMerge } from './object-utils'; |
|||
|
|||
export class InternalStore<State> { |
|||
private state$ = new BehaviorSubject<State>(this.initialState); |
|||
|
|||
private update$ = new Subject<DeepPartial<State>>(); |
|||
|
|||
get state() { |
|||
return this.state$.value; |
|||
} |
|||
|
|||
sliceState = <Slice>( |
|||
selector: (state: State) => Slice, |
|||
compareFn: (s1: Slice, s2: Slice) => boolean = compare, |
|||
) => this.state$.pipe(map(selector), distinctUntilChanged(compareFn)); |
|||
|
|||
sliceUpdate = <Slice>( |
|||
selector: (state: DeepPartial<State>) => Slice, |
|||
filterFn = (x: Slice) => x !== undefined, |
|||
) => this.update$.pipe(map(selector), filter(filterFn)); |
|||
|
|||
constructor(private initialState: State) {} |
|||
|
|||
patch(state: DeepPartial<State>) { |
|||
this.state$.next(deepMerge(this.state, state)); |
|||
this.update$.next(state); |
|||
} |
|||
|
|||
reset() { |
|||
this.patch(this.initialState); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue