UNPKG

synapse-storage

Version:

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

190 lines (179 loc) 7.29 kB
import { handleCallbackError } from "../_utils/error-handling.util.js"; import { MemoryStorage } from "../core/index.js"; import { Dispatcher } from "../reactive/index.js"; import { createSynapse } from "./createSynapse/index.js"; function matchEventPattern(eventName, pattern) { if (pattern === '*') return true; if (pattern.includes('*')) { const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$'); return regex.test(eventName); } return eventName === pattern; } function cleanupOldEvents(storage, maxEvents) { const state = storage.getState(); const events = Object.entries(state.events || {}); if (events.length > maxEvents) { const sorted = events.sort((a, b)=>b[1].timestamp - a[1].timestamp); const toKeep = sorted.slice(0, maxEvents); storage.set('events', Object.fromEntries(toKeep)); } } /** * Class-диспетчер EventBus. Экшены объявляются как поля (имя экшена = имя поля), * сервисы (storage-каст, реестр активных подписок, config) захватываются в замыкания * через конструктор. */ class EventBusDispatcher extends Dispatcher { /** MemoryStorage синхронный — безопасно кастуем для синхронного API. */ #sync; /** Активные подписки для очистки при destroy. */ #activeSubscriptions = new Map(); #config; constructor(storage, config){ super(storage); this.#sync = storage; this.#config = config; } /** Публикация события в EventBus. */ publish = this.action((_storage, { event, data, metadata = {} })=>{ const storage = this.#sync; const eventId = `${event}_${Date.now()}_${Math.random()}`; storage.set(`events.${eventId}`, { id: eventId, event, data, metadata: { ttl: metadata.ttl || null, priority: metadata.priority || 'normal', ...metadata }, timestamp: Date.now() }); if (this.#config.autoCleanup) { cleanupOldEvents(storage, this.#config.maxEvents || 1000); } return { eventId, event, data }; }, { type: 'PUBLISH_EVENT', meta: { description: 'Публикация события в EventBus' } }); /** Подписка на события в EventBus. */ subscribe = this.action((_storage, { eventPattern, handler, options = {} })=>{ const storage = this.#sync; const subscriptionId = `sub_${Date.now()}_${Math.random()}`; const unsubscribe = storage.subscribe((state)=>state.events, (events)=>{ Object.values(events || {}).forEach((event)=>{ if (matchEventPattern(event.event, eventPattern)) { if (options.priority && event.metadata.priority !== options.priority) { return; } try { handler(event.data, event); } catch (error) { handleCallbackError(`EventBus: error in handler for "${event.event}"`, error); } } }); }); storage.set(`subscriptions.${subscriptionId}`, { id: subscriptionId, pattern: eventPattern, options, createdAt: Date.now() }); this.#activeSubscriptions.set(subscriptionId, unsubscribe); const wrappedUnsubscribe = ()=>{ this.#activeSubscriptions.delete(subscriptionId); unsubscribe(); }; return { subscriptionId, unsubscribe: wrappedUnsubscribe }; }, { type: 'SUBSCRIBE_TO_EVENT', meta: { description: 'Подписка на события в EventBus' } }); /** Получение истории событий. */ getEventHistory = this.action((_storage, { eventType, limit = 100 })=>{ const state = this.#sync.getState(); return Object.values(state.events || {}).filter((e)=>e.event === eventType).sort((a, b)=>b.timestamp - a.timestamp).slice(0, limit); }, { type: 'GET_EVENT_HISTORY', meta: { description: 'Получение истории событий' } }); /** Очистка событий. */ clearEvents = this.action((_storage, { olderThan } = {})=>{ const storage = this.#sync; if (olderThan) { const cutoff = Date.now() - olderThan; storage.update((state)=>{ Object.keys(state.events || {}).forEach((key)=>{ if (state.events[key].timestamp < cutoff) { delete state.events[key]; } }); }); } else { storage.set('events', {}); } }, { type: 'CLEAR_EVENTS', meta: { description: 'Очистка событий' } }); /** Получение активных подписок. */ getActiveSubscriptions = this.action(()=>{ const state = this.#sync.getState(); return Object.values(state.subscriptions || {}); }, { type: 'GET_SUBSCRIPTIONS', meta: { description: 'Получение активных подписок' } }); destroy() { this.#activeSubscriptions.forEach((unsub)=>unsub()); this.#activeSubscriptions.clear(); super.destroy(); } } /** * Создает EventBus для связи между модулями. * * Возвращает `SynapseModule`-handle (ленивый, PromiseLike): фабрика исполняется при * первом `await`/`ready()`. `eventBus.dispatcher` — инстанс class-диспетчера, его поля * (`publish`/`subscribe`/...) и есть dispatch-функции. * * @example * ```typescript * const eventBus = createEventBus({ name: 'app-events', autoCleanup: true, maxEvents: 500 }) * const bus = await eventBus * * bus.dispatcher.publish({ event: 'USER_UPDATED', data: { userId: 123 } }) * bus.dispatcher.subscribe({ eventPattern: 'CORE_*', handler: (data, event) => {} }) * * // Использование как внешнего диспетчера другого synapse: * const mySynapse = createSynapse({ * storage: () => new MemoryStorage<MyState>({ name: 'my', initialState }), * dispatcher: (s) => new MyDispatcher(s), * externalDispatchers: () => ({ eventBus: eventBus.dispatcher }), * effects: () => new MyEffects(), * }) * ``` */ const createEventBus = (config = {})=>createSynapse({ storage: ()=>new MemoryStorage({ name: config.name || 'eventBus', initialState: { events: {}, subscriptions: {} } }), dispatcher: (storage)=>new EventBusDispatcher(storage, config) }); export { createEventBus }; //# sourceMappingURL=createEventBus.js.map