import * as plugins from './smartstate.plugins.js'; import { StatePart } from './smartstate.classes.statepart.js'; import { computed } from './smartstate.classes.computed.js'; export type TInitMode = 'soft' | 'mandatory' | 'force' | 'persistent'; /** * Smartstate takes care of providing state */ export class Smartstate { public statePartMap: { [key in StatePartNameType]?: StatePart } = {}; private pendingStatePartCreation: Map>> = new Map(); // Batch support private batchDepth = 0; private isFlushing = false; private pendingNotifications = new Set>(); constructor() {} /** * whether state changes are currently being batched */ public get isBatching(): boolean { return this.batchDepth > 0; } /** * registers a state part for deferred notification during a batch */ public registerPendingNotification(statePart: StatePart): void { this.pendingNotifications.add(statePart); } /** * batches multiple state updates so subscribers are only notified once all updates complete */ public async batch(updateFn: () => Promise | void): Promise { this.batchDepth++; try { await updateFn(); } finally { this.batchDepth--; if (this.batchDepth === 0 && !this.isFlushing) { this.isFlushing = true; try { while (this.pendingNotifications.size > 0) { const pending = [...this.pendingNotifications]; this.pendingNotifications.clear(); for (const sp of pending) { await sp.notifyChange(); } } } finally { this.isFlushing = false; } } } } /** * creates a computed observable derived from multiple state parts */ public computed( sources: StatePart[], computeFn: (...states: any[]) => TResult, ): plugins.smartrx.rxjs.Observable { return computed(sources, computeFn); } /** * Allows getting and initializing a new statepart */ public async getStatePart( statePartNameArg: StatePartNameType, initialArg?: PayloadType, initMode: TInitMode = 'soft' ): Promise> { // Return pending creation if one exists to prevent duplicate state parts const pending = this.pendingStatePartCreation.get(statePartNameArg); if (pending) { return pending as Promise>; } const existingStatePart = this.statePartMap[statePartNameArg]; if (existingStatePart) { switch (initMode) { case 'mandatory': throw new Error( `State part '${statePartNameArg}' already exists, but initMode is 'mandatory'` ); case 'force': existingStatePart.dispose(); break; case 'soft': case 'persistent': default: return existingStatePart as StatePart; } } else { if (initialArg === undefined) { throw new Error( `State part '${statePartNameArg}' does not exist and no initial state provided` ); } } const creationPromise = this.createStatePart(statePartNameArg, initialArg, initMode); this.pendingStatePartCreation.set(statePartNameArg, creationPromise); try { const result = await creationPromise; return result; } finally { this.pendingStatePartCreation.delete(statePartNameArg); } } /** * Creates a statepart */ private async createStatePart( statePartName: StatePartNameType, initialPayloadArg: PayloadType, initMode: TInitMode = 'soft' ): Promise> { const newState = new StatePart( statePartName, initMode === 'persistent' ? { dbName: 'smartstate', storeName: statePartName, } : null ); newState.smartstateRef = this; await newState.init(); const currentState = newState.getState(); if (initMode === 'persistent' && currentState !== undefined) { await newState.setState({ ...initialPayloadArg, ...currentState, }); } else { await newState.setState(initialPayloadArg); } this.statePartMap[statePartName] = newState; return newState; } }