import { expect, test, vi } from "vitest" import type { BuildEvents } from "#Source/event/index.ts" import { EventManager } from "#Source/event/index.ts" type TestEvents = BuildEvents<{ ready: (value: number) => void | Promise done: (label: string) => void | Promise }> const createEventManager = (): EventManager => { return new EventManager() } test("EventManager.has reports whether a subscriber is registered", () => { const eventManager = createEventManager() const subscriber = (value: number): void => { void value } expect(eventManager.has("ready", subscriber)).toBe(false) eventManager.subscribe("ready", subscriber) expect(eventManager.has("ready", subscriber)).toBe(true) eventManager.unsubscribe("ready", subscriber) expect(eventManager.has("ready", subscriber)).toBe(false) }) test("EventManager.subscribe returns a stable entry and keeps regular subscribers active", () => { const eventManager = createEventManager() const values: number[] = [] const subscriber = (value: number): void => { values.push(value) } const firstEntry = eventManager.subscribe("ready", subscriber) const duplicateEntry = eventManager.subscribe("ready", subscriber) expect(duplicateEntry).toBe(firstEntry) expect(firstEntry.once).toBe(false) expect(eventManager.emit("ready", 1)).toBe(true) expect(eventManager.emit("ready", 2)).toBe(true) expect(values).toEqual([1, 2]) }) test("EventManager.subscribeOnce removes the subscriber after the first emission", () => { const eventManager = createEventManager() const values: number[] = [] const subscriber = (value: number): void => { values.push(value) } const entry = eventManager.subscribeOnce("ready", subscriber) expect(entry.once).toBe(true) expect(eventManager.emit("ready", 1)).toBe(true) expect(eventManager.emit("ready", 2)).toBe(false) expect(values).toEqual([1]) expect(eventManager.has("ready", subscriber)).toBe(false) }) test("EventManager.unsubscribe removes only the specified subscriber", () => { const eventManager = createEventManager() const values: number[] = [] const keptSubscriber = (value: number): void => { values.push(value) } const removedSubscriber = (value: number): void => { values.push(value * 10) } eventManager.subscribe("ready", keptSubscriber) eventManager.subscribe("ready", removedSubscriber) expect(eventManager.unsubscribe("ready", removedSubscriber)).toBe(eventManager) expect(eventManager.emit("ready", 2)).toBe(true) expect(values).toEqual([2]) }) test("EventManager.removeSubscribersOfEvent clears subscribers of only one event", () => { const eventManager = createEventManager() const doneValues: string[] = [] eventManager.subscribe("ready", () => { throw new Error("should-not-run") }) eventManager.subscribe("done", (label) => { doneValues.push(label) }) expect(eventManager.removeSubscribersOfEvent("ready")).toBe(eventManager) expect(eventManager.emit("ready", 1)).toBe(false) expect(eventManager.emit("done", "kept")).toBe(true) expect(doneValues).toEqual(["kept"]) }) test("EventManager.removeAllSubscribers clears every event subscription", () => { const eventManager = createEventManager() eventManager.subscribe("ready", () => undefined) eventManager.subscribe("done", () => undefined) expect(eventManager.removeAllSubscribers()).toBe(eventManager) expect(eventManager.emit("ready", 1)).toBe(false) expect(eventManager.emit("done", "done")).toBe(false) }) test("EventManager.emit invokes subscribers synchronously and handles sync errors", () => { const handledErrors: unknown[] = [] const eventManager = new EventManager({ onSubscriberError: (_subscriberEntry, error): void => { handledErrors.push(error) }, }) const callOrder: string[] = [] const onceSubscriber = (): void => { callOrder.push("once") throw new Error("once-failed") } eventManager.subscribe("ready", (value) => { callOrder.push(`regular-${value}`) }) eventManager.subscribeOnce("ready", onceSubscriber) eventManager.subscribe("done", () => { throw new Error("sync-failed") }) expect(eventManager.emit("ready", 1)).toBe(true) expect(eventManager.emit("ready", 2)).toBe(true) expect(callOrder).toEqual(["regular-1", "once", "regular-2"]) expect(eventManager.has("ready", onceSubscriber)).toBe(false) expect(eventManager.emit("done", "alpha")).toBe(true) expect(handledErrors).toHaveLength(2) const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined) try { const unmanagedEventManager = createEventManager() unmanagedEventManager.subscribe("done", () => { throw new Error("unhandled") }) expect(unmanagedEventManager.emit("done", "beta")).toBe(true) expect(consoleErrorSpy).toHaveBeenCalledTimes(1) } finally { consoleErrorSpy.mockRestore() } }) test("EventManager.emitAsync awaits subscribers in registration order and handles async errors", async () => { const handledErrors: unknown[] = [] const eventManager = new EventManager({ onSubscriberError: (_subscriberEntry, error): void => { handledErrors.push(error) }, }) const orderedSteps: string[] = [] // oxlint-disable-next-line require-await const onceSubscriber = async (): Promise => { throw new Error("async-failed") } eventManager.subscribe("ready", async (value) => { orderedSteps.push(`first-start-${value}`) await Promise.resolve() orderedSteps.push(`first-end-${value}`) }) eventManager.subscribe("ready", async (value) => { orderedSteps.push(`second-start-${value}`) await Promise.resolve() orderedSteps.push(`second-end-${value}`) }) eventManager.subscribeOnce("ready", onceSubscriber) expect(await eventManager.emitAsync("ready", 7)).toBe(true) expect(orderedSteps).toEqual(["first-start-7", "first-end-7", "second-start-7", "second-end-7"]) expect(handledErrors).toHaveLength(1) expect(eventManager.has("ready", onceSubscriber)).toBe(false) }) test("EventManager.emitConcurrentAsync awaits subscribers concurrently and handles async errors", async () => { const handledErrors: unknown[] = [] const eventManager = new EventManager({ onSubscriberError: (_subscriberEntry, error): void => { handledErrors.push(error) }, }) const orderedSteps: string[] = [] let resolveFirst = (): void => undefined let resolveSecond = (): void => undefined const firstPromise = new Promise((resolve) => { resolveFirst = resolve }) const secondPromise = new Promise((resolve) => { resolveSecond = resolve }) eventManager.subscribe("ready", async () => { orderedSteps.push("first-start") await firstPromise orderedSteps.push("first-end") }) eventManager.subscribe("ready", async () => { orderedSteps.push("second-start") await secondPromise orderedSteps.push("second-end") }) const emitPromise = eventManager.emitConcurrentAsync("ready", 8) await Promise.resolve() expect(orderedSteps).toEqual(["first-start", "second-start"]) resolveSecond() await Promise.resolve() resolveFirst() expect(await emitPromise).toBe(true) expect(orderedSteps).toEqual(["first-start", "second-start", "second-end", "first-end"]) eventManager.removeAllSubscribers() orderedSteps.length = 0 eventManager.subscribe("ready", async () => { orderedSteps.push("third-start") await Promise.resolve() orderedSteps.push("third-end") }) eventManager.subscribe("ready", async () => { orderedSteps.push("fourth-start") await Promise.resolve() throw new Error("concurrent-failed") }) expect(await eventManager.emitConcurrentAsync("ready", 9)).toBe(true) expect(orderedSteps).toEqual(["third-start", "fourth-start", "third-end"]) expect(handledErrors).toHaveLength(1) })