/* * @since 2020-11-03 13:31:31 * @author acrazing */ import { Action } from './action'; import { Box, Mutation } from './box'; import { Selector } from './selector'; import { Signal } from './signal'; import { AmosObject, defineAmosObject, isArray } from './utils'; /** * the state snapshot in store * * @stable */ export type Snapshot = Record; /** * dispatchable things * * @stable */ export type Dispatchable = Mutation | Action | Signal; /** * base amos signature, this is used for someone want to change the signature of useDispatch() * * @stable */ export interface AmosDispatch extends AmosObject<'store.dispatch'> { (task: Dispatchable): R; (tasks: readonly [Dispatchable]): [R1]; (tasks: readonly [Dispatchable, Dispatchable]): [R1, R2]; (tasks: readonly [Dispatchable, Dispatchable, Dispatchable]): [ R1, R2, R3, ]; ( tasks: readonly [Dispatchable, Dispatchable, Dispatchable, Dispatchable], ): [R1, R2, R3, R4]; ( tasks: readonly [ Dispatchable, Dispatchable, Dispatchable, Dispatchable, Dispatchable, ], ): [R1, R2, R3, R4, R5]; ( tasks: readonly [ Dispatchable, Dispatchable, Dispatchable, Dispatchable, Dispatchable, Dispatchable, ], ): [R1, R2, R3, R4, R5, R6]; ( tasks: readonly [ Dispatchable, Dispatchable, Dispatchable, Dispatchable, Dispatchable, Dispatchable, Dispatchable, ], ): [R1, R2, R3, R4, R5, R6, R7]; ( tasks: readonly [ Dispatchable, Dispatchable, Dispatchable, Dispatchable, Dispatchable, Dispatchable, Dispatchable, Dispatchable, ], ): [R1, R2, R3, R4, R5, R6, R7, R8]; ( tasks: readonly [ Dispatchable, Dispatchable, Dispatchable, Dispatchable, Dispatchable, Dispatchable, Dispatchable, Dispatchable, Dispatchable, ], ): [R1, R2, R3, R4, R5, R6, R7, R8, R9]; (tasks: readonly Dispatchable[]): R[]; } export interface Dispatch extends AmosDispatch {} /** * selectable things * * @stable */ export type Selectable = Box | Selector; /** * select * * @stable */ export interface Select extends AmosObject<'store.select'> { (selectable: Selectable, snapshot?: Snapshot): R; } /** * Store * * @stable */ export interface Store { /** * get the state snapshot of the store. * * Please note that any mutation of the snapshot is silent. */ snapshot: () => Snapshot; /** * dispatch one or more dispatchable things. */ dispatch: Dispatch; /** * subscribe the mutations * @param fn */ subscribe: (fn: (updatedState: Snapshot) => void) => () => void; /** * select a selectable thing */ select: Select; } export type StoreEnhancer = (store: Store) => Store; /** * create a store * @param preloadedState * @param enhancers * * @stable */ export function createStore(preloadedState?: Snapshot, ...enhancers: StoreEnhancer[]): Store { const state: Snapshot = {}; const boxes: Box[] = []; const listeners: Array<(updatedSnapshot: Snapshot) => void> = []; const ensure = (box: Box) => { if (state.hasOwnProperty(box.key)) { return; } let boxState = box.initialState; if (preloadedState?.hasOwnProperty(box.key)) { boxState = box.preload(preloadedState[box.key], boxState); } state[box.key] = boxState; boxes.push(box); }; let dispatchDepth = 0; let dispatchingSnapshot: Snapshot = {}; const record = (key: string, newState: unknown) => { if (newState !== state[key] || dispatchingSnapshot.hasOwnProperty(key)) { dispatchingSnapshot[key] = newState; state[key] = newState; } }; const exec = (dispatchable: Dispatchable) => { switch (dispatchable.object) { case 'action': return dispatchable.actor(store.dispatch, store.select, ...dispatchable.args); case 'mutation': ensure(dispatchable.box); record( dispatchable.box.key, dispatchable.mutator(state[dispatchable.box.key], ...dispatchable.args), ); return dispatchable.result; case 'signal': for (const box of boxes) { const fn = box.listeners[dispatchable.type]; fn && record(box.key, fn(state[box.key], dispatchable.data)); } return dispatchable.data; } }; let selectingSnapshot: Snapshot | undefined; let store: Store = { snapshot: () => state, subscribe: (fn) => { listeners.push(fn); return () => { const index = listeners.indexOf(fn); if (index > -1) { listeners.splice(index, 1); } }; }, dispatch: defineAmosObject( 'store.dispatch', (tasks: Dispatchable | readonly Dispatchable[]) => { if (++dispatchDepth === 1) { dispatchingSnapshot = {}; } try { if (isArray(tasks)) { return tasks.map(exec); } else { return exec(tasks); } } finally { if (--dispatchDepth === 0) { if (Object.keys(dispatchingSnapshot).length > 0) { const resultSnapshot = { ...dispatchingSnapshot }; [...listeners].forEach((fn) => fn(resultSnapshot)); } } } }, ), select: defineAmosObject('store.select', (selectable: Selectable, snapshot?: Snapshot): any => { if (typeof selectable === 'function') { if (snapshot) { if (selectingSnapshot) { throw new Error(`[Amos] recursive snapshot collection is not supported.`); } selectingSnapshot = snapshot; try { return selectable(store.select); } finally { selectingSnapshot = void 0; } } else { return selectable(store.select); } } else { ensure(selectable); if (selectingSnapshot) { selectingSnapshot[selectable.key] = state[selectable.key]; } return state[selectable.key]; } }), }; store = enhancers.reduce((previousValue, currentValue) => currentValue(previousValue), store); if (typeof process === 'object' && process.env.NODE_ENV === 'development') { Object.freeze(store); } return store; }