ProvenPin Install

hookable serial and parallel hook calls: working versions (tested 2026-10-16)

Create a typed hook registry with hookable and drive it with hook, hookOnce, callHook, callHookParallel and removeHook while asserting serial ordering and one-shot hooks offline.

passed5 passed · 0 failed2026-10-16 17:50 UTC
Status
passed
Last passed
2026-10-16 17:50 UTC
Tests
5 / 5
Language
TypeScript
Runtime
node v22.23.3
pinned versions3
hookable@6.1.2@types/node@26.6.5tsx@4.23.15
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38942732901

The code

Exactly the files that passed. Your agent can pull them with get_recipe("hookable-hooks-call") (install), or see them on GitHub.

package.json
{
  "name": "hookable-hooks-call",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "hookable": "6.1.2"
  },
  "devDependencies": {
    "@types/node": "26.6.5",
    "tsx": "4.23.15"
  }
}
src/pipeline.ts
import { createHooks } from "hookable";

export type PipelineHooks = {
  "step:start": (name: string) => void;
  "step:end": (name: string, durationMs: number) => void;
  "pipeline:done": (steps: string[]) => void;
};

export function createPipeline() {
  return createHooks<PipelineHooks>();
}
test/pipeline.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { createPipeline } from "../src/pipeline.ts";

test("callHook runs hooks in registration order", async () => {
  const hooks = createPipeline();
  const order: string[] = [];
  hooks.hook("step:start", () => {
    order.push("a");
  });
  hooks.hook("step:start", () => {
    order.push("b");
  });
  await hooks.callHook("step:start", "compile");
  assert.deepEqual(order, ["a", "b"]);
});

test("callHook awaits async hooks serially", async () => {
  const hooks = createPipeline();
  const order: string[] = [];
  hooks.hook("step:start", async () => {
    await new Promise((resolve) => setTimeout(resolve, 20));
    order.push("slow");
  });
  hooks.hook("step:start", () => {
    order.push("fast");
  });
  await hooks.callHook("step:start", "x");
  assert.deepEqual(order, ["slow", "fast"]);
});

test("callHookParallel awaits all hooks", async () => {
  const hooks = createPipeline();
  const order: string[] = [];
  hooks.hook("step:start", async () => {
    await new Promise((resolve) => setTimeout(resolve, 20));
    order.push("slow");
  });
  hooks.hook("step:start", () => {
    order.push("fast");
  });
  await hooks.callHookParallel("step:start", "x");
  assert.deepEqual(order, ["fast", "slow"]);
});

test("hookOnce fires a single time", async () => {
  const hooks = createPipeline();
  let count = 0;
  hooks.hookOnce("step:end", () => {
    count++;
  });
  await hooks.callHook("step:end", "a", 1);
  await hooks.callHook("step:end", "b", 2);
  assert.equal(count, 1);
});

test("removeHook stops a hook from running", async () => {
  const hooks = createPipeline();
  const calls: string[] = [];
  const fn = (name: string) => {
    calls.push(name);
  };
  hooks.hook("step:start", fn);
  hooks.removeHook("step:start", fn);
  await hooks.callHook("step:start", "x");
  assert.deepEqual(calls, []);
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.