event-emitter.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. /**
  2. * @fileoverview Enhanced event emitter with type safety and error handling
  3. * @author GrabZilla Development Team
  4. * @version 2.1.0
  5. * @since 2024-01-01
  6. */
  7. import { EVENTS } from '../constants/config.js';
  8. /**
  9. * Enhanced Event Emitter with Observer Pattern
  10. *
  11. * Provides type-safe event handling with proper error boundaries
  12. * and performance optimizations for the application state system
  13. */
  14. class EventEmitter {
  15. /**
  16. * Creates new EventEmitter instance
  17. */
  18. constructor() {
  19. this.listeners = new Map();
  20. this.maxListeners = 50; // Prevent memory leaks
  21. }
  22. /**
  23. * Register event listener with validation
  24. * @param {string} event - Event name (must be from EVENTS constants)
  25. * @param {Function} callback - Event callback function
  26. * @param {Object} options - Listener options
  27. * @param {boolean} options.once - Remove listener after first call
  28. * @param {number} options.priority - Execution priority (higher = earlier)
  29. * @throws {Error} When event name is invalid or max listeners exceeded
  30. */
  31. on(event, callback, options = {}) {
  32. // Validate event name
  33. if (!Object.values(EVENTS).includes(event)) {
  34. console.warn(`Unknown event type: ${event}. Consider adding to EVENTS constants.`);
  35. }
  36. // Validate callback
  37. if (typeof callback !== 'function') {
  38. throw new Error('Event callback must be a function');
  39. }
  40. // Initialize listeners array for event
  41. if (!this.listeners.has(event)) {
  42. this.listeners.set(event, []);
  43. }
  44. const eventListeners = this.listeners.get(event);
  45. // Check max listeners limit
  46. if (eventListeners.length >= this.maxListeners) {
  47. throw new Error(`Maximum listeners (${this.maxListeners}) exceeded for event: ${event}`);
  48. }
  49. // Create listener object with metadata
  50. const listener = {
  51. callback,
  52. once: options.once || false,
  53. priority: options.priority || 0,
  54. id: this.generateListenerId()
  55. };
  56. // Insert listener based on priority (higher priority first)
  57. const insertIndex = eventListeners.findIndex(l => l.priority < listener.priority);
  58. if (insertIndex === -1) {
  59. eventListeners.push(listener);
  60. } else {
  61. eventListeners.splice(insertIndex, 0, listener);
  62. }
  63. return listener.id; // Return ID for removal
  64. }
  65. /**
  66. * Register one-time event listener
  67. * @param {string} event - Event name
  68. * @param {Function} callback - Event callback function
  69. * @returns {string} Listener ID for removal
  70. */
  71. once(event, callback) {
  72. return this.on(event, callback, { once: true });
  73. }
  74. /**
  75. * Remove event listener by callback or ID
  76. * @param {string} event - Event name
  77. * @param {Function|string} callbackOrId - Callback function or listener ID
  78. * @returns {boolean} True if listener was removed
  79. */
  80. off(event, callbackOrId) {
  81. if (!this.listeners.has(event)) {
  82. return false;
  83. }
  84. const eventListeners = this.listeners.get(event);
  85. let index = -1;
  86. if (typeof callbackOrId === 'string') {
  87. // Remove by ID
  88. index = eventListeners.findIndex(l => l.id === callbackOrId);
  89. } else {
  90. // Remove by callback function
  91. index = eventListeners.findIndex(l => l.callback === callbackOrId);
  92. }
  93. if (index > -1) {
  94. eventListeners.splice(index, 1);
  95. return true;
  96. }
  97. return false;
  98. }
  99. /**
  100. * Remove all listeners for an event
  101. * @param {string} event - Event name
  102. * @returns {number} Number of listeners removed
  103. */
  104. removeAllListeners(event) {
  105. if (!this.listeners.has(event)) {
  106. return 0;
  107. }
  108. const count = this.listeners.get(event).length;
  109. this.listeners.delete(event);
  110. return count;
  111. }
  112. /**
  113. * Emit event to all registered listeners with error handling
  114. * @param {string} event - Event name
  115. * @param {Object} data - Event data
  116. * @returns {Promise<Object>} Emission results with success/error counts
  117. */
  118. async emit(event, data = {}) {
  119. if (!this.listeners.has(event)) {
  120. return { success: 0, errors: 0, listeners: 0 };
  121. }
  122. const eventListeners = [...this.listeners.get(event)]; // Copy to avoid mutation during iteration
  123. const results = { success: 0, errors: 0, listeners: eventListeners.length };
  124. const toRemove = []; // Track one-time listeners to remove
  125. // Execute listeners with error boundaries
  126. for (const listener of eventListeners) {
  127. try {
  128. // Execute callback (handle both sync and async)
  129. const result = listener.callback(data);
  130. if (result instanceof Promise) {
  131. await result;
  132. }
  133. results.success++;
  134. // Mark one-time listeners for removal
  135. if (listener.once) {
  136. toRemove.push(listener.id);
  137. }
  138. } catch (error) {
  139. results.errors++;
  140. console.error(`Error in event listener for ${event}:`, {
  141. error: error.message,
  142. stack: error.stack,
  143. listenerId: listener.id,
  144. eventData: data
  145. });
  146. }
  147. }
  148. // Remove one-time listeners
  149. toRemove.forEach(id => this.off(event, id));
  150. return results;
  151. }
  152. /**
  153. * Get listener count for an event
  154. * @param {string} event - Event name
  155. * @returns {number} Number of listeners
  156. */
  157. listenerCount(event) {
  158. return this.listeners.has(event) ? this.listeners.get(event).length : 0;
  159. }
  160. /**
  161. * Get all event names with listeners
  162. * @returns {string[]} Array of event names
  163. */
  164. eventNames() {
  165. return Array.from(this.listeners.keys());
  166. }
  167. /**
  168. * Set maximum number of listeners per event
  169. * @param {number} max - Maximum listeners (0 = unlimited)
  170. */
  171. setMaxListeners(max) {
  172. this.maxListeners = Math.max(0, max);
  173. }
  174. /**
  175. * Generate unique listener ID
  176. * @returns {string} Unique listener identifier
  177. * @private
  178. */
  179. generateListenerId() {
  180. return `listener_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  181. }
  182. /**
  183. * Clear all listeners (useful for cleanup)
  184. */
  185. clear() {
  186. this.listeners.clear();
  187. }
  188. }