UNPKG

synapse-storage

Version:

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

145 lines (132 loc) 6.97 kB
import { Fragment, jsx } from "react/jsx-runtime"; import { createContext, forwardRef, useContext, useEffect, useRef, useState } from "react"; import { handleCleanupError } from "../../_utils/error-handling.util.js"; import { StorageStatus } from "../../core/index.js"; import { dehydrateModule } from "../../utils/index.js"; const ERROR_HOOK_MESSAGE = 'Хук необходимо использовать внутри компонента contextSynapse'; const ERROR_CONTEXT_INIT = 'Ошибка при инициализации контекста:'; function createSynapseCtx(synapseModule, options) { const { loadingComponent = /*#__PURE__*/ jsx("div", { children: "Инициализация контекста..." }) } = options || {}; const SynapseContext = /*#__PURE__*/ createContext(null); // Клиент: общий main-синглтон. Sync-handle строит его лениво при первом обращении к геттеру — // трогаем `.storage`, затем берём готовый снапшот. Стор один на приложение, роуты пере-сеют его. const getClientStore = ()=>{ void synapseModule.storage; return synapseModule.getSnapshot(); }; const useSynapseStorage = ()=>{ const context = useContext(SynapseContext); if (!context) throw new Error(`useSynapseStorage: ${ERROR_HOOK_MESSAGE}`); return context.storage; }; const useSynapseSelectors = ()=>{ const context = useContext(SynapseContext); if (!context) throw new Error(`useSynapseSelectors: ${ERROR_HOOK_MESSAGE}`); return context.selectors; }; const useSynapseActions = ()=>{ const context = useContext(SynapseContext); if (!context) throw new Error(`useSynapseActions: ${ERROR_HOOK_MESSAGE}`); return context.actions; }; const useSynapseState$ = ()=>{ const context = useContext(SynapseContext); if (!context) throw new Error(`useSynapseState$: ${ERROR_HOOK_MESSAGE}`); return context.state$; }; // Снимает серверный снапшот состояния для пропа dehydratedState (классический SSR: renderToString). const dehydrate = (opts)=>dehydrateModule(synapseModule, { state: opts?.initialState }); /** Декоратор для обёртки компонентов в контекст Synapse. */ function contextSynapse(Component) { const WrappedComponent = /*#__PURE__*/ forwardRef(function WrappedComponent(props, ref) { const { dehydratedState, ...restProps } = props; // Засев эмитит подписчикам; повторный засев того же стора этим же инстансом выстрелил бы эмиссию // во время рендера. Набор ПЕР-ИНСТАНСНЫЙ (не общий): на сервере каждый рендер обязан пере-сеять // свой стор своим снапшотом (изоляция запросов), общий набор ошибочно бы это пропустил. const seededRef = useRef(null); const seeded = seededRef.current ??= new WeakSet(); // Синхронный засев снапшота до первого рендера — одинаковый HTML на сервере и клиенте. Идемпотентно. const seedHydration = (store)=>{ if (store && dehydratedState !== undefined && store.storage.initStatus.status === StorageStatus.READY && !seeded.has(store)) { seeded.add(store); store.storage.hydrate(dehydratedState); } }; const [store, setStore] = useState(()=>{ // Сервер: свежий throwaway-стор на каждый рендер — изоляция запроса by construction (main не трогаем). if (typeof window === 'undefined') { const shell = synapseModule.buildSyncShell?.() ?? undefined; seedHydration(shell); return shell; } // Клиент: общий main синхронно, сеем снапшотом. const main = getClientStore(); seedHydration(main); return main; }); const [error, setError] = useState(null); useEffect(()=>{ // Эффекты стартуют только на клиенте, на общем main; идентичность стора при этом НЕ меняется. let cancelled = false; if (!store) setStore(getClientStore()); // редкий случай: main не был готов на первом кадре synapseModule.ready().catch((err)=>{ if (!cancelled) setError(err instanceof Error ? err : new Error(String(err))); }); return ()=>{ cancelled = true; }; }, []); if (error) return /*#__PURE__*/ jsx("div", { children: `${ERROR_CONTEXT_INIT} ${error.message}` }); if (!store) return /*#__PURE__*/ jsx(Fragment, { children: loadingComponent }); return /*#__PURE__*/ jsx(SynapseContext.Provider, { value: store, children: /*#__PURE__*/ jsx(Component, { ...restProps, ref: ref }) }); }); const componentName = Component.displayName || Component.name || 'Component'; WrappedComponent.displayName = `SynapseContext(${componentName})`; // Копируем статические свойства оригинального компонента const excludedKeys = new Set([ '$$typeof', 'render', 'defaultProps', 'displayName', 'propTypes' ]); Object.keys(Component).forEach((key)=>{ if (!excludedKeys.has(key)) { ; WrappedComponent[key] = Component[key]; } }); return WrappedComponent; } const cleanupSynapse = async ()=>{ try { await synapseModule.destroy(); } catch (error) { handleCleanupError('createSynapseCtx: error during Synapse cleanup', error); } }; return { contextSynapse, dehydrate, useSynapseStorage, useSynapseSelectors, useSynapseActions, useSynapseState$, cleanupSynapse }; } export { createSynapseCtx }; //# sourceMappingURL=createSynapseCtx.js.map