| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375 |
- // GrabZilla 2.1 - Application Event Bus
- // Centralized event system for loose coupling between modules
- class EventBus {
- constructor() {
- this.listeners = new Map();
- this.eventHistory = [];
- this.maxHistorySize = 100;
- this.debugMode = false;
- }
- // Enable/disable debug logging
- setDebugMode(enabled) {
- this.debugMode = enabled;
- }
- // Subscribe to an event
- on(event, callback, options = {}) {
- if (typeof callback !== 'function') {
- throw new Error('Callback must be a function');
- }
- if (!this.listeners.has(event)) {
- this.listeners.set(event, []);
- }
- const listener = {
- callback,
- once: options.once || false,
- priority: options.priority || 0,
- context: options.context || null,
- id: this.generateListenerId()
- };
- const listeners = this.listeners.get(event);
- listeners.push(listener);
- // Sort by priority (higher priority first)
- listeners.sort((a, b) => b.priority - a.priority);
- if (this.debugMode) {
- console.log(`[EventBus] Subscribed to '${event}' (ID: ${listener.id})`);
- }
- return listener.id;
- }
- // Subscribe to an event only once
- once(event, callback, options = {}) {
- return this.on(event, callback, { ...options, once: true });
- }
- // Unsubscribe from an event
- off(event, callbackOrId) {
- if (!this.listeners.has(event)) {
- return false;
- }
- const listeners = this.listeners.get(event);
- let removed = false;
- if (typeof callbackOrId === 'function') {
- // Remove by callback function
- const index = listeners.findIndex(listener => listener.callback === callbackOrId);
- if (index > -1) {
- listeners.splice(index, 1);
- removed = true;
- }
- } else if (typeof callbackOrId === 'string') {
- // Remove by listener ID
- const index = listeners.findIndex(listener => listener.id === callbackOrId);
- if (index > -1) {
- listeners.splice(index, 1);
- removed = true;
- }
- }
- // Clean up empty event arrays
- if (listeners.length === 0) {
- this.listeners.delete(event);
- }
- if (this.debugMode && removed) {
- console.log(`[EventBus] Unsubscribed from '${event}'`);
- }
- return removed;
- }
- // Remove all listeners for an event
- removeAllListeners(event) {
- if (event) {
- const removed = this.listeners.has(event);
- this.listeners.delete(event);
- if (this.debugMode && removed) {
- console.log(`[EventBus] Removed all listeners for '${event}'`);
- }
- return removed;
- } else {
- // Remove all listeners for all events
- const count = this.listeners.size;
- this.listeners.clear();
- if (this.debugMode && count > 0) {
- console.log(`[EventBus] Removed all listeners (${count} events)`);
- }
- return count > 0;
- }
- }
- // Emit an event
- emit(event, data = null) {
- const eventData = {
- event,
- data,
- timestamp: Date.now(),
- id: this.generateEventId()
- };
- // Add to history
- this.addToHistory(eventData);
- if (this.debugMode) {
- console.log(`[EventBus] Emitting '${event}'`, data);
- }
- if (!this.listeners.has(event)) {
- if (this.debugMode) {
- console.log(`[EventBus] No listeners for '${event}'`);
- }
- return 0;
- }
- const listeners = [...this.listeners.get(event)]; // Copy to avoid modification during iteration
- let callbackCount = 0;
- const removeList = [];
- for (const listener of listeners) {
- try {
- // Call the callback with appropriate context
- if (listener.context) {
- listener.callback.call(listener.context, data, eventData);
- } else {
- listener.callback(data, eventData);
- }
- callbackCount++;
- // Mark for removal if it's a one-time listener
- if (listener.once) {
- removeList.push(listener.id);
- }
- } catch (error) {
- console.error(`[EventBus] Error in listener for '${event}':`, error);
- // Optionally emit an error event
- if (event !== 'error') {
- setTimeout(() => {
- this.emit('error', {
- originalEvent: event,
- originalData: data,
- error,
- listenerId: listener.id
- });
- }, 0);
- }
- }
- }
- // Remove one-time listeners
- removeList.forEach(id => this.off(event, id));
- return callbackCount;
- }
- // Emit an event asynchronously
- async emitAsync(event, data = null) {
- const eventData = {
- event,
- data,
- timestamp: Date.now(),
- id: this.generateEventId()
- };
- // Add to history
- this.addToHistory(eventData);
- if (this.debugMode) {
- console.log(`[EventBus] Emitting async '${event}'`, data);
- }
- if (!this.listeners.has(event)) {
- return 0;
- }
- const listeners = [...this.listeners.get(event)];
- let callbackCount = 0;
- const removeList = [];
- const promises = [];
- for (const listener of listeners) {
- const promise = (async () => {
- try {
- let result;
- if (listener.context) {
- result = listener.callback.call(listener.context, data, eventData);
- } else {
- result = listener.callback(data, eventData);
- }
- // Handle async callbacks
- if (result instanceof Promise) {
- await result;
- }
- callbackCount++;
- if (listener.once) {
- removeList.push(listener.id);
- }
- } catch (error) {
- console.error(`[EventBus] Error in async listener for '${event}':`, error);
- if (event !== 'error') {
- setTimeout(() => {
- this.emit('error', {
- originalEvent: event,
- originalData: data,
- error,
- listenerId: listener.id
- });
- }, 0);
- }
- }
- })();
- promises.push(promise);
- }
- await Promise.all(promises);
- // Remove one-time listeners
- removeList.forEach(id => this.off(event, id));
- return callbackCount;
- }
- // Check if there are listeners for an event
- hasListeners(event) {
- return this.listeners.has(event) && this.listeners.get(event).length > 0;
- }
- // Get the number of listeners for an event
- getListenerCount(event) {
- return this.listeners.has(event) ? this.listeners.get(event).length : 0;
- }
- // Get all event names that have listeners
- getEventNames() {
- return Array.from(this.listeners.keys());
- }
- // Get event history
- getEventHistory(eventFilter = null, limit = 10) {
- let history = [...this.eventHistory];
- if (eventFilter) {
- history = history.filter(item => item.event === eventFilter);
- }
- return history.slice(-limit);
- }
- // Clear event history
- clearHistory() {
- this.eventHistory = [];
- }
- // Generate unique listener ID
- generateListenerId() {
- return 'listener_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
- }
- // Generate unique event ID
- generateEventId() {
- return 'event_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
- }
- // Add event to history
- addToHistory(eventData) {
- this.eventHistory.push(eventData);
- // Limit history size
- if (this.eventHistory.length > this.maxHistorySize) {
- this.eventHistory = this.eventHistory.slice(-this.maxHistorySize);
- }
- }
- // Wait for a specific event (returns a promise)
- waitFor(event, timeout = 5000) {
- return new Promise((resolve, reject) => {
- let timeoutId;
- let listenerId;
- // Set up timeout
- if (timeout > 0) {
- timeoutId = setTimeout(() => {
- this.off(event, listenerId);
- reject(new Error(`Timeout waiting for event '${event}'`));
- }, timeout);
- }
- // Listen for the event
- listenerId = this.once(event, (data) => {
- if (timeoutId) {
- clearTimeout(timeoutId);
- }
- resolve(data);
- });
- });
- }
- // Create a namespace for events (useful for modules)
- namespace(prefix) {
- return {
- on: (event, callback, options) => this.on(`${prefix}:${event}`, callback, options),
- once: (event, callback, options) => this.once(`${prefix}:${event}`, callback, options),
- off: (event, callback) => this.off(`${prefix}:${event}`, callback),
- emit: (event, data) => this.emit(`${prefix}:${event}`, data),
- emitAsync: (event, data) => this.emitAsync(`${prefix}:${event}`, data),
- hasListeners: (event) => this.hasListeners(`${prefix}:${event}`),
- getListenerCount: (event) => this.getListenerCount(`${prefix}:${event}`)
- };
- }
- // Debug information
- getDebugInfo() {
- const events = this.getEventNames().map(event => ({
- event,
- listenerCount: this.getListenerCount(event),
- listeners: this.listeners.get(event).map(l => ({
- id: l.id,
- priority: l.priority,
- once: l.once,
- hasContext: !!l.context
- }))
- }));
- return {
- totalEvents: events.length,
- totalListeners: events.reduce((sum, e) => sum + e.listenerCount, 0),
- events,
- historySize: this.eventHistory.length
- };
- }
- }
- // Create global event bus instance
- const eventBus = new EventBus();
- // Enable debug mode in development
- if (typeof window !== 'undefined' && window.location?.hostname === 'localhost') {
- eventBus.setDebugMode(true);
- }
- // Export for use in other modules
- if (typeof module !== 'undefined' && module.exports) {
- // Node.js environment
- module.exports = { EventBus, eventBus };
- } else {
- // Browser environment - attach to window
- window.EventBus = EventBus;
- window.eventBus = eventBus;
- }
|