import type { WorkflowEngine } from './engine' import type { NativeCacheConfig } from './types' /** The debug-friendly shape exposed per engine instance on the window object. */ export interface EngineDebugView { /** Zustand data store — read slice state directly. */ store: WorkflowEngine['store'] /** Performance stats store — counters per registered key. */ statsStore: WorkflowEngine['statsStore'] /** Trigger a fetch from the console. */ trigger: WorkflowEngine['trigger'] /** Manually patch a slice and sync to Redux. */ updateSlice: WorkflowEngine['updateSlice'] } /** Snapshot of all registered engines, keyed by instance name. */ export type EngineSnapshot = Record const REGISTRY_GLOBAL_KEY = '__zustandEngineRegistry__' /** * Global singleton registry. Every WorkflowEngine auto-registers * itself here at construction time — no setup needed. * * The instance is stored on `globalThis` so it survives multiple module * evaluations — a common occurrence with Metro bundle splits, webpack chunk * duplication, or HMR in development. Only one registry ever exists per JS * runtime, regardless of how many times this module is evaluated. * * Engine names must be unique. Registering two engines with the same name * throws immediately so duplicate wiring is caught at startup, not silently. * * @example * // Expose all engines to the browser/RN console (call once in dev entry): * engineRegistry.attachToWindow('_daas_engine') * // → window._daas_engine.snapshot()['cart'].store.getState().slices * // → window._daas_engine.snapshot()['cart'].statsStore.getState() * // → window._daas_engine.snapshot()['cart'].trigger('products') */ class EngineRegistry { readonly instances = new Map() private _defaultNativeCache: NativeCacheConfig | undefined private _defaultNativeCacheFilter: | ((engineName: string) => boolean) | undefined /** * Set a global default native cache applied to every engine that does not * supply its own `nativeCache` in its constructor config. * * Call this once at app startup, before creating any engine instances. * * @param config - The `retrieve` / `upsert` functions shared across engines. * @param filter - Optional predicate. Receives the engine `name`; return * `false` to opt that engine out of the global cache. * * @example * engineRegistry.setDefaultNativeCache(adapter.nativeCache) * // All engines inherit the cache unless they set nativeCache: false * * engineRegistry.setDefaultNativeCache( * adapter.nativeCache, * (name) => name !== 'analytics', * ) * // The 'analytics' engine is excluded; all others inherit. */ setDefaultNativeCache( config: NativeCacheConfig, filter?: (engineName: string) => boolean, ): void { this._defaultNativeCache = config this._defaultNativeCacheFilter = filter } /** * @internal — called by WorkflowEngine constructor to resolve the effective * native cache when no engine-level config was explicitly provided. */ _resolveDefaultNativeCache( engineName: string, ): NativeCacheConfig | undefined { if (!this._defaultNativeCache) return undefined if ( this._defaultNativeCacheFilter && !this._defaultNativeCacheFilter(engineName) ) return undefined return this._defaultNativeCache } /** @internal — called by WorkflowEngine constructor. */ _register(engine: WorkflowEngine): void { if (this.instances.has(engine.name)) { throw new Error( `[engineRegistry] An engine named "${engine.name}" is already registered. ` + 'Engine names must be unique. Use a different name or call engineRegistry.clear() first.', ) } this.instances.set(engine.name, engine) } get(name: string): WorkflowEngine | undefined { return this.instances.get(name) } /** * Returns a plain object (keyed by instance name) of debug-friendly views * for every registered engine. Suitable for direct console inspection. */ snapshot(): EngineSnapshot { const out: EngineSnapshot = {} for (const [key, engine] of this.instances) { out[key] = { store: engine.store, statsStore: engine.statsStore, trigger: engine.trigger.bind(engine), updateSlice: engine.updateSlice.bind(engine), } } return out } /** * Remove all registered engine instances and reset the default native cache. * Primarily for test teardown — call in afterEach to allow the same name to * be reused across tests with a clean registry state. */ clear(): void { this.instances.clear() this._defaultNativeCache = undefined this._defaultNativeCacheFilter = undefined } /** * Assigns a live reference to this registry onto the global `window` object * under the given key. After this call, `window[key]` exposes every engine * instance (including ones created after this call) via `window[key].snapshot()`, * and individual engines via `window[key].instances.get('name')`. * * @param key - The window property name. Defaults to `'__zustandEngine'`. * * @example * engineRegistry.attachToWindow('_daas_engine') * // window._daas_engine.snapshot() — live snapshot * // window._daas_engine.instances — Map of all engines */ attachToWindow(key = '__zustandEngine'): void { if (typeof window === 'undefined') return // biome-ignore lint/suspicious/noExplicitAny: intentional window assignment for debugging ;(window as any)[key] = this } } // biome-ignore lint/suspicious/noExplicitAny: globalThis key for cross-evaluation singleton const _g = globalThis as any if (!_g[REGISTRY_GLOBAL_KEY]) _g[REGISTRY_GLOBAL_KEY] = new EngineRegistry() export const engineRegistry: EngineRegistry = _g[REGISTRY_GLOBAL_KEY]