synapse-storage
Version:
Набор инструментов для управления состоянием и апи-запросами
145 lines (138 loc) • 8.19 kB
JavaScript
import { handleCallbackError } from "../_utils/error-handling.util.js";
import { StorageStatus } from "../core/index.js";
/** Похож ли вход на thenable (handle/Promise), а не на уже готовый synapse. */ const isThenable = (value)=>typeof value?.then === 'function';
/**
* Пытается достать ГОТОВЫЙ стор прямо сейчас, без ожидания. Два случая:
* - вход — handle: берём собранный стор через getSnapshot() (после dehydrate он READY);
* - вход — уже готовый стор напрямую (не Promise) с READY-хранилищем.
* Не вышло → undefined → пойдём асинхронным путём. Это и есть SSR sync-fast-path:
* так серверный рендер получает main синхронно, без спиннера.
*/ const resolveSyncReady = (input)=>{
// Вход — handle: берём уже собранный стор (сервер после dehydrate / повторный mount на клиенте).
const snapshot = input?.getSnapshot?.();
if (snapshot && snapshot.storage.initStatus.status === StorageStatus.READY) return snapshot;
// Вход — готовый стор передали напрямую (не Promise), с READY-хранилищем.
if (!isThenable(input) && input.storage?.initStatus?.status === StorageStatus.READY) {
return input;
}
return undefined;
};
/**
* Создает фреймворк-независимую утилиту для ожидания готовности Synapse.
* Принимает `SynapseModule`-handle (PromiseLike), Promise готового synapse либо сам
* готовый synapse. Работает в любом JS окружении: Node.js, браузер, React Native.
*/ function createSynapseAwaiter(synapseStorePromise) {
// Состояние автомата: статус + сам стор + ошибка. Меняются либо синхронно (fast-path ниже),
// либо асинхронно (storeInitPromise), либо гасятся в destroy().
let status = 'pending';
let store;
let error = null;
let destroyed = false;
// Подписчики на готовность/ошибку (их наполняет onReady/onError, дёргает async-путь).
const readyCallbacks = new Set();
const errorCallbacks = new Set();
// Sync-fast-path: если стор готов уже сейчас — выставляем 'ready' ДО возврата, чтобы
// getStoreIfReady() отдал его на первом же синхронном рендере (сервер / клиентская гидрация).
const syncReady = resolveSyncReady(synapseStorePromise);
if (syncReady) {
store = syncReady;
status = 'ready';
}
// Async-путь: ждём, пока стор соберётся и хранилище станет READY, затем 'ready' + зовём onReady
// (или onError). Нужен, когда синхронно готового не было (async-стор на клиенте): компонент
// подписался в useEffect и перерисуется, когда стор достроится.
const storeInitPromise = (async ()=>{
try {
const resolvedStore = await Promise.resolve(synapseStorePromise);
// Ждём готовности хранилища (storage.initialize() может быть асинхронным).
await resolvedStore.storage.waitForReady();
// awaiter уничтожили пока ждали — состояние не трогаем.
if (destroyed) return resolvedStore;
store = resolvedStore;
status = 'ready';
// Уведомляем всех подписчиков о готовности
readyCallbacks.forEach((callback)=>{
try {
callback(store);
} catch (err) {
handleCallbackError('SynapseAwaiter: error in ready callback', err);
}
});
return resolvedStore;
} catch (err) {
const errorObj = err instanceof Error ? err : new Error(String(err));
// Если awaiter был уничтожен во время инициализации — не обновляем состояние
if (destroyed) throw errorObj;
error = errorObj;
status = 'error';
// Уведомляем всех подписчиков об ошибке
errorCallbacks.forEach((callback)=>{
try {
callback(errorObj);
} catch (callbackErr) {
handleCallbackError('SynapseAwaiter: error in error callback', callbackErr);
}
});
throw errorObj;
}
})();
// Защита от unhandled rejection: ошибка сборки доставляется через onError/getError (channel
// подписок), но сам storeInitPromise при этом отклоняется. Если его никто не await'ит (типичный
// случай: createSynapseCtx работает через onReady/onError, не через waitForReady), отклонение
// всплыло бы как unhandledRejection (шум на сервере, падение strict-процессов). Гасим отдельным
// .catch — waitForReady() по-прежнему отдаёт тот же промис, и его потребитель получит throw.
storeInitPromise.catch(()=>{});
// Публичный API: синхронные геттеры + подписки. Из createSynapseCtx зовут getStoreIfReady()
// (в useState/useEffect) и onReady/onError (подписка в useEffect на async-готовность).
return {
waitForReady: ()=>storeInitPromise,
isReady: ()=>status === 'ready',
getStoreIfReady: ()=>store,
onReady: (callback)=>{
// Если уже готов, вызываем callback немедленно
if (status === 'ready' && store) {
try {
callback(store);
} catch (err) {
handleCallbackError('SynapseAwaiter: error in immediate ready callback', err);
}
} else {
// Иначе добавляем в список ожидания
readyCallbacks.add(callback);
}
return ()=>{
readyCallbacks.delete(callback);
};
},
onError: (callback)=>{
// Если уже есть ошибка, вызываем callback немедленно
if (status === 'error' && error) {
try {
callback(error);
} catch (err) {
handleCallbackError('SynapseAwaiter: error in immediate error callback', err);
}
} else {
// Иначе добавляем в список ожидания
errorCallbacks.add(callback);
}
return ()=>{
errorCallbacks.delete(callback);
};
},
getStatus: ()=>status,
getError: ()=>error,
// Сброс: отписываем всех и обнуляем состояние. Флаг destroyed не даёт async-пути записать
// готовность после уничтожения (напр. размонтировали клиентский awaiter в cleanupSynapse).
destroy: ()=>{
destroyed = true;
readyCallbacks.clear();
errorCallbacks.clear();
store = undefined;
error = null;
status = 'pending';
}
};
}
export { createSynapseAwaiter };
//# sourceMappingURL=createSynapseAwaiter.js.map