UNPKG

synapse-storage

Version:

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

260 lines (253 loc) 10.8 kB
import { SingletonKeyGenerator, SingletonManager } from "../modules/singleton/singleton.util.js"; import { StorageEvents, StorageStatus } from "../storage.interface.js"; /** * Общая константа для глобальной подписки. * Используется в StorageCore и наследниках. */ const GLOBAL_SUBSCRIPTION_KEY = '*'; /** * Абстрактный базовый класс с общей инфраструктурой хранилища. * * Содержит: status management, subscriptions, lifecycle wrappers, * state cache, event emission, singleton cleanup. * * Не содержит: middleware, plugin executor, do* методы, публичные CRUD-операции. */ class StorageCore { coreConfig; eventEmitter; logger; name; /** * Синхронное ли хранилище — задаётся в SyncBaseStorage/AsyncBaseStorage. * Опциональный (не abstract): внешние кастомные адаптеры, наследующие StorageCore * и скомпилированные против прежней minor-версии, не обязаны его объявлять (R4). */ isSync; _initStatus = { status: StorageStatus.IDLE }; _isDestroyed = false; statusSubscribers = new Set(); subscribers = new Map(); _stateCache = {}; constructor(coreConfig, eventEmitter, logger){ this.coreConfig = coreConfig; this.eventEmitter = eventEmitter; this.logger = logger; this.name = coreConfig.name; } // ─── Status Management ────────────────────────────────────────────────────── get initStatus() { return { ...this._initStatus }; } async waitForReady() { if (this._initStatus.status === StorageStatus.READY) return this; if (this._initStatus.status === StorageStatus.ERROR) { throw this._initStatus.error || new Error('Storage initialization failed'); } return new Promise((resolve, reject)=>{ const unsubscribe = this.onStatusChange((status)=>{ if (status.status === StorageStatus.READY) { unsubscribe(); resolve(this); } else if (status.status === StorageStatus.ERROR) { unsubscribe(); reject(status.error || new Error('Storage initialization failed')); } }); }); } onStatusChange(callback) { this.statusSubscribers.add(callback); callback(this.initStatus); return ()=>{ this.statusSubscribers.delete(callback); }; } updateInitStatus(update) { const previousStatus = this._initStatus.status; this._initStatus = { ...this._initStatus, ...update }; if (previousStatus !== this._initStatus.status) { this.logger?.debug(`Storage "${this.name}" status changed: ${previousStatus} -> ${this._initStatus.status}`); } const statusCopy = this.initStatus; this.statusSubscribers.forEach((callback)=>{ try { callback(statusCopy); } catch (error) { this.logger?.error('Error in status change callback', { error }); } }); } ensureReady() { if (this._isDestroyed) { throw new Error(`Storage "${this.name}" has been destroyed`); } if (this._initStatus.status !== StorageStatus.READY) { throw new Error(`Storage "${this.name}" is not ready. Current status: ${this._initStatus.status}`); } } // ─── State Cache ──────────────────────────────────────────────────────────── getStateSync() { return this._stateCache; } // ─── Subscriptions ────────────────────────────────────────────────────────── subscribeToAll(callback) { if (!this.subscribers.has(GLOBAL_SUBSCRIPTION_KEY)) { this.subscribers.set(GLOBAL_SUBSCRIPTION_KEY, new Set()); } this.subscribers.get(GLOBAL_SUBSCRIPTION_KEY).add(callback); return ()=>{ const subscribers = this.subscribers.get(GLOBAL_SUBSCRIPTION_KEY); if (subscribers) { subscribers.delete(callback); if (subscribers.size === 0) { this.subscribers.delete(GLOBAL_SUBSCRIPTION_KEY); } } }; } subscribe(keyOrSelector, callback) { if (typeof keyOrSelector === 'string') { return this.subscribeByKey(keyOrSelector, callback); } return this.subscribeBySelector(keyOrSelector, callback); } /** * Хук изменения ключа. По умолчанию no-op. * Async-хранилища переопределяют для защиты от race condition в subscribeByKey * (см. AsyncBaseStorage). Sync версии не нужен — get() синхронный. */ trackKeyVersion(_keyStr) {} notifySubscribers(key, value) { const keyStr = key.toString(); this.trackKeyVersion(keyStr); const exactSubscribers = this.subscribers.get(keyStr); if (exactSubscribers?.size) { const subscribersCopy = new Set(exactSubscribers); subscribersCopy.forEach((callback)=>{ try { callback(value); } catch (error) { this.logger?.error('Ошибка в подписчике на колбэк', { key: keyStr, error }); } }); } } // ─── Lifecycle ────────────────────────────────────────────────────────────── async initialize() { if (this._initStatus.status === StorageStatus.READY) return this; if (this._initStatus.status === StorageStatus.LOADING) return this.waitForReady(); // Allow re-initialization after destroy this._isDestroyed = false; this.updateInitStatus({ status: StorageStatus.LOADING, error: undefined }); try { await this.performInitialize(); this.updateInitStatus({ status: StorageStatus.READY, error: undefined }); return this; } catch (error) { this.updateInitStatus({ status: StorageStatus.ERROR, error: error instanceof Error ? error : new Error(String(error)) }); throw error; } } /** * Синхронная инициализация: доводит хранилище до `READY` в пределах одного тика, без * `await`. Нужна для SSR — синхронного построения «пустого» стора из `initialState` * (см. `createSynapse` → `buildSyncShell`). Доступна только на sync-хранилищах; * у async-хранилищ бросает понятную ошибку. */ initializeSync() { if (this._initStatus.status === StorageStatus.READY) return this; if (typeof this.performInitializeSync !== 'function') { throw new Error(`Storage "${this.name}" не поддерживает синхронную инициализацию (initializeSync доступен только на sync-хранилищах, напр. MemoryStorage).`); } // Allow re-initialization after destroy (симметрично initialize()). this._isDestroyed = false; this.updateInitStatus({ status: StorageStatus.LOADING, error: undefined }); try { this.performInitializeSync(); this.updateInitStatus({ status: StorageStatus.READY, error: undefined }); return this; } catch (error) { this.updateInitStatus({ status: StorageStatus.ERROR, error: error instanceof Error ? error : new Error(String(error)) }); throw error; } } async destroy() { if (this._isDestroyed) return; this._isDestroyed = true; try { if (this._initStatus.status === StorageStatus.LOADING) { try { await this.waitForReady(); } catch { // Инициализация упала — продолжаем destroy } } if (this._initStatus.status === StorageStatus.READY) { await this.performCleanup(); } this.statusSubscribers.clear(); if (this.coreConfig.singleton?.enabled) { const key = SingletonKeyGenerator.generate(this.coreConfig, this.type); SingletonManager.remove(key); } this._stateCache = {}; this.updateInitStatus({ status: StorageStatus.IDLE }); await this.emitEvent({ type: StorageEvents.STORAGE_DESTROY }); } catch (error) { this.logger?.error('Error destroying storage', { error }); throw error; } } // ─── Events ───────────────────────────────────────────────────────────────── async emitEvent(event) { try { await this.eventEmitter?.emit({ ...event, metadata: { ...event.metadata || {}, timestamp: Date.now(), storageName: this.name } }); } catch (error) { this.logger?.error('Error emitting event', { event, error }); } } } export { GLOBAL_SUBSCRIPTION_KEY, StorageCore }; //# sourceMappingURL=storage-core.js.map