diff --git a/packages/amos-boxes/src/recordMapBox.ts b/packages/amos-boxes/src/recordMapBox.ts index ddfd35b..e15a28d 100644 --- a/packages/amos-boxes/src/recordMapBox.ts +++ b/packages/amos-boxes/src/recordMapBox.ts @@ -23,6 +23,7 @@ export interface RecordMapBox = RecordMap, never>> { setItem(key: MapKey, value: MapValue): Mutation; setItem(value: MapValue): Mutation; + /** {@link RecordMap.mergeItem} */ mergeItem(props: PartialRequiredProps, RecordMapKeyField>): Mutation; mergeItem(key: MapKey, props: PartialProps>): Mutation; } diff --git a/packages/amos-core/src/box.ts b/packages/amos-core/src/box.ts index 69a978a..def7caf 100644 --- a/packages/amos-core/src/box.ts +++ b/packages/amos-core/src/box.ts @@ -87,6 +87,10 @@ export interface BoxFactory extends BoxFactoryStatic { new (key: string, initialState: BoxState): B; } +export interface BoxFactoryMutationOptions { + update: (box: B, state: BoxState, ...args: A) => BoxState; +} + export interface BoxFactorySelectorOptions extends Omit>, 'type' | 'compute'> { derive?: (state: S, ...args: A) => R; @@ -102,7 +106,10 @@ export interface BoxFactoryOptions { ? never : P : never]: TBox[P] extends MutationFactory> - ? null | ((state: BoxState, ...args: A) => BoxState) + ? + | null + | ((state: BoxState, ...args: A) => BoxState) + | BoxFactoryMutationOptions : never; }; selectors: { @@ -152,12 +159,15 @@ function createBoxFactory( } }; for (const k in mutations) { - const fn = - typeof mutations[k] === 'function' - ? mutations[k] - : (state: any, ...args: any[]) => state[k](...args); Object.defineProperty(Box.prototype, k, { - value: function (this: Box, ...args: any[]): Mutation { + value: function (this: B, ...args: any[]): Mutation { + const fn: (state: any, ...args: any[]) => any = + typeof mutations[k] === 'function' + ? mutations[k] + : !mutations[k] + ? (state: any, ...args: any[]) => state[k](...args) + : (state: any, ...args: any[]) => + (mutations[k] as BoxFactoryMutationOptions).update(this, state, ...args); return createAmosObject('mutation', { type: `${this.key}/${k as string}`, mutator: (state: any) => fn(state, ...args), @@ -178,7 +188,7 @@ function createBoxFactory( ...resolvedOptions, id: this.id, type: `${this.key}/${k as string}`, - compute: (select: Select, ...args) => derive(select(this), ...args), + compute: (select: Select) => derive(select(this), ...args), args: args, }); }, @@ -194,7 +204,11 @@ function createBoxFactory( export const Box: BoxFactory = createBoxFactory({ name: 'Box', mutations: { - setState: (state, next) => resolveFuncValue(next, state), + setState: { + update: (box, state, ...args) => { + return args.length ? resolveFuncValue(args[0], state) : box.initialState; + }, + }, }, selectors: {}, }); diff --git a/packages/amos-core/src/selector.spec.ts b/packages/amos-core/src/selector.spec.ts index ded8be0..4da866c 100644 --- a/packages/amos-core/src/selector.spec.ts +++ b/packages/amos-core/src/selector.spec.ts @@ -3,23 +3,31 @@ * @author acrazing */ -import { selectUser } from 'amos-testing'; +import { double, fourfold, select, selectUser } from 'amos-testing'; import { createAmosObject, is } from 'amos-utils'; -import { Selector } from './selector'; +import { Selector, SelectorFactory } from './selector'; describe('selector', () => { it('should create selector factory', () => { - expect(selectUser).toBe(expect.any(Function)); + expect(selectUser).toEqual(expect.any(Function)); + expect({ ...selectUser }).toEqual( + createAmosObject('selector_factory', { + id: expect.any(String), + }), + ); }); it('should create selector', () => { - expect(selectUser()).toEqual( + const s = double(3); + expect(s).toEqual( createAmosObject('selector', { compute: expect.any(Function), - type: '', + type: 'amos/double', equal: is, - args: [], + args: [3], id: expect.any(String), }), ); + expect(s.compute(select)).toEqual(6); + expect(fourfold(3).compute(select)).toEqual(12); }); }); diff --git a/packages/amos-core/src/selector.ts b/packages/amos-core/src/selector.ts index a746a7b..53bd798 100644 --- a/packages/amos-core/src/selector.ts +++ b/packages/amos-core/src/selector.ts @@ -18,7 +18,6 @@ export type Compute = (select: Select, ...args: export interface SelectorOptions { type: string; - compute: Compute; /** * The equal fn, which is used for check the result is updated or not. If the fn @@ -50,7 +49,8 @@ export interface SelectorOptions { export interface Selector extends AmosObject<'selector'>, SelectorOptions { - args: A; + args: readonly unknown[]; + compute: (select: Select) => R; } export interface SelectorFactory @@ -58,7 +58,7 @@ export interface SelectorFactory (...args: A): Selector; } -export const enhanceSelector = enhancerCollector<[SelectorOptions], SelectorFactory>(); +export const enhanceSelector = enhancerCollector<[Compute, SelectorOptions], SelectorFactory>(); export function selector( compute: Compute, @@ -67,11 +67,11 @@ export function selector( const finalOptions = { ...options } as SelectorOptions; finalOptions.type ??= ''; finalOptions.equal ??= is; - finalOptions.compute = compute; - return enhanceSelector.apply([finalOptions], (options) => { + return enhanceSelector.apply([compute, finalOptions], (compute, options) => { const factory = createAmosObject('selector_factory', ((...args: A) => { return createAmosObject>('selector', { ...options, + compute: (select) => compute(select, ...args), id: factory.id, args, }); diff --git a/packages/amos-core/src/signal.spec.ts b/packages/amos-core/src/signal.spec.ts index d710629..ef36ef9 100644 --- a/packages/amos-core/src/signal.spec.ts +++ b/packages/amos-core/src/signal.spec.ts @@ -3,7 +3,7 @@ * @author acrazing */ -import { LOGOUT } from 'amos-testing'; +import { LOGOUT, LogoutEvent, select } from 'amos-testing'; import { isAmosObject } from 'amos-utils'; import { pick } from 'lodash'; @@ -14,13 +14,15 @@ describe('event', () => { expect(LOGOUT.dispatch).toBeInstanceOf(Function); }); it('should create signal', () => { - const s = LOGOUT({ userId: 1, sessionId: 1 }); + const e: LogoutEvent = { userId: 1, sessionId: 1 }; + const s = LOGOUT(e); expect(isAmosObject(s, 'signal')).toBe(true); expect(pick(s, 'args', 'creator', 'factory', 'type')).toEqual({ - args: [{ userId: 1, sessionId: 1 }], + args: [e], creator: expect.any(Function), factory: LOGOUT, type: 'session.logout', }); + expect(s.creator(select, ...s.args)).toBe(e); }); }); diff --git a/packages/amos-core/src/signal.ts b/packages/amos-core/src/signal.ts index 8e7dcdb..75fe0e7 100644 --- a/packages/amos-core/src/signal.ts +++ b/packages/amos-core/src/signal.ts @@ -9,7 +9,7 @@ import { createEventCenter, enhancerCollector, EventCenter, - identity, + second, } from 'amos-utils'; import { Dispatch, Select } from './types'; @@ -64,7 +64,7 @@ export function signal(a: any, b?: any, c?: any): SignalFactory { const bIsFunc = typeof b === 'function'; const options: SignalOptions = (bIsFunc ? c : b) || {}; options.type = a; - options.creator = (bIsFunc && b) || identity; + options.creator = (bIsFunc && b) || second; const factory = enhanceSignal.apply([options as SignalOptions], (options) => { return Object.assign((...args: any[]) => { return createAmosObject('signal', { diff --git a/packages/amos-core/src/store.spec.ts b/packages/amos-core/src/store.spec.ts index fa28a6a..d01ffc7 100644 --- a/packages/amos-core/src/store.spec.ts +++ b/packages/amos-core/src/store.spec.ts @@ -3,7 +3,20 @@ * @author junbao */ -import { loginAsync, LOGOUT, LogoutEvent, sessionIdBox, sessionMapBox } from 'amos-testing'; +import { + addTodo, + countBox, + LOGIN, + loginAsync, + loginSync, + LOGOUT, + LogoutEvent, + selectTodoList, + sessionIdBox, + sessionMapBox, + todoMapBox, +} from 'amos-testing'; +import { pick } from 'lodash'; import { createStore, Store } from './store'; describe('store', () => { @@ -37,6 +50,37 @@ describe('store', () => { const s4 = store.dispatch(LOGOUT({ userId: 1, sessionId: s3 })); expect(s4).toEqual({ userId: 1, sessionId: s3 }); state[sessionMapBox.key] = sessionMapBox.initialState; - expect(store.snapshot()).toEqual(state); + expect(pick(store.snapshot(), Object.keys(state))).toMatchObject(state); + }); + + it('should select base selectable', async () => { + const { select, dispatch } = createStore(); + expect(select(sessionMapBox)).toBe(sessionMapBox.initialState); + const id = await dispatch(addTodo({ title: 'Hello', description: 'World' })); + expect(pick(select(todoMapBox.getItem(id)).toJSON(), ['id', 'title', 'description'])).toEqual({ + id: id, + title: 'Hello', + description: 'World', + }); + expect(select(selectTodoList()).toJSON()).toEqual([id]); + }); + + it('should dispatch event', () => { + const { select, dispatch, subscribe } = createStore(); + const f1 = jest.fn(); + const u1 = subscribe(f1); + dispatch(loginSync(1)); + expect(f1).toHaveBeenCalledTimes(1); + f1.mockReset(); + select(countBox); + expect(f1).toHaveBeenCalledTimes(0); + dispatch(loginSync(1)); + dispatch(countBox.setState()); + dispatch(LOGIN({ userId: 1, sessionId: 1 })); + expect(f1).toHaveBeenCalledTimes(3); + f1.mockReset(); + u1(); + dispatch(LOGOUT({ userId: 1, sessionId: 1 })); + expect(f1).toHaveBeenCalledTimes(0); }); }); diff --git a/packages/amos-core/src/store.ts b/packages/amos-core/src/store.ts index 2eaf56a..d240d03 100644 --- a/packages/amos-core/src/store.ts +++ b/packages/amos-core/src/store.ts @@ -148,7 +148,7 @@ export function createStore(options: StoreOptions = {}, ...enhancers: StoreEnhan } return store.state[selectable.key]; } - return selectable.compute(store.select, ...selectable.args); + return selectable.compute(store.select); }, }; }, diff --git a/packages/amos-shapes/src/RecordMap.ts b/packages/amos-shapes/src/RecordMap.ts index 3daac21..af80f6d 100644 --- a/packages/amos-shapes/src/RecordMap.ts +++ b/packages/amos-shapes/src/RecordMap.ts @@ -37,6 +37,7 @@ export class RecordMap, KF extends keyof RecordProps> e override mergeItem(a: any, b?: any) { const props = b ?? a; const key = b ? a : a[this.keyField]; + props[this.keyField] = key; return this.setItem(key, this.getItem(key).merge(props)); } @@ -69,7 +70,15 @@ export class RecordMap, KF extends keyof RecordProps> e | ReadonlyArray | Entry, PartialProps>>, ): this { if (Array.isArray(items)) { - return super.setAll(items.map((v): any => (Array.isArray(v) ? v : [v[this.keyField], v]))); + return super.setAll( + items.map((v): any => { + if (Array.isArray(v)) { + v[1][this.keyField] = v[0]; + return v; + } + return [v[this.keyField], v]; + }), + ); } else { return super.setAll( Object.entries(items).map(([k, v]): any => [k, this.getItem(k as IDOf).merge(v)]), diff --git a/packages/amos-testing/src/store/misc.actions.ts b/packages/amos-testing/src/store/misc.actions.ts index 400583d..32024fb 100644 --- a/packages/amos-testing/src/store/misc.actions.ts +++ b/packages/amos-testing/src/store/misc.actions.ts @@ -5,12 +5,15 @@ * keep atomic boxes */ -import { action } from 'amos-core'; -import { countBox } from './misc.boxes'; +import { action, selector } from 'amos-core'; import { sleep } from '../utils'; +import { countBox } from './misc.boxes'; export const addTwiceAsync = action(async (dispatch, select, value: number) => { await sleep(1); dispatch(countBox.add(value)); dispatch(countBox.add(value)); }); + +export const double = selector((select, v: number) => v * 2); +export const fourfold = selector((select, v: number) => select(double(v)) * 2); diff --git a/packages/amos-testing/src/store/session.boxes.ts b/packages/amos-testing/src/store/session.boxes.ts index d3fbd69..cd6d422 100644 --- a/packages/amos-testing/src/store/session.boxes.ts +++ b/packages/amos-testing/src/store/session.boxes.ts @@ -25,6 +25,6 @@ export class SessionRecord extends Record({ } export const sessionMapBox = recordMapBox('sessions', SessionRecord, 'id'); -sessionMapBox.subscribe(LOGOUT, (state, data) => state.removeItem(data.userId)); +sessionMapBox.subscribe(LOGOUT, (state, data) => state.removeItem(data.sessionId)); export const sessionIdBox = box('sessions.currentId', 0); diff --git a/packages/amos-testing/src/store/todo.actions.ts b/packages/amos-testing/src/store/todo.actions.ts index 3342f7d..edcf8e6 100644 --- a/packages/amos-testing/src/store/todo.actions.ts +++ b/packages/amos-testing/src/store/todo.actions.ts @@ -18,5 +18,6 @@ export const addTodo = action( await sleep(); const id = Math.random(); dispatch([todoMapBox.mergeItem(id, input), userTodoListBox.unshiftIn(userId, id)]); + return id; }, ); diff --git a/packages/amos-utils/src/equals.ts b/packages/amos-utils/src/equals.ts index 729b3fe..d318e2d 100644 --- a/packages/amos-utils/src/equals.ts +++ b/packages/amos-utils/src/equals.ts @@ -19,6 +19,7 @@ export const is: (x: any, y: any) => boolean = Object.is || shimObjectIs; * @param v */ export const identity = (v: T) => v; +export const second = (a: any, b: T) => b; export const notNullable = (v: T): v is Exclude => v != null; export const isNullable = (v: unknown): v is undefined | null => v == null; export const isTruly = (v: T): v is Exclude => !!v;