@fimbul-works/observable
Version:
A lightweight, strongly-typed TypeScript library for reactive programming patterns, providing observable collections, values, and event handling.
202 lines (201 loc) • 6.86 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Signal = void 0;
/**
* Signal class implements a publish/subscribe pattern for event handling with error management
* and support for both synchronous and asynchronous operations.
*
* @template T The type of signal to emit
* @template R The return type of handlers (void or Promise<void>)
*/
class Signal {
/** Event handlers */
#handlers = new Set();
/** Error event handlers */
#errorHandlers = new Set();
/** One-time event handlers */
#onceHandlers = new Set();
/**
* Registers a callback function to be executed when the signal is emitted.
* The callback can be synchronous or asynchronous (return a Promise).
*
* @param fn - Function to be called when the signal is emitted
* @returns A cleanup function that removes the handler when called
*/
connect(fn) {
this.#handlers.add(fn);
return () => this.#handlers.delete(fn);
}
/**
* Registers a one-time callback function that will be automatically removed after being called.
* The callback can be synchronous or asynchronous (return a Promise).
*
* @param fn - Function to be called once when the signal is emitted
* @returns A cleanup function that removes the handler when called
*/
once(fn) {
this.#onceHandlers.add(fn);
return () => this.#onceHandlers.delete(fn);
}
/**
* Removes a previously registered callback function.
* @param fn - The callback function to remove. A falsy value will disconnect all subscribers.
* @returns this for method chaining
*/
disconnect(fn) {
if (!fn) {
this.#handlers.clear();
this.#onceHandlers.clear();
}
else {
this.#handlers.delete(fn);
this.#onceHandlers.delete(fn);
}
return this;
}
/**
* Triggers the signal, executing all registered callbacks with the provided data.
* This method runs synchronously and doesn't wait for any promises returned by handlers.
*
* @param data - The data to pass to the callback functions
* @returns The number of handlers that were called
*/
emit(data) {
let handlerCount = 0;
// Handle regular subscribers
for (const fn of this.#handlers.values()) {
try {
const result = fn(data);
// If handler returns a promise, attach error handling but don't wait
if (result instanceof Promise) {
result.catch(this.#handleError.bind(this));
}
handlerCount++;
}
catch (error) {
this.#handleError(error);
}
}
// Handle one-time subscribers
for (const fn of Array.from(this.#onceHandlers.values())) {
try {
const result = fn(data);
// If handler returns a promise, attach error handling but don't wait
if (result instanceof Promise) {
result.catch(this.#handleError.bind(this));
}
handlerCount++;
this.#onceHandlers.delete(fn);
}
catch (error) {
this.#handleError(error);
this.#onceHandlers.delete(fn);
}
}
return handlerCount;
}
/**
* Triggers the signal and waits for all handlers to complete, including any that return Promises.
*
* @param data - The data to pass to the callback functions
* @returns Promise resolving to the number of handlers that were called
*/
async emitAsync(data) {
let handlerCount = 0;
const promises = [];
// Process regular handlers
for (const fn of this.#handlers.values()) {
try {
const result = fn(data);
if (result instanceof Promise) {
promises.push(result.catch(this.#handleError.bind(this)));
}
handlerCount++;
}
catch (error) {
this.#handleError(error);
}
}
// Process one-time handlers
for (const fn of Array.from(this.#onceHandlers.values())) {
try {
const result = fn(data);
if (result instanceof Promise) {
promises.push(result.catch(this.#handleError.bind(this)));
}
handlerCount++;
this.#onceHandlers.delete(fn);
}
catch (error) {
this.#handleError(error);
this.#onceHandlers.delete(fn);
}
}
// Wait for all promises to resolve
if (promises.length > 0) {
await Promise.all(promises);
}
return handlerCount;
}
/**
* Registers an error handler function.
* @param fn - The error handler function to add
* @returns A cleanup function that removes the error handler when called
*/
connectError(fn) {
this.#errorHandlers.add(fn);
return () => this.#errorHandlers.delete(fn);
}
/**
* Removes a previously registered error handler function.
* @param fn - The error handler function to remove
* @returns this for method chaining
*/
disconnectError(fn) {
this.#errorHandlers.delete(fn);
return this;
}
/**
* Returns the total number of handlers currently registered.
* @returns The number of all types of handlers combined
*/
listenerCount() {
return this.#handlers.size + this.#onceHandlers.size;
}
/**
* Checks if there are any handlers registered.
* @returns True if there are any handlers, false otherwise
*/
hasHandlers() {
return this.#handlers.size > 0 || this.#onceHandlers.size > 0;
}
/**
* Cleans up all event subscriptions and releases resources.
* Call this method when the signal is no longer needed.
*/
destroy() {
this.#handlers.clear();
this.#onceHandlers.clear();
this.#errorHandlers.clear();
}
/**
* Internal method to handle errors from event handlers
*/
#handleError(error) {
const errorObj = error instanceof Error ? error : new Error(String(error));
if (this.#errorHandlers.size > 0) {
for (const errFn of this.#errorHandlers) {
try {
errFn(errorObj);
}
catch (err) {
console.error("Error in error handler:", err);
}
}
}
else {
console.error(errorObj);
}
}
}
exports.Signal = Signal;