Skip to content

[fix] resolve the pack scope per environment, not through node:async_hooks - #28

Merged
Thibaut-Fatus merged 1 commit into
mainfrom
fix/packs-browser-scope
Aug 31, 2026
Merged

Thibaut-Fatus merged 1 commit into
mainfrom
fix/packs-browser-scope

Conversation

@Thibaut-Fatus

Copy link
Copy Markdown
Collaborator

Why

packs.ts creates its AsyncLocalStorage from a static node:async_hooks import. The browser reaches that module — Mechanism.listAll() and RiskCategory.listAll() both read Packs.current() — so every client bundle that imports the barrel pulls a node builtin in. Bundlers externalize the builtin to a stub and then fail on the named import, which has broken kora-infra's website build since #27:

"AsyncLocalStorage" is not exported by "__vite-browser-external",
imported by ".../packs/packs.js"

What

The scope now arrives through the #packScope subpath import, resolved by condition in package.json:

  • node/workers (default) — packScope.node.ts, the real AsyncLocalStorage. A server runs several runs concurrently in one isolate, so the active pack still has to follow each one across its awaits.
  • browserpackScope.browser.ts, a save/restore stack with no node import. A page renders against one pack, and Packs.run() is only ever called with a synchronous callback, so the stack is accurate for that shape.

Both conditions point at build output, so the specifier resolves for consumers but not for vitest running from source; the vitest config aliases it back to the node implementation, which also keeps an unbuilt checkout testable.

No API change — Packs is untouched.

Tests

Both implementations run against the same behavior (no store outside run(), store visible inside, enclosing store restored on return and on throw), plus a guard that packs.ts imports no node builtin — nothing else would catch the regression before a downstream client build.

tsbuild, 237 tests and prettier pass; lint unchanged (1 pre-existing warning in cli/reassessCommand.ts).

Downstream

korabench/infra needs the matching imports map in its packages/lib-benchmark wrapper manifest; that lands with the submodule bump on feat/pluggable-taxonomy-packs (infra #188), where yarn app-website:build now exits 0 with no async_hooks in dist/client and the real one still in dist/server.

…hooks

packs.ts created its AsyncLocalStorage from a static `node:async_hooks` import.
The browser reaches that module — Mechanism.listAll() and RiskCategory.listAll()
both read Packs.current() — so every client bundle importing the barrel pulled a
node builtin in. Bundlers externalize the builtin to a stub and then fail on the
named import; kora-infra's website build has been broken since #27:

  "AsyncLocalStorage" is not exported by "__vite-browser-external",
  imported by ".../packs/packs.js"

The scope now comes in through the `#packScope` subpath import, resolved by
condition in package.json:

  - node/workers (default): packScope.node.ts, the real AsyncLocalStorage. A
    server runs several runs concurrently in one isolate, so the active pack
    still has to follow each one across its awaits.
  - browser: packScope.browser.ts, a save/restore stack with no node import. A
    page renders against one pack, and `Packs.run()` is only ever called with a
    synchronous callback, so the stack is accurate for that shape.

Both conditions point at build output, so the specifier resolves for consumers
but not for vitest running from source; the vitest config aliases it back to the
node implementation, which also keeps an unbuilt checkout testable.

Tests cover both implementations against the same behavior — no store outside
run(), the store visible inside, and the enclosing store restored on return and
on throw — plus a guard that packs.ts imports no node builtin, since nothing else
would catch the regression before a downstream client build.

tsbuild, 237 tests and prettier pass; lint unchanged (1 pre-existing warning).
@Thibaut-Fatus
Thibaut-Fatus merged commit 5086c9e into main Aug 31, 2026
4 checks passed
@Thibaut-Fatus
Thibaut-Fatus deleted the fix/packs-browser-scope branch August 31, 2026 08:52
Thibaut-Fatus added a commit that referenced this pull request Aug 31, 2026
#28 resolved the pack scope by condition, and a Cloudflare build asks for
`browser` as well as `workerd`. With `browser` the only condition listed ahead
of `default`, the worker bundle got the stack scope: verified in kora-infra,
where app-website's dist/server/assets/worker-entry-*.js shipped StackScope.

That is the runtime the async scope exists for. A worker serves several runs
concurrently in one isolate, and the stack scope drops the active pack at the
first await — silently, with the right types and green tests, exactly the
mis-binding #27 set out to prevent.

`workerd` now precedes `browser` and answers with the node implementation;
workerd exposes AsyncLocalStorage under nodejs_compat, which all three
kora-infra workers set.

The order is the whole fix and nothing else would catch it losing, so the guard
asserts it directly against the manifest — it fails if the two conditions are
swapped.

tsbuild, 239 tests and prettier pass; lint unchanged (1 pre-existing warning).
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