Event Source/Handler helpers with optional frequency ratio limit
A few simple generic classes to create event sources and handlers. The handlers can optionally be set to receive events with a maximum frequency limit.
export type EventHandlerCallback<TArgs extends unknown[]> = (...args: TArgs) => void;
const MARK_REMOVED = Symbol("markRemoved");
export class EventHandler<TArgs extends unknown[]> {
private lastFired: number = 0;
private removed: boolean = false;
constructor(
private readonly source: EventSource<TArgs>,
private readonly callback: EventHandlerCallback<TArgs>,
private readonly minIntervalMS: number,
) {}
get isRemoved(): boolean {
return this.removed;
}
fire(args: TArgs): void {
// A handler removed earlier in the current dispatch round must not run.
if (this.removed) {
return;
}
const now = performance.now();;
if (this.minIntervalMS > 0 && now - this.lastFired < this.minIntervalMS) {
return;
}
this.lastFired = now;
this.callback(...args);
}
remove(): void {
this.source.removeHandler(this);
}
/** @internal — called by EventSource only. */
[MARK_REMOVED](): void {
this.removed = true;
}
}
export class EventSource<TArgs extends unknown[] = []> {
private readonly handlers: EventHandler<TArgs>[] = [];
addHandler(callback: EventHandlerCallback<TArgs>, minIntervalMS: number = 0): EventHandler<TArgs> {
const handler = new EventHandler(this, callback, minIntervalMS);
this.handlers.push(handler);
return handler;
}
removeHandler(handler: EventHandler<TArgs>): void {
const idx = this.handlers.indexOf(handler);
if (idx === -1) {
return;
}
this.handlers.splice(idx, 1);
handler[MARK_REMOVED]();
}
fire(...args: TArgs): void {
// Iterate over a snapshot: handlers may add or remove handlers while running.
const snapshot = this.handlers.slice();
for (const handler of snapshot) {
try {
handler.fire(args);
} catch (error) {
// One failing handler must not abort the remaining ones. Rethrow
// asynchronously so the error still surfaces to the global handler
// instead of being silently swallowed.
queueMicrotask(() => {
throw error;
});
}
}
}
}
Comments
No comments yet - be the first.
Leave a comment