UNPKG

synapse-storage

Version:

Набор инструментов для управления состоянием и апи-запросами

195 lines (188 loc) 8.36 kB
import { Effects, EffectsModule, FINALIZE, toObservable } from "../../reactive/index.js"; import { waitForDependencies } from "./waitForDependencies.js"; /** Раскладывает effects-вход в плоский список module-эффектов + инстансы (для onDestroy). */ function collectEffects(input) { const items = input === undefined ? [] : Array.isArray(input) ? input : [ input ]; const moduleEffects = []; const instances = []; for (const item of items){ if (item instanceof Effects) { instances.push(item); moduleEffects.push(...item.getEffects()); } else if (typeof item === 'function') { moduleEffects.push(item); } else { throw new Error('createSynapse: каждый элемент "effects" должен быть инстансом Effects или функцией-эффектом.'); } } return { moduleEffects, instances }; } /** LIFO-teardown. */ async function teardown(cleanup) { for(let i = cleanup.length - 1; i >= 0; i--)await cleanup[i](); } // Синхронная конструкция ядра: storage → READY (initializeSync), dispatcher финализирован, // селекторы материализованы, state$ всегда. Эффекты НЕ стартуют. Требует sync-хранилища. function constructSyncCore(config) { const storage = config.storage(); if (storage.isSync !== true || typeof storage.initializeSync !== 'function') { throw new Error('createSynapse: "storage" должен быть синхронным (напр. MemoryStorage) — только он умеет синхронную конструкцию.'); } const cleanup = []; storage.initializeSync(); cleanup.push(()=>storage.destroy()); const selectors = config.selectors?.(storage); if (selectors) cleanup.push(()=>selectors.destroy()); const dispatcher = config.dispatcher?.(storage); if (dispatcher) { dispatcher[FINALIZE](); cleanup.push(()=>dispatcher.destroy()); } const state$ = toObservable(storage); let destroyed = false; const synapse = { storage, state$, dispatcher: dispatcher, actions: dispatcher, selectors: selectors, destroy: async ()=>{ if (destroyed) return; destroyed = true; await teardown(cleanup); } }; // Синхронный post-construct хук (storage READY, dispatcher финализирован) — до первого рендера. if (config.postConstruct) { try { config.postConstruct(synapse); } catch (error) { // Fail-fast: откатываем уже созданное ядро и пробрасываем. void teardown(cleanup).catch(()=>{}); throw error; } } return { synapse, cleanup }; } /** * Ленивый handle C-формы: конструкция main синхронна и мемоизирована (строится при первом обращении * к геттеру/`ready()`), эффекты стартуют отдельно в `ready()` после `waitForDependencies`. Расцеп даёт * синхронный `handle.selectors`/`.storage`/`.state$` — основа cross-store DI. */ function createSyncSynapseModule(config) { let main; let mainCleanup = []; let effectsCleanup = []; let readyPromise; // Синхронно строит (или возвращает) main-ядро. НЕ стартует эффекты. const ensureMain = ()=>{ if (!main) { const built = constructSyncCore(config); main = built.synapse; mainCleanup = built.cleanup; } return main; }; // Стартует эффекты на уже построенном main: ждёт deps, конструирует эффекты из ctx, запускает. const startEffects = async (core)=>{ await waitForDependencies(config.dependencies, config.dependencyTimeout); const deps = await Promise.all((config.dependencies ?? []).map((d)=>Promise.resolve(d))); const ctx = { storage: core.storage, dispatcher: core.dispatcher, selectors: core.selectors, deps }; // Фабрика эффектов может быть async (ленивый резолв endpoints) — ждём её здесь. const input = await config.effects?.(ctx); const { moduleEffects, instances } = collectEffects(input); // Внешние диспетчеры резолвим лениво здесь же (deps уже готовы → чужой dispatcher финализирован). const external = typeof config.externalDispatchers === 'function' ? config.externalDispatchers(ctx) : config.externalDispatchers ?? {}; for (const instance of instances){ if (instance.onDestroy) effectsCleanup.push(()=>instance.onDestroy()); } if (moduleEffects.length > 0) { if (!core.dispatcher) throw new Error('createSynapse: "effects" требуют "dispatcher".'); const effectsModule = new EffectsModule(core.storage, core.dispatcher, external); effectsModule.addEffects(moduleEffects); effectsCleanup.push(()=>{ effectsModule.stop(); }); await effectsModule.start(); } return core; }; const handle = { ready (options) { const core = ensureMain(); // Серверный прогрев/дегидрация: только конструкция, без эффектов. if (options?.withEffects === false) return Promise.resolve(core); if (!readyPromise) { readyPromise = startEffects(core).catch((error)=>{ // Fail-fast: сбрасываем мемо старта, чтобы повтор мог попробовать снова. readyPromise = undefined; throw error; }); } return readyPromise; }, isReady () { return main !== undefined; }, getSnapshot () { return main; }, fork () { return createSyncSynapseModule(config); }, async destroy () { const orderedCleanup = [ ...mainCleanup, ...effectsCleanup ]; main = undefined; mainCleanup = []; effectsCleanup = []; readyPromise = undefined; // Сначала эффекты (позже сконструированы), потом ядро — LIFO по общему порядку. await teardown(orderedCleanup).catch(()=>{}); }, then (onFulfilled, onRejected) { return handle.ready().then(onFulfilled, onRejected); } }; // Синхронные геттеры main-ядра: дают cross-store DI синхронный доступ к чужому стору // (`otherModule.selectors` в конструкторе селекторов). Строят main лениво при первом доступе. Object.defineProperties(handle, { storage: { get: ()=>ensureMain().storage, enumerable: true }, state$: { get: ()=>ensureMain().state$, enumerable: true }, dispatcher: { get: ()=>ensureMain().dispatcher, enumerable: true }, actions: { get: ()=>ensureMain().dispatcher, enumerable: true }, selectors: { get: ()=>ensureMain().selectors, enumerable: true } }); // Свежее throwaway-ядро (не main): per-request изоляция на сервере + первый кадр гидрации. handle.buildSyncShell = ()=>constructSyncCore(config).synapse; return handle; } export { createSyncSynapseModule }; //# sourceMappingURL=syncModule.js.map