/**
* @typedef {Object} SignalHandler
* @property {Object.<string, Function[]>} signals - Map of event names to arrays of callback functions
* @property {string[]} allSignals - List of all registered signal names
*/
/**
* Adds event handling capabilities to a prototype.
* Creates a simple event system that allows objects to emit and listen to events.
*
* The function modifies the prototype by adding:
* - Event registration methods
* - Event emission methods
* - Signal initialization
* - Signal storage
*
*
* Implementation Details
*
* The signal system works by:
* 1. Extending the prototype with signal tracking properties
* 2. Maintaining arrays of callbacks for each signal type
* 3. Providing methods to register and trigger callbacks
*
* Signal Storage Structure:
* ```javascript
* {
* signals: {
* 'eventName1': [callback1, callback2, ...],
* 'eventName2': [callback3, callback4, ...]
* },
* allSignals: ['eventName1', 'eventName2', ...]
* }
* ```
*
* Performance Considerations:
* - Callbacks are stored in arrays for fast iteration
* - Signals are initialized lazily on first use
* - Direct property access for quick event emission
*
* Usage Notes:
* - Events must be registered before they can be used
* - Multiple callbacks can be registered for the same event
* - Callbacks are executed synchronously
* - Parameters are passed through to callbacks unchanged
*
* @function
* @param {Object} proto - The prototype to enhance with signal capabilities
* @param {...string} signals - Names of signals to register
*
* @example
* ```javascript
* // Add events to a class
* class MyClass {}
* addSignals(MyClass, 'update', 'change');
*
* // Use events
* const obj = new MyClass();
* obj.addEvent('update', () => console.log('Updated!'));
* obj.emit('update');
* ```
*
* @example
* ```javascript
* // Multiple signals
* class DataHandler {}
* addSignals(DataHandler,
* 'dataLoaded',
* 'dataProcessed',
* 'error'
* );
*
* const handler = new DataHandler();
* handler.addEvent('dataLoaded', (data) => {
* console.log('Data loaded:', data);
* });
* ```
*/
function addSignals(proto, ...signals) {
proto.prototype.allSignals ??= [];
proto.prototype.allSignals = [...proto.prototype.allSignals, ...signals];
/**
* Methods added to the prototype
*/
/**
* Initializes the signals system for an instance.
* Creates the signals storage object and populates it with empty arrays
* for each registered signal type.
*
* @memberof SignalHandler
* @instance
* @private
*/
proto.prototype.initSignals = function () {
// Use nullish coalescing for signal initialization
this.signals ??= Object.fromEntries(this.allSignals.map(s => [s, []]));
}
/**
* Registers a callback function for a specific event.
*
* @memberof SignalHandler
* @instance
* @param {string} event - The event name to listen for
* @param {Function} callback - Function to be called when event is emitted
* @throws {Error} Implicitly if event doesn't exist
*
* @example
* ```javascript
* obj.addEvent('update', (param1, param2) => {
* console.log('Update occurred with:', param1, param2);
* });
* ```
*/
proto.prototype.addEvent = function (event, callback) {
// Use optional chaining for safer access
this.signals?.hasOwnProperty(event) || this.initSignals();
this.signals[event].push(callback);
}
/**
* Adds a one-time event listener that will be automatically removed after first execution.
* Once the event is emitted, the listener is automatically removed before the callback
* is executed.
*
* @memberof SignalHandler
* @instance
* @param {string} event - The event name to listen for once
* @param {Function} callback - Function to be called once when event is emitted
* @throws {Error} Implicitly if event doesn't exist or callback is not a function
*
* @example
* ```javascript
* obj.once('update', (param) => {
* console.log('This will only run once:', param);
* });
* ```
*/
proto.prototype.once = function (event, callback) {
if (!callback || typeof callback !== 'function') {
console.error('Callback must be a function');
return;
}
const wrappedCallback = (...args) => {
// Remove the listener before calling the callback
// to prevent recursion if the callback emits the same event
this.removeEvent(event, wrappedCallback);
callback.apply(this, args);
};
this.addEvent(event, wrappedCallback);
}
/**
* Removes an event callback or all callbacks for a specific event.
* If no callback is provided, all callbacks for the event are removed.
* If a callback is provided, only that specific callback is removed.
*
* @memberof SignalHandler
* @instance
* @param {string} event - The event name to remove callback(s) from
* @param {Function} [callback] - Optional specific callback function to remove
* @returns {boolean} True if callback(s) were removed, false if event or callback not found
* @throws {Error} Implicitly if event doesn't exist
*
* @example
* ```javascript
* // Remove specific callback
* const callback = (data) => console.log(data);
* obj.addEvent('update', callback);
* obj.removeEvent('update', callback);
*
* // Remove all callbacks for an event
* obj.removeEvent('update');
* ```
*/
proto.prototype.removeEvent = function (event, callback) {
if (!this.signals) {
this.initSignals();
return false;
}
if (!this.signals[event]) {
return false;
}
if (callback === undefined) {
// Remove all callbacks for this event
const hadCallbacks = this.signals[event].length > 0;
this.signals[event] = [];
return hadCallbacks;
}
// Find and remove specific callback
const initialLength = this.signals[event].length;
this.signals[event] = this.signals[event].filter(cb => cb !== callback);
return initialLength > this.signals[event].length;
}
/**
* Emits an event, triggering all registered callbacks.
* Callbacks are executed in the order they were registered.
* Creates a copy of the callbacks array before iteration to prevent
* issues if callbacks modify the listeners during emission.
*
* @memberof SignalHandler
* @instance
* @param {string} event - The event name to emit
* @param {...*} parameters - Parameters to pass to the callback functions
*
* @example
* ```javascript
* obj.emit('update', 'param1', 42);
* ```
*/
proto.prototype.emit = function (event, ...parameters) {
if (!this.signals)
this.initSignals();
// Create a copy of the callbacks array to safely iterate even if
// callbacks modify the listeners
const callbacks = [...this.signals[event]];
for (let r of callbacks)
r(...parameters);
}
}
export { addSignals }