synapse-storage
Version:
Набор инструментов для управления состоянием и апи-запросами
165 lines (156 loc) • 7.68 kB
JavaScript
import { EndpointClass } from "./components/endpoint.js";
import { QueryStorage } from "./components/query-storage.js";
import { apiLogger } from "./utils/api-helpers.js";
class ApiClient {
/** Хранилище запросов */ queryStorage;
/** Флаг завершённой инициализации */ _initialized = false;
/** Промис текущей инициализации (для дедупликации параллельных вызовов) */ _initPromise = null;
/** Снапшот для гидрации, отложенный до завершения init (если hydrate вызван раньше) */ _pendingHydration = null;
cacheableHeaderKeys;
globalCacheConfig;
baseQueryConfig;
storageExternal;
globalRetryConfig;
createEndpoints;
/** Реестр эндпоинтов */ endpoints = {};
constructor(options){
// Сохраняем переданные параметры
this.cacheableHeaderKeys = options.cacheableHeaderKeys;
this.globalCacheConfig = options.cache;
this.baseQueryConfig = options.baseQuery;
this.storageExternal = options.storage;
this.globalRetryConfig = options.retry;
this.createEndpoints = options.endpoints;
}
async init() {
if (this._initialized) return this;
if (this._initPromise) return this._initPromise;
this._initPromise = this._doInit();
return this._initPromise;
}
async _doInit() {
try {
// 1. Создаем кэшированное хранилище запросов (storage инициализируется внутри QueryStorage)
this.queryStorage = await new QueryStorage(this.storageExternal, this.globalCacheConfig).initialize();
// 2. Применяем отложенную гидрацию (если hydrate был вызван до init)
if (this._pendingHydration) {
await this.queryStorage.hydrate(this._pendingHydration);
this._pendingHydration = null;
}
// 3. Создаем эндпоинты
await this.initializeEndpoints();
this._initialized = true;
return this;
} catch (error) {
this._initPromise = null;
throw error;
}
}
async initializeEndpoints() {
// Получаем конфигурацию будущих эндпоинтов
const create = (config)=>config;
// Создаем объект с конфигурациями для эндпоинтов
const endpointsConfig = await this.createEndpoints(create);
// Создаем эндпоинты
for (const [endpointKey, endpointConfig] of Object.entries(endpointsConfig)){
const key = endpointKey;
this.endpoints[key] = new EndpointClass({
name: endpointKey,
queryStorage: this.queryStorage,
config: endpointConfig,
cacheableHeaderKeys: this.cacheableHeaderKeys,
globalCacheConfig: this.globalCacheConfig,
globalRetryConfig: this.globalRetryConfig,
baseQueryConfig: this.baseQueryConfig
});
}
}
ensureInitialized() {
if (!this._initialized) {
throw new Error('ApiClient не инициализирован. Вызовите await api.init() перед использованием.');
}
}
/**
* Получает все эндпоинты с улучшенной типизацией
* @returns Типизированный объект эндпоинтов
*/ getEndpoints() {
this.ensureInitialized();
return this.endpoints;
}
/**
* Выполняет запрос к API с типизацией и обработкой ошибок
* @param endpointName Имя эндпоинта (с подсказками TypeScript)
* @param params Параметры запроса (с типизацией)
* @param options Опции запроса
* @returns Promise с типизированным результатом запроса
*/ async request(endpointName, params, options) {
this.ensureInitialized();
const endpoints = this.getEndpoints();
const endpoint = endpoints[endpointName];
if (!endpoint) {
throw new Error(`Эндпоинт ${String(endpointName)} не найден`);
}
try {
const stateRequest = endpoint.request(params, options);
return await stateRequest.wait();
} catch (error) {
apiLogger.error(`Ошибка запроса к ${String(endpointName)}`, {
error,
params
});
throw error;
}
}
/**
* Снимок кэша для дегидрации (SSR: сервер → HTML → клиент).
*
* Возвращает всё состояние кэш-хранилища (записи API с абсолютными `expiresAt`,
* поэтому TTL переживает перенос на клиент). Симметрично `dehydrateModule` для
* synapse-модулей. Типичный серверный рецепт: `await api.request(...)` для
* прогрева → `await api.dehydrate()` → сериализовать в HTML.
*/ async dehydrate() {
this.ensureInitialized();
const storage = this.queryStorage.getStorage();
if (!storage) return {};
// getState у sync-хранилищ возвращает значение, у async — Promise; await покрывает оба
return await storage.getState();
}
/**
* Гидрация кэша снапшотом с сервера.
*
* - Вызванная ДО `init()` — снапшот запоминается и применяется сразу после
* создания хранилища (init не перезатрёт серверное состояние).
* - Вызванная ПОСЛЕ `init()` — немедленно заменяет состояние кэша и
* перестраивает индекс тегов.
*
* После гидрации первый `request()`/`getCachedSync()` с теми же параметрами
* попадёт в кэш — на клиенте не будет повторного сетевого запроса.
*/ async hydrate(state) {
if (this._initialized) {
await this.queryStorage.hydrate(state);
} else {
this._pendingHydration = state;
}
return this;
}
/**
* Доступ к экземпляру хранилища кэша (для ручных сценариев SSR/отладки).
*/ getStorage() {
this.ensureInitialized();
return this.queryStorage.getStorage();
}
async destroy() {
if (!this._initialized) return;
// 1. Уничтожаем каждый эндпоинт
Object.values(this.endpoints).forEach((endpoint)=>endpoint.destroy());
// 2. Очищаем коллекцию эндпоинтов
this.endpoints = {};
// 3. Уничтожаем хранилище
await this.queryStorage.destroy();
// 4. Сбрасываем состояние инициализации (позволяет повторный init)
this._initialized = false;
this._initPromise = null;
}
}
export { ApiClient };
//# sourceMappingURL=api.module.js.map