From d4dd369bfa7e30173f72de247597c147d33acef2 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Tue, 8 Sep 2020 18:51:02 +0300 Subject: [PATCH 1/2] feat: add DeepPartial to utility types --- npm/ng-packs/packages/core/src/lib/models/utility.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/npm/ng-packs/packages/core/src/lib/models/utility.ts b/npm/ng-packs/packages/core/src/lib/models/utility.ts index e84ad15340..228cab0de3 100644 --- a/npm/ng-packs/packages/core/src/lib/models/utility.ts +++ b/npm/ng-packs/packages/core/src/lib/models/utility.ts @@ -1,4 +1,13 @@ import { TemplateRef, Type } from '@angular/core'; +export type DeepPartial = { + [P in keyof T]?: T[P] extends Serializable ? DeepPartial : T[P]; +}; + +type Serializable = Record< + string | number | symbol, + string | number | boolean | Record +>; + export type InferredInstanceOf = T extends Type ? U : never; export type InferredContextOf = T extends TemplateRef ? U : never; From 3db16151f1570a562050e7e8d76f14b5b78c4fe6 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Tue, 8 Sep 2020 18:51:26 +0300 Subject: [PATCH 2/2] feat: add InternalStore utility class --- .../core/src/lib/tests/internal-store.spec.ts | 108 ++++++++++++++++++ .../packages/core/src/lib/utils/index.ts | 1 + .../src/lib/utils/internal-store-utils.ts | 36 ++++++ 3 files changed, 145 insertions(+) create mode 100644 npm/ng-packs/packages/core/src/lib/tests/internal-store.spec.ts create mode 100644 npm/ng-packs/packages/core/src/lib/utils/internal-store-utils.ts diff --git a/npm/ng-packs/packages/core/src/lib/tests/internal-store.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/internal-store.spec.ts new file mode 100644 index 0000000000..e0eed44eb6 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/internal-store.spec.ts @@ -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, + }, + n: 0, + }, + x: '', + a: false, +}; + +type MockState = typeof mockInitialState; + +const patch1: DeepPartial = { foo: { bar: { baz: [() => {}] } } }; +const expected1: MockState = clone(mockInitialState); +expected1.foo.bar.baz = patch1.foo.bar.baz; + +const patch2: DeepPartial = { foo: { bar: { qux: Promise.resolve() } } }; +const expected2: MockState = clone(mockInitialState); +expected2.foo.bar.qux = patch2.foo.bar.qux; + +const patch3: DeepPartial = { foo: { n: 1 } }; +const expected3: MockState = clone(mockInitialState); +expected3.foo.n = patch3.foo.n; + +const patch4: DeepPartial = { x: 'X' }; +const expected4: MockState = clone(mockInitialState); +expected4.x = patch4.x; + +const patch5: DeepPartial = { 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); + }); + }); +}); diff --git a/npm/ng-packs/packages/core/src/lib/utils/index.ts b/npm/ng-packs/packages/core/src/lib/utils/index.ts index 383261fc07..429a59d0b1 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/index.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/index.ts @@ -6,6 +6,7 @@ export * from './factory-utils'; export * from './form-utils'; export * from './generator-utils'; export * from './initial-utils'; +export * from './internal-store-utils'; export * from './lazy-load-utils'; export * from './localization-utils'; export * from './multi-tenancy-utils'; diff --git a/npm/ng-packs/packages/core/src/lib/utils/internal-store-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/internal-store-utils.ts new file mode 100644 index 0000000000..7a481121ee --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/utils/internal-store-utils.ts @@ -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 { + private state$ = new BehaviorSubject(this.initialState); + + private update$ = new Subject>(); + + get state() { + return this.state$.value; + } + + sliceState = ( + selector: (state: State) => Slice, + compareFn: (s1: Slice, s2: Slice) => boolean = compare, + ) => this.state$.pipe(map(selector), distinctUntilChanged(compareFn)); + + sliceUpdate = ( + selector: (state: DeepPartial) => Slice, + filterFn = (x: Slice) => x !== undefined, + ) => this.update$.pipe(map(selector), filter(filterFn)); + + constructor(private initialState: State) {} + + patch(state: DeepPartial) { + this.state$.next(deepMerge(this.state, state)); + this.update$.next(state); + } + + reset() { + this.patch(this.initialState); + } +}