mirror of https://github.com/abpframework/abp
Merge pull request #5341 from abpframework/feat/5340
Created an InternalStore for migration from NGXSpull/5347/head
commit
1b77cb5582
@ -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