Skip to content

feat(foundry): Hook on/off/emit, single registry, server:start hook - #17

Merged
3m1n3nc3 merged 2 commits into
mainfrom
feat/hook-emit-server-start
Oct 21, 2026
Merged

3m1n3nc3 merged 2 commits into
mainfrom
feat/hook-emit-server-start

Conversation

@3m1n3nc3

@3m1n3nc3 3m1n3nc3 commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Summary

Reworks Hook into an on / off / emit API, adds a server:start hook to both drivers (with a tunneled position), and fixes several hook bugs. The biggest one meant middleware:auth hooks registered as documented never ran on Express.

Hook API

Hook.on('server:start', { tunneled: ({ tunnelUrl }) => registerWebhook(`${tunnelUrl}/api/webhooks`) })
await Hook.emit('middleware:auth', 'before', { req, res })
Hook.off('server:start', 'tunneled')
  • on(name, positions) registers and merges positions. off(name?, pos?) removes. all() lists.
  • emit(name, pos, ...args) calls a position and returns its result: a Promise for async hooks, unlike EventEmitter.emit()'s boolean. It's a no-op when nothing is registered, so call sites drop the if (Hook.has(...)) guard.
  • get() only retrieves now. Previously, get(name, pos, ...args) called the hook when given arguments, so a zero-argument hook couldn't be triggered through it.
  • Deprecated aliases keep working: set → on, unset → off, getAll → all, and calling through get → emit.

server:start hook

Both drivers emit server:start once the server is listening, after the startup banner and once any tunnel is open. It fires in development and production.

  • after always runs, with ServerStartInfo: host, port, scheme, secure, a browsable url, and tunnelUrl.
  • tunneled runs next, only when a tunnel is open, with TunneledServerStartInfo (where tunnelUrl: string).
  • tunnelUrl is the URL the tunnel actually opened. The original draft read TUNNEL_URL before ngrok set it, so it was undefined under ark dev -t.
  • Failures: each position is isolated. A failing listener is logged, never stops the server, and doesn't skip the other position.
  • The types are declared once in @arkstack/contract, replacing per-driver dev:server declarations.

Fixes

Each has a test that I confirmed fails on the previous code.

  1. Two Hook registries. @arkstack/common shipped its own copy of Hook (a separate static Map). The Express auth middleware read common's, so middleware:auth hooks registered via @arkstack/foundry (as the docs say) never ran on Express. common now re-exports foundry's Hook and types.
  2. shutdown checked for a before hook but called after.
  3. unset(name) with an unregistered name cleared every hook.
  4. H3's auth middleware didn't await its hooks; Express did.
  5. A throwing Express after middleware exited the process. Logger.error exits by default, and the post-response handler didn't pass exit = false.

Breaking

TypeScript doesn't merge module augmentations through a re-export. Code that declared typed hook names by augmenting HookRegistry on @arkstack/common must move that to @arkstack/foundry; the common augmentation now silently does nothing. arcstack's own declarations are moved. This is noted in the CHANGELOG and the hooks guide.

Tests & docs

  • New tests cover:
    • emit with arguments, with none, async, and on missing hooks;
    • the deprecated aliases, off, and the shared registry;
    • server:start on both drivers (local url, opened vs preset tunnel, banner-then-hook order, tunneled firing/not firing, failure isolation), using a fake app / mocked serve() and mocked ngrok, with no real ports;
    • the after-middleware crash.
  • Full suite: 621 passed. Lint and Prettier are clean, with no new typecheck errors vs main.
  • Docs: hooks guide rewritten (API, typed hooks, built-in hooks table, server:start, deprecations); foundry README holds the API; common README points to it; overview and auth guide updated; CHANGELOG under Unreleased.

This branch also carries ee31e839 style(catalog): standardize quotes in catalog entries.

Hook API
- `Hook.on(name, positions)` registers (merging positions),
  `Hook.off(name?, pos?)` removes, and `Hook.emit(name, pos, ...args)`
  calls a position and returns its result (a Promise for async hooks).
  `emit` is a no-op when nothing is registered, so call sites drop the
  `if (Hook.has(...))` guard. `Hook.all()` lists hooks.
- `get()` only retrieves now. Deprecated aliases keep working: `set` ->
  `on`, `unset` -> `off`, `getAll` -> `all`, and `get(name, pos, ...args)`
  (which called the hook) -> `emit`.

server:start hook
- The Express and H3 drivers emit `server:start` once listening, after
  the startup banner and any tunnel: `after` always, then `tunneled` only
  when a tunnel is open. Payloads are `ServerStartInfo` /
  `TunneledServerStartInfo` from @arkstack/contract (host, port, scheme,
  secure, browsable url, tunnelUrl). `tunnelUrl` is the URL the tunnel
  actually opened. A failing listener is logged, never stops the server,
  and doesn't skip the other position.

Fixes
- @arkstack/common shipped its own copy of Hook, a second registry, so
  hooks registered through one import never fired for code reading the
  other. The Express auth middleware read common's, so `middleware:auth`
  hooks registered as documented (via foundry) never ran on Express.
  common now re-exports foundry's Hook and types. Typed hook names must
  be declared by augmenting HookRegistry on @arkstack/foundry.
- `shutdown` checked for a `before` hook but called `after`.
- `unset(name)` for an unregistered name cleared every hook.
- H3's auth middleware now awaits its hooks, as Express already did.
- A failing Express `after` middleware exited the process: Logger.error
  exits by default, and the post-response handler didn't pass false.
@3m1n3nc3
3m1n3nc3 merged commit dcee422 into main Oct 21, 2026
3 checks passed
@codecov

codecov Bot commented Oct 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.82759% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
packages/common/src/network.ts 0.00% 2 Missing ⚠️
packages/common/src/lifecycle.ts 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant