Add event handler class
This commit is contained in:
parent
1ed22c72d7
commit
ad3191957a
14 changed files with 2428 additions and 2309 deletions
|
|
@ -0,0 +1,147 @@
|
|||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { EventListeners } from "./event-listeners";
|
||||
|
||||
describe("EventListeners", () => {
|
||||
it("should add & remove listeners", () => {
|
||||
const listeners = new EventListeners<() => void>();
|
||||
const listener = () => { };
|
||||
|
||||
listeners.add(listener);
|
||||
|
||||
assert.strictEqual(listeners.count, 1);
|
||||
|
||||
const removed = listeners.remove(listener);
|
||||
assert.strictEqual(removed, true);
|
||||
assert.strictEqual(listeners.count, 0);
|
||||
});
|
||||
|
||||
|
||||
it("should remove listeners using unsubscribe function", () => {
|
||||
const listeners = new EventListeners<() => void>();
|
||||
const listener = () => { };
|
||||
|
||||
const unsubscribe = listeners.add(listener);
|
||||
unsubscribe();
|
||||
|
||||
assert.strictEqual(listeners.count, 0);
|
||||
});
|
||||
|
||||
it("should return false when removing non-existent listener", () => {
|
||||
const listeners = new EventListeners<() => void>();
|
||||
const listener = () => { };
|
||||
|
||||
const removed = listeners.remove(listener);
|
||||
|
||||
assert.strictEqual(removed, false);
|
||||
});
|
||||
|
||||
it("should handle multiple listeners", () => {
|
||||
const listeners = new EventListeners<() => void>();
|
||||
const listener1 = () => { };
|
||||
const listener2 = () => { };
|
||||
const listener3 = () => { };
|
||||
|
||||
listeners.add(listener1);
|
||||
listeners.add(listener2);
|
||||
listeners.add(listener3);
|
||||
|
||||
assert.strictEqual(listeners.count, 3);
|
||||
|
||||
listeners.remove(listener2);
|
||||
|
||||
assert.strictEqual(listeners.count, 2);
|
||||
});
|
||||
|
||||
it("should trigger all listeners synchronously", () => {
|
||||
const listeners = new EventListeners<(value: string) => void>();
|
||||
const calls: string[] = [];
|
||||
|
||||
listeners.add((value) => calls.push(`listener1-${value}`));
|
||||
listeners.add((value) => calls.push(`listener2-${value}`));
|
||||
|
||||
listeners.trigger("test");
|
||||
|
||||
assert.deepStrictEqual(calls, ["listener1-test", "listener2-test"]);
|
||||
});
|
||||
|
||||
it("should trigger listeners with multiple arguments", () => {
|
||||
const listeners = new EventListeners<
|
||||
(a: number, b: string, c: boolean) => void
|
||||
>();
|
||||
const calls: [number, string, boolean][] = [];
|
||||
|
||||
listeners.add((a, b, c) => calls.push([a, b, c]));
|
||||
listeners.trigger(42, "hello", true);
|
||||
|
||||
assert.deepStrictEqual(calls, [[42, "hello", true]]);
|
||||
});
|
||||
|
||||
it("should not trigger removed listeners", () => {
|
||||
const listeners = new EventListeners<() => void>();
|
||||
let count1 = 0;
|
||||
let count2 = 0;
|
||||
|
||||
const listener1 = () => {
|
||||
count1++;
|
||||
};
|
||||
const listener2 = () => {
|
||||
count2++;
|
||||
};
|
||||
|
||||
listeners.add(listener1);
|
||||
const unsubscribe = listeners.add(listener2);
|
||||
|
||||
unsubscribe();
|
||||
listeners.trigger();
|
||||
|
||||
assert.strictEqual(count1, 1);
|
||||
assert.strictEqual(count2, 0);
|
||||
});
|
||||
|
||||
it("should trigger all listeners and await promises", async () => {
|
||||
const listeners = new EventListeners<
|
||||
(value: string) => Promise<void> | void
|
||||
>();
|
||||
const results: string[] = [];
|
||||
|
||||
listeners.add(async (value) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
results.push(`async1-${value}`);
|
||||
});
|
||||
|
||||
listeners.add((value) => {
|
||||
results.push(`sync-${value}`);
|
||||
});
|
||||
|
||||
listeners.add(async (value) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
results.push(`async2-${value}`);
|
||||
});
|
||||
|
||||
await listeners.triggerAsync("test");
|
||||
|
||||
assert.ok(results.includes("async1-test"));
|
||||
assert.ok(results.includes("sync-test"));
|
||||
assert.ok(results.includes("async2-test"));
|
||||
assert.strictEqual(results.length, 3);
|
||||
});
|
||||
|
||||
|
||||
|
||||
it("should not trigger cleared listeners", () => {
|
||||
const listeners = new EventListeners<() => void>();
|
||||
let called = false;
|
||||
const listener = () => {
|
||||
called = true;
|
||||
};
|
||||
|
||||
listeners.add(listener);
|
||||
listeners.clear();
|
||||
|
||||
assert.strictEqual(listeners.count, 0);
|
||||
listeners.trigger();
|
||||
|
||||
assert.strictEqual(called, false);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import { removeFromArray } from "../remove-from-array";
|
||||
import { awaitAll } from "../await-all";
|
||||
|
||||
/**
|
||||
* A utility class for managing event listeners with type-safe add/remove operations.
|
||||
*/
|
||||
export class EventListeners<TListener extends (...args: any[]) => any> {
|
||||
private readonly listeners: TListener[] = [];
|
||||
|
||||
/**
|
||||
* Adds a new listener to the collection.
|
||||
*
|
||||
* @param listener The listener callback to add
|
||||
* @returns An unsubscribe function that removes this listener when called
|
||||
*/
|
||||
public add(listener: TListener): () => void {
|
||||
this.listeners.push(listener);
|
||||
return () => this.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a listener from the collection.
|
||||
*
|
||||
* @param listener The listener callback to remove
|
||||
* @returns true if the listener was found and removed, false otherwise
|
||||
*/
|
||||
public remove(listener: TListener): boolean {
|
||||
return removeFromArray(this.listeners, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers all listeners synchronously with the provided arguments.
|
||||
* Any returned promises are ignored. Use triggerAsync() to await them.
|
||||
*
|
||||
* @param args The arguments to pass to each listener
|
||||
*/
|
||||
public trigger(...args: Parameters<TListener>): void {
|
||||
this.listeners.forEach((listener) => {
|
||||
listener(...args);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers all listeners and awaits any promises they return.
|
||||
* Synchronous listeners are called immediately, and any async listeners
|
||||
* are awaited in parallel.
|
||||
*
|
||||
* @param args The arguments to pass to each listener
|
||||
*/
|
||||
public async triggerAsync(...args: Parameters<TListener>): Promise<void> {
|
||||
await awaitAll(
|
||||
this.listeners
|
||||
.map((listener) => {
|
||||
return listener(...args);
|
||||
})
|
||||
.filter((result): result is Promise<unknown> => {
|
||||
return result instanceof Promise;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.listeners.length = 0;
|
||||
}
|
||||
|
||||
public get count(): number {
|
||||
return this.listeners.length;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue