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