Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
323 lines
8.6 KiB
JavaScript
323 lines
8.6 KiB
JavaScript
/**
|
|
* Event Bus Module
|
|
*
|
|
* Provides centralized event system for cross-module communication.
|
|
* Features:
|
|
* - Pub/sub pattern
|
|
* - Namespaced events
|
|
* - Event filtering
|
|
* - Event history
|
|
* - Integration with LiveComponents
|
|
* - Integration with SSE
|
|
*/
|
|
|
|
import { Logger } from '../../core/logger.js';
|
|
|
|
/**
|
|
* EventBus - Centralized event system
|
|
*/
|
|
export class EventBus {
|
|
constructor(config = {}) {
|
|
this.config = {
|
|
enableHistory: config.enableHistory ?? false,
|
|
maxHistorySize: config.maxHistorySize || 100,
|
|
enableWildcards: config.enableWildcards ?? true,
|
|
...config
|
|
};
|
|
|
|
this.subscribers = new Map(); // Map<eventName, Set<callback>>
|
|
this.history = [];
|
|
this.middleware = [];
|
|
|
|
Logger.info('[EventBus] Initialized', {
|
|
enableHistory: this.config.enableHistory,
|
|
enableWildcards: this.config.enableWildcards
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a new EventBus instance
|
|
*/
|
|
static create(config = {}) {
|
|
return new EventBus(config);
|
|
}
|
|
|
|
/**
|
|
* Subscribe to an event
|
|
*/
|
|
on(eventName, callback, options = {}) {
|
|
if (typeof callback !== 'function') {
|
|
throw new Error('Callback must be a function');
|
|
}
|
|
|
|
if (!this.subscribers.has(eventName)) {
|
|
this.subscribers.set(eventName, new Set());
|
|
}
|
|
|
|
const subscriber = {
|
|
callback,
|
|
once: options.once ?? false,
|
|
priority: options.priority || 0,
|
|
filter: options.filter || null
|
|
};
|
|
|
|
this.subscribers.get(eventName).add(subscriber);
|
|
|
|
// Sort by priority
|
|
const subscribers = Array.from(this.subscribers.get(eventName));
|
|
subscribers.sort((a, b) => b.priority - a.priority);
|
|
this.subscribers.set(eventName, new Set(subscribers));
|
|
|
|
// Return unsubscribe function
|
|
return () => {
|
|
const callbacks = this.subscribers.get(eventName);
|
|
if (callbacks) {
|
|
callbacks.delete(subscriber);
|
|
if (callbacks.size === 0) {
|
|
this.subscribers.delete(eventName);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Subscribe to an event (once)
|
|
*/
|
|
once(eventName, callback, options = {}) {
|
|
return this.on(eventName, callback, { ...options, once: true });
|
|
}
|
|
|
|
/**
|
|
* Unsubscribe from an event
|
|
*/
|
|
off(eventName, callback) {
|
|
if (!this.subscribers.has(eventName)) {
|
|
return;
|
|
}
|
|
|
|
const callbacks = this.subscribers.get(eventName);
|
|
for (const subscriber of callbacks) {
|
|
if (subscriber.callback === callback) {
|
|
callbacks.delete(subscriber);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (callbacks.size === 0) {
|
|
this.subscribers.delete(eventName);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emit an event
|
|
*/
|
|
emit(eventName, data = null, options = {}) {
|
|
// Apply middleware
|
|
let processedData = data;
|
|
for (const middleware of this.middleware) {
|
|
const result = middleware(eventName, processedData, options);
|
|
if (result === null || result === false) {
|
|
return; // Middleware blocked the event
|
|
}
|
|
if (result !== undefined) {
|
|
processedData = result;
|
|
}
|
|
}
|
|
|
|
// Add to history
|
|
if (this.config.enableHistory) {
|
|
this.addToHistory(eventName, processedData, options);
|
|
}
|
|
|
|
// Get subscribers for exact event name
|
|
const subscribers = this.subscribers.get(eventName);
|
|
if (subscribers) {
|
|
this.notifySubscribers(subscribers, eventName, processedData, options);
|
|
}
|
|
|
|
// Handle wildcards
|
|
if (this.config.enableWildcards) {
|
|
this.handleWildcards(eventName, processedData, options);
|
|
}
|
|
|
|
// Handle namespaced events (e.g., 'user:created' triggers 'user:*')
|
|
this.handleNamespacedEvents(eventName, processedData, options);
|
|
|
|
Logger.debug('[EventBus] Event emitted', { eventName, data: processedData });
|
|
}
|
|
|
|
/**
|
|
* Notify subscribers
|
|
*/
|
|
notifySubscribers(subscribers, eventName, data, options) {
|
|
const subscribersArray = Array.from(subscribers);
|
|
|
|
for (const subscriber of subscribersArray) {
|
|
// Apply filter
|
|
if (subscriber.filter && !subscriber.filter(data, options)) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
subscriber.callback(data, eventName, options);
|
|
} catch (error) {
|
|
Logger.error('[EventBus] Subscriber error', error);
|
|
}
|
|
|
|
// Remove if once
|
|
if (subscriber.once) {
|
|
subscribers.delete(subscriber);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle wildcard subscriptions
|
|
*/
|
|
handleWildcards(eventName, data, options) {
|
|
// Check for '*' subscribers
|
|
const wildcardSubscribers = this.subscribers.get('*');
|
|
if (wildcardSubscribers) {
|
|
this.notifySubscribers(wildcardSubscribers, eventName, data, options);
|
|
}
|
|
|
|
// Check for pattern matches (e.g., 'user:*' matches 'user:created')
|
|
for (const [pattern, subscribers] of this.subscribers.entries()) {
|
|
if (pattern.includes('*') && this.matchesPattern(eventName, pattern)) {
|
|
this.notifySubscribers(subscribers, eventName, data, options);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle namespaced events
|
|
*/
|
|
handleNamespacedEvents(eventName, data, options) {
|
|
const parts = eventName.split(':');
|
|
if (parts.length > 1) {
|
|
// Emit namespace wildcard (e.g., 'user:created' triggers 'user:*')
|
|
const namespacePattern = parts[0] + ':*';
|
|
const namespaceSubscribers = this.subscribers.get(namespacePattern);
|
|
if (namespaceSubscribers) {
|
|
this.notifySubscribers(namespaceSubscribers, eventName, data, options);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if event name matches pattern
|
|
*/
|
|
matchesPattern(eventName, pattern) {
|
|
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
return regex.test(eventName);
|
|
}
|
|
|
|
/**
|
|
* Add middleware
|
|
*/
|
|
use(middleware) {
|
|
if (typeof middleware !== 'function') {
|
|
throw new Error('Middleware must be a function');
|
|
}
|
|
this.middleware.push(middleware);
|
|
}
|
|
|
|
/**
|
|
* Add event to history
|
|
*/
|
|
addToHistory(eventName, data, options) {
|
|
this.history.push({
|
|
eventName,
|
|
data,
|
|
options,
|
|
timestamp: Date.now()
|
|
});
|
|
|
|
// Limit history size
|
|
if (this.history.length > this.config.maxHistorySize) {
|
|
this.history.shift();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get event history
|
|
*/
|
|
getHistory(filter = null) {
|
|
if (!filter) {
|
|
return [...this.history];
|
|
}
|
|
|
|
if (typeof filter === 'string') {
|
|
// Filter by event name
|
|
return this.history.filter(event => event.eventName === filter);
|
|
}
|
|
|
|
if (typeof filter === 'function') {
|
|
// Custom filter function
|
|
return this.history.filter(filter);
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Clear event history
|
|
*/
|
|
clearHistory() {
|
|
this.history = [];
|
|
}
|
|
|
|
/**
|
|
* Get all event names
|
|
*/
|
|
getEventNames() {
|
|
return Array.from(this.subscribers.keys());
|
|
}
|
|
|
|
/**
|
|
* Get subscriber count for an event
|
|
*/
|
|
getSubscriberCount(eventName) {
|
|
const subscribers = this.subscribers.get(eventName);
|
|
return subscribers ? subscribers.size : 0;
|
|
}
|
|
|
|
/**
|
|
* Remove all subscribers for an event
|
|
*/
|
|
removeAllListeners(eventName = null) {
|
|
if (eventName) {
|
|
this.subscribers.delete(eventName);
|
|
} else {
|
|
this.subscribers.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Destroy event bus
|
|
*/
|
|
destroy() {
|
|
this.subscribers.clear();
|
|
this.history = [];
|
|
this.middleware = [];
|
|
|
|
Logger.info('[EventBus] Destroyed');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a global event bus instance
|
|
*/
|
|
let globalEventBus = null;
|
|
|
|
/**
|
|
* Get or create global event bus
|
|
*/
|
|
export function getGlobalEventBus(config = {}) {
|
|
if (!globalEventBus) {
|
|
globalEventBus = EventBus.create(config);
|
|
}
|
|
return globalEventBus;
|
|
}
|
|
|