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
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.