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