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