Browse Source

feat: add HookRegistry for ordered handler dispatch

Michael Henke 1 month ago
parent
commit
c98c84c41b
2 changed files with 80 additions and 0 deletions
  1. 37 0
      src/hooks/hook-registry.test.ts
  2. 43 0
      src/hooks/hook-registry.ts

+ 37 - 0
src/hooks/hook-registry.test.ts

@@ -0,0 +1,37 @@
+import { describe, expect, test } from 'bun:test';
+import { HookRegistry } from './hook-registry';
+
+describe('HookRegistry', () => {
+  test('dispatch runs handlers in registration order', async () => {
+    const r = new HookRegistry();
+    const order: number[] = [];
+    r.register('test', async () => {
+      order.push(1);
+    });
+    r.register('test', async () => {
+      order.push(2);
+    });
+    await r.dispatch('test', {}, {});
+    expect(order).toEqual([1, 2]);
+  });
+
+  test('unregistered hook point is no-op', async () => {
+    const r = new HookRegistry();
+    await r.dispatch('none', {}, {});
+  });
+
+  test('handlers returns empty for unregistered point', () => {
+    const r = new HookRegistry();
+    expect(r.handlers('x')).toEqual([]);
+  });
+
+  test('dispatch passes input and output to handlers', async () => {
+    const r = new HookRegistry();
+    const captured: unknown[] = [];
+    r.register('test', async (i, o) => {
+      captured.push(i, o);
+    });
+    await r.dispatch('test', { a: 1 }, { b: 2 });
+    expect(captured).toEqual([{ a: 1 }, { b: 2 }]);
+  });
+});

+ 43 - 0
src/hooks/hook-registry.ts

@@ -0,0 +1,43 @@
+export class HookRegistry {
+  #handlers = new Map<
+    string,
+    Array<(input: unknown, output: unknown) => Promise<void>>
+  >();
+  #firedHookPoints = new Set<string>();
+
+  register(
+    hookPoint: string,
+    handler: (input: unknown, output: unknown) => Promise<void>,
+  ): void {
+    if (this.#firedHookPoints.has(hookPoint)) {
+      console.warn(
+        `[hook-registry] "${hookPoint}" already dispatched; late registration may miss events`,
+      );
+    }
+    const group = this.#handlers.get(hookPoint);
+    if (group) {
+      group.push(handler);
+    } else {
+      this.#handlers.set(hookPoint, [handler]);
+    }
+  }
+
+  async dispatch(
+    hookPoint: string,
+    input: unknown,
+    output: unknown,
+  ): Promise<void> {
+    this.#firedHookPoints.add(hookPoint);
+    const group = this.#handlers.get(hookPoint);
+    if (!group) return;
+    for (const handler of group) {
+      await handler(input, output);
+    }
+  }
+
+  handlers(
+    hookPoint: string,
+  ): ReadonlyArray<(input: unknown, output: unknown) => Promise<void>> {
+    return this.#handlers.get(hookPoint) ?? [];
+  }
+}