UNPKG

@irrelon/emitter

Version:

Simple JavaScript event emitter with some powerful features.

440 lines (439 loc) 21.2 kB
import { jest } from "@jest/globals"; import assert from "assert"; import { Emitter, ListenerReturnFlag } from "../src/Emitter"; let expectedAssertions = 0; let actualAssertions = 0; function expectAssertionCount(val) { expectedAssertions = val; } function resetAssertionCount() { expectedAssertions = 0; actualAssertions = 0; } function countAssertion() { actualAssertions++; } function checkAssertionCount() { if (expectedAssertions === undefined || expectedAssertions === actualAssertions) { return; } throw new Error("expected " + expectedAssertions + " assertions, got " + actualAssertions); } beforeEach(resetAssertionCount); afterEach(checkAssertionCount); describe("Emitter", () => { describe("new Emitter()", () => { it("Creates emitter instance by instantiation", () => { const myClass = new Emitter(); myClass.on("moo", () => { }); assert.strictEqual(myClass._eventListeners?.moo["*"].length, 1, "Listener registered on event"); myClass.off("moo"); assert.strictEqual(!myClass._eventListeners?.moo || !myClass._eventListeners.moo["*"] || myClass._eventListeners.moo["*"].length, true, "Listeners all removed from event"); }); it("Supports extending Emitter as a base class", () => { // Test extending Emitter and modifying base functionality class MyClass extends Emitter { on(eventName, ...rest) { const restTypes = rest.map((arg) => typeof arg); if (restTypes[0] === "function") { const listener = restTypes[0]; const id = "^^noId"; return super.on(eventName, id, listener); } return super.on(eventName, rest[0], rest[1]); } // @ts-ignore emit(eventName, ...rest) { return super.emitId(eventName, "^^noId", ...rest); } off(eventName, ...rest) { let id = rest[0]; let func = rest[1]; if (typeof rest[0] === "function") { func = rest[0]; id = "^^noId"; } else if (typeof id === "undefined") { id = "^^noId"; } return super.off(eventName, id, func); } } const myClass = new MyClass(); let emitCount = 0; // Register id-based listener myClass.on("moo", "testId", () => { emitCount++; }); // Register non-id-based listener myClass.on("moo", () => { emitCount++; }); assert.strictEqual(myClass._eventListeners?.moo["testId"].length, 1, "Listener registered on event"); myClass.emitId("moo", "testId", "foo"); assert.strictEqual(emitCount, 1, "Event fired correctly"); myClass.off("moo", "testId"); assert.strictEqual(!myClass._eventListeners?.moo || !myClass._eventListeners?.moo["testId"] || myClass._eventListeners?.moo["testId"].length === 0, true, "Listeners all removed from event"); assert.strictEqual(myClass._eventListeners?.moo && myClass._eventListeners?.moo["^^noId"] && myClass._eventListeners?.moo["^^noId"].length === 1, true, "Global listener still there"); assert.strictEqual(!myClass._eventListeners?.moo || !myClass._eventListeners?.moo["*"] || myClass._eventListeners?.moo["*"].length === 0, true, "Global listener still there"); myClass.off("moo"); assert.strictEqual(!myClass._eventListeners?.moo || !myClass._eventListeners?.moo["*"] || myClass._eventListeners?.moo["*"].length === 0, true, "Listeners all removed from event"); }); }); describe("emit()", () => { it("Supports type safe usage", async () => { const emitter = new Emitter(); let listenerFiredCount = 0; emitter.on("person", (name) => { return new Promise((resolve) => { setTimeout(() => { listenerFiredCount++; resolve(name); }, 1000); }); }); emitter.on("post", () => { return new Promise((resolve) => { setTimeout(() => { listenerFiredCount++; resolve(); }, 1000); }); }); emitter.on("post", () => { return new Promise((resolve) => { setTimeout(() => { listenerFiredCount++; resolve(); }, 1000); }); }); const time = new Date().getTime(); await Promise.all(emitter.emit("post")); const delta = new Date().getTime() - time; assert.ok(delta > 900, `Delta was not correct, await may not have paused? Delta was: ${delta}`); assert.ok(listenerFiredCount === 2, `listenerFiredCount was not correct, await may not have paused? listenerFiredCount was: ${listenerFiredCount}`); }); it("Supports awaiting async listeners", async () => { const emitter = new Emitter(); let listenerFiredCount = 0; emitter.on("foo", async () => { return new Promise((resolve) => { setTimeout(() => { listenerFiredCount++; resolve(); }, 1000); }); }); emitter.on("foo", async () => { return new Promise((resolve) => { setTimeout(() => { listenerFiredCount++; resolve(); }, 1000); }); }); const time = new Date().getTime(); await Promise.all(emitter.emit("foo")); const delta = new Date().getTime() - time; assert.ok(delta > 900, "Delta was not correct, await may not have paused?"); assert.strictEqual(listenerFiredCount, 2, "Listener fire count was incorect"); }); it("Supports awaiting async and non-async listener return data", async () => { const emitter = new Emitter(); let listenerFiredCount = 0; emitter.on("foo", () => { return new Promise((resolve) => { setTimeout(() => { listenerFiredCount++; resolve("Foo1 Return Value"); }, 1000); }); }); emitter.on("foo", () => { return "Foo2 Return Value"; }); const time = new Date().getTime(); const results = await Promise.all(emitter.emit("foo")); const delta = new Date().getTime() - time; assert.ok(delta > 900, "Delta was not correct, await may not have paused?"); assert.ok(listenerFiredCount === 1, "listenerFiredCount was not correct, await may not have been used?"); assert.strictEqual(results[0], "Foo1 Return Value", "Return data for foo1 was not correct"); assert.strictEqual(results[1], "Foo2 Return Value", "Return data for foo2 was not correct"); }); it("Supports awaiting async and non-async rpc listener return data", async () => { const emitter = new Emitter(); let listenerFiredCount = 0; emitter.on("foo1", () => { return new Promise((resolve) => { setTimeout(() => { listenerFiredCount++; resolve("Foo1 Return Value"); }, 1000); }); }); emitter.on("foo2", () => { return "Foo2 Return Value"; }); const time = new Date().getTime(); const result1 = await emitter.rpc("foo1"); const result2 = emitter.rpc("foo2"); const delta = new Date().getTime() - time; assert.ok(delta > 900, "Delta was not correct, await may not have paused?"); assert.ok(listenerFiredCount === 1, "listenerFiredCount was not correct, await may not have been used?"); assert.strictEqual(result1, "Foo1 Return Value", "Return data for foo1 was not correct"); assert.strictEqual(result2, "Foo2 Return Value", "Return data for foo2 was not correct"); }); it("Supports responding with a cancellation signal", async () => { const emitter = new Emitter(); let listenerFiredCount = 0; emitter.on("foo", () => { listenerFiredCount++; return "Foo1 Return Value"; }); emitter.on("foo", () => { listenerFiredCount++; // This listener will respond with a cancellation, this should terminate further listener calls return ListenerReturnFlag.cancel; }); emitter.on("foo", () => { listenerFiredCount++; return "Foo3 Return Value"; }); emitter.on("foo", () => { listenerFiredCount++; return "Foo4 Return Value"; }); const results = emitter.emit("foo"); assert.ok(listenerFiredCount === 2, "listenerFiredCount was not correct, await may not have been used?"); assert.strictEqual(results[0], "Foo1 Return Value", "Return data should be a cancel flag"); assert.strictEqual(results[1], ListenerReturnFlag.cancel, "Return data should be a cancel flag"); assert.ok(emitter.didCancel(results), "didCancel should detect a cancel flag"); }); }); describe("off()", () => { it("Cancels all listeners (event)", () => { const myClass = new Emitter(); const listener1 = () => { }; const listener2 = () => { }; myClass.on("moo", () => { }); assert.strictEqual(myClass._eventListeners?.moo["*"].length, 1, "Listeners registered on event"); myClass.on("moo", "testId", () => { }); assert.strictEqual(myClass._eventListeners?.moo["testId"].length, 1, "Listeners registered on event"); myClass.on("moo", listener1); assert.strictEqual(myClass._eventListeners?.moo["*"].length, 2, "Listeners registered on event"); myClass.on("moo", "testId", listener2); assert.strictEqual(myClass._eventListeners?.moo["testId"].length, 2, "Listeners registered on event"); myClass.off("moo"); assert.strictEqual(!myClass._eventListeners?.moo || (myClass._eventListeners?.moo["*"].length === 0 && myClass._eventListeners?.moo["testId"].length === 0), true, "Listeners all removed from event"); }); it("Cancels id-based listeners (event, id)", () => { const myClass = new Emitter(); const listener1 = () => { }; const listener2 = () => { }; myClass.on("moo", () => { }); assert.strictEqual(myClass._eventListeners?.moo["*"].length, 1, "Listeners registered on event"); myClass.on("moo", "testId", () => { }); assert.strictEqual(myClass._eventListeners?.moo["testId"].length, 1, "Listeners registered on event"); myClass.on("moo", listener1); assert.strictEqual(myClass._eventListeners?.moo["*"].length, 2, "Listeners registered on event"); myClass.on("moo", "testId", listener2); assert.strictEqual(myClass._eventListeners?.moo["testId"].length, 2, "Listeners registered on event"); myClass.off("moo", "testId"); assert.strictEqual(myClass._eventListeners?.moo["*"].length === 2, true, "Listeners all removed from event"); assert.strictEqual(!myClass._eventListeners?.moo["testId"] || myClass._eventListeners?.moo["testId"].length === 0, true, "Listeners all removed from event"); }); it("Cancels listener-based listeners (event, listener)", () => { const myClass = new Emitter(); const listener1 = () => { }; const listener2 = () => { }; myClass.on("moo", () => { }); assert.strictEqual(myClass._eventListeners?.moo["*"].length, 1, "Listeners registered on event"); myClass.on("moo", "testId", () => { }); assert.strictEqual(myClass._eventListeners?.moo["testId"].length, 1, "Listeners registered on event"); myClass.on("moo", listener1); assert.strictEqual(myClass._eventListeners?.moo["*"].length, 2, "Listeners registered on event"); myClass.on("moo", "testId", listener2); assert.strictEqual(myClass._eventListeners?.moo["testId"].length, 2, "Listeners registered on event"); myClass.off("moo", listener1); assert.strictEqual(myClass._eventListeners?.moo["*"].length === 1, true, "Listeners all removed from event"); assert.strictEqual(myClass._eventListeners?.moo["testId"].length === 2, true, "Listeners all removed from event"); }); it("Cancels id-based + listener-based listeners (event, id, listener)", () => { const myClass = new Emitter(); const listener1 = () => { }; const listener2 = () => { }; myClass.on("moo", () => { }); assert.strictEqual(myClass._eventListeners?.moo["*"].length, 1, "Listeners registered on event"); myClass.on("moo", "testId", () => { }); assert.strictEqual(myClass._eventListeners?.moo["testId"].length, 1, "Listeners registered on event"); myClass.on("moo", listener1); assert.strictEqual(myClass._eventListeners?.moo["*"].length, 2, "Listeners registered on event"); myClass.on("moo", "testId", listener2); assert.strictEqual(myClass._eventListeners?.moo["testId"].length, 2, "Listeners registered on event"); myClass.off("moo", "testId", listener2); assert.strictEqual(myClass._eventListeners?.moo["*"].length === 2, true, "Listeners all removed from event"); assert.strictEqual(myClass._eventListeners?.moo["testId"].length === 1, true, "Listeners all removed from event"); }); }); describe("emitStatic()", () => { it("Static emitter will emit when a new listener is added", (resolve) => { class MyClass extends Emitter { } expectAssertionCount(1); const myClass = new MyClass(); myClass.emitStatic("moo"); myClass.on("moo", () => { countAssertion(); assert.ok(true, "Callback was fired"); resolve(); }); }); }); describe("cancelStatic()", () => { it("Removes static emitter", (resolve) => { class MyClass extends Emitter { } expectAssertionCount(1); const myClass = new MyClass(); myClass.emitStatic("moo"); myClass.on("moo", () => { countAssertion(); assert.ok(true, "Callback was fired"); }); myClass.cancelStatic("moo"); myClass.on("moo", () => { countAssertion(); assert.ok(false, "Callback was fired"); }); setTimeout(() => { resolve(); }, 10); }); }); describe("overwrite()", () => { it("Only fires the last listener added, cancelling all other listeners before it", (resolve) => { class MyClass extends Emitter { } expectAssertionCount(1); const myClass = new MyClass(); myClass.on("moo", () => { countAssertion(); assert.ok(false, "Correct callback was fired"); }); myClass.on("moo", () => { countAssertion(); assert.ok(false, "Correct callback was fired"); }); myClass.overwrite("moo", () => { countAssertion(); assert.ok(true, "Correct callback was fired"); }); myClass.emit("moo"); setTimeout(() => { resolve(); }, 10); }); it("Only fires the last listener added (with id), cancelling all other listeners before it", (resolve) => { class MyClass extends Emitter { } expectAssertionCount(2); const myClass = new MyClass(); // Define a listener without an id, this should not be overwritten myClass.on("moo", () => { countAssertion(); assert.ok(true, "Correct callback was fired"); }); // Define some listeners WITH ids, these should be overwritten myClass.on("moo", "1234", () => { countAssertion(); assert.ok(false, "Correct callback was fired"); }); myClass.on("moo", "1234", () => { countAssertion(); assert.ok(false, "Correct callback was fired"); }); // Overwrite the id listeners myClass.overwrite("moo", "1234", () => { countAssertion(); assert.ok(true, "Correct callback was fired"); }); myClass.emitId("moo", "1234"); setTimeout(() => { resolve(); }, 50); }); }); describe("emitId()", () => { it("Fires an id-based event first followed by a global event", () => { const myClass = new Emitter(); let emitCount = 0; // Register id-based listener myClass.on("moo", "testId", () => { emitCount++; }); // Register non-id-based listener myClass.on("moo", () => { emitCount++; }); assert.strictEqual(myClass._eventListeners?.moo["testId"].length, 1, "Listener registered on event"); myClass.emitId("moo", "testId", "foo"); assert.strictEqual(emitCount, 2, "Event fired correctly"); myClass.off("moo", "testId"); assert.strictEqual(!myClass._eventListeners?.moo || !myClass._eventListeners?.moo["testId"] || myClass._eventListeners?.moo["testId"].length, true, "Listeners all removed from event"); assert.strictEqual(myClass._eventListeners?.moo && myClass._eventListeners?.moo["*"] && myClass._eventListeners?.moo["*"].length === 1, true, "Global listener still there"); myClass.off("moo"); assert.strictEqual(!myClass._eventListeners?.moo || !myClass._eventListeners?.moo["*"] || myClass._eventListeners?.moo["*"].length === 0, true, "Listeners all removed from event"); }); }); describe("Global listener functionality", () => { it("Listens for any event emitted and fires the listener with the onAny() function", () => { const emitter = new Emitter(); const spy = jest.fn(); emitter.onAny(spy); emitter.emit("someEvent", "someArg1", true, false); expect(spy).toHaveBeenCalledWith("someEvent", "someArg1", true, false); }); it(`Listens for any event emitted and fires the listener with the on("*") function`, () => { const emitter = new Emitter(); const spy = jest.fn(); emitter.on("*", spy); emitter.emit("someEvent", "someArg1", true, false); expect(spy).toHaveBeenCalledWith("someEvent", "someArg1", true, false); }); it(`Cancels an event listener for any event emitted with the offAny() function`, () => { const emitter = new Emitter(); const spy = jest.fn(); emitter.on("*", spy); emitter.offAny(spy); emitter.emit("someEvent", "someArg1", true, false); expect(spy).toHaveBeenCalledTimes(0); }); it(`Cancels an event listener for any event emitted with the off("*") function`, () => { const emitter = new Emitter(); const spy = jest.fn(); emitter.on("*", spy); emitter.off("*", spy); emitter.emit("someEvent", "someArg1", true, false); expect(spy).toHaveBeenCalledTimes(0); }); }); });