evnty
Version:
Async-first, reactive event handling library for complex event flows in browser and Node.js
136 lines (128 loc) • 3.72 kB
text/typescript
import { Async } from './async.js';
/**
* Promise-based async coordination primitive.
* `emit()` resolves the pending `receive()` promise (shared across callers).
* Reusable - after each emission a new round of `receive()` calls can be made.
* Disposable via `[Symbol.dispose]()` or an optional AbortSignal.
*
* @template T The type of value that this signal carries.
*
* @example
* ```typescript
* const signal = new Signal<string>();
*
* const promise = signal.receive();
* signal.emit('hello');
* await promise; // 'hello'
* ```
*/
export class Signal<T> extends Async<T, boolean> {
#rx?: PromiseWithResolvers<T>;
readonly [Symbol.toStringTag] = 'Signal';
/**
* Merges multiple source signals into a target signal.
* Values from any source signal are emitted to the target signal.
* The merge continues until the target signal is disposed.
*
* Note: When the target is disposed, iteration stops after the next value
* from each source. For immediate cleanup, dispose source signals directly.
*
* @param target The signal that will receive values from all sources
* @param signals The source signals to merge from
*
* @example
* ```typescript
* // Create a target signal and source signals
* const target = new Signal<string>();
* const source1 = new Signal<string>();
* const source2 = new Signal<string>();
*
* // Merge sources into target
* Signal.merge(target, source1, source2);
*
* // Values from any source appear in target
* const promise = target.receive();
* source1.emit('Hello');
* const value = await promise; // 'Hello'
* ```
*/
static merge<T>(target: Signal<T>, ...signals: Signal<T>[]): void {
if (target.disposed) {
return;
}
for (const source of signals) {
void (async () => {
try {
const sink = target.sink;
for await (const value of source) {
if (!sink(value) && target.disposed) {
return;
}
}
} catch {
// ignore disposed signal
}
})();
}
}
/**
* Creates a new Signal instance.
*
* @param abortSignal An optional AbortSignal that can be used to cancel the signal operation.
*
* @example
* ```typescript
* // Create a signal with abort capability
* const controller = new AbortController();
* const signal = new Signal<number>(controller.signal);
*
* // Signal can be cancelled
* controller.abort('Operation cancelled');
* ```
*/
constructor(abortSignal?: AbortSignal) {
super(abortSignal);
}
/**
* Sends a value to the waiting receiver, if any.
*
* @param value - The value to send.
* @returns `true` if the value was emitted.
*/
emit(value: T): boolean {
if (!this.#rx) return false;
this.#rx.resolve(value);
this.#rx = undefined;
return true;
}
/**
* Waits for the next value to be sent to this signal. If the signal has been aborted
* or disposed, this method rejects with Error('Disposed').
*
* @returns A promise that resolves with the next value sent to the signal.
*
* @example
* ```typescript
* const signal = new Signal<string>();
*
* // Wait for a value
* const valuePromise = signal.receive();
*
* // Send a value from elsewhere
* signal.emit('Hello');
*
* const value = await valuePromise; // 'Hello'
* ```
*/
receive(): Promise<T> {
if (this.disposed) {
return Promise.reject(new Error('Disposed'));
}
this.#rx ??= Promise.withResolvers<T>();
return this.#rx.promise;
}
dispose(): void {
this.#rx?.reject(new Error('Disposed'));
this.#rx = undefined;
}
}