synapse-storage
Version:
Набор инструментов для управления состоянием и апи-запросами
195 lines (188 loc) • 8.36 kB
JavaScript
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