Skip to content

cellb: lower await on in-browser sites, so AsyncLocalStorage keeps concurrent stores - #10

Merged
mkotelnikov merged 3 commits into
mainfrom
ccr-ab3c0d30-6du8do-browser-als
Oct 27, 2026
Merged

mkotelnikov merged 3 commits into
mainfrom
ccr-ab3c0d30-6du8do-browser-als

Conversation

@mkotelnikov

Copy link
Copy Markdown
Contributor

Problem

On an in-browser site (createProjectSite), modules were served with native await. cellb's node:async_hooks polyfill can only carry a store across an await that goes through Promise.prototype.then. As a result, the site fell back to run-at-a-time stores, and two objects answering at once could read each other's AsyncLocalStorage store. Bundles already avoided this, because bundleWorkerForBrowser lowers await with esbuild.

Change

  • src/project/lower-await.ts (new, built on acorn): lowers async functions to generators driven by a small runner.
    • await X becomes (yield X).

    • for await becomes a loop over yield it.next(). It closes the iterator on early exit and handles labels.

    • Functions keep their this and arguments.

    • Arrows forward arguments only when they read them.

    • Left as they are:

      • async generators;
      • async functions that use super or new.target;
      • top-level await.

      After an await, these see no store rather than another event's.

  • createProjectSite: with nodeCompat, runs webrun-modules' default transform and then lowerAwait, for both the project's modules and loaded Workers.
  • browser-host: installAsyncContext() now runs for every nodejs_compat project, sites included, not only bundles.
  • Docs: gaps.md, browser-host.md, the polyfill's header comment and the flue-example README are updated. A site's nodeCompat must match the project's nodejs_compat flag; this is now documented.

Tests

  • test/p17-lower-await.test.ts: checks that the lowering keeps values, errors, this, arguments and ordering, and covers for await (async and sync iterables, break, labels, closing the iterator). With the polyfill installed, four concurrent runs each keep their own store.
  • site-projects.spec: a new test runs test/async-context-worker.ts (16 objects at once, every kind of continuation) on a site.
    • Without the lowering it fails: stores are lost after the first await.
    • With the lowering it passes.
  • Local runs:
    • Lint and typecheck are clean.
    • Unit tests: 346 passed. The 4 unhandled errors from p09-celld-conformance also occur on main.
    • Chromium: 207 passed and 15 skipped. Two tests failed in that run with "execution context destroyed" because I edited files mid-run and the dev server reloaded the pages. Rerun 3× each, both pass.

Generated by Claude Code

…ncurrent stores

A site's modules (createProjectSite with nodeCompat) now get their async functions lowered to
generators after webrun-modules compiles them (lowerAwait, on acorn), as bundleWorkerForBrowser
does with esbuild. The browser host then installs the polyfill's continuations for a site too:
16 objects answering at once each keep their own store, as on Node and in a bundle.

Left native: async generators, async functions using super or new.target, top-level await;
they see no store after an await rather than another event's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SXJQA3LbggJHi5qZBpUKe8
… lowering otherwise

In a realm with TC39 AsyncContext, AsyncLocalStorage is a layer over AsyncContext.Variable and
Snapshot: the engine carries stores through native awaits, as in workerd, celld and Node, and
installAsyncContext() patches nothing. asyncRoot() starts events in an AsyncContext.Snapshot.
enterWith() has no AsyncContext counterpart and throws there.

Without it the polyfill simulates AsyncContext as before: patched continuations plus lowered
awaits. createProjectSite lowers by default only when the browser lacks AsyncContext
(lowerAwait overrides); bundleWorkerForBrowser takes lowerAwait: false for such browsers.

test/p18 runs the backend on an AsyncContext made of Node's engine-backed AsyncLocalStorage,
with native awaits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SXJQA3LbggJHi5qZBpUKe8
…ync-context

cellb: node:async_hooks on native AsyncContext when the realm has it; lowering otherwise
@mkotelnikov
mkotelnikov merged commit 3c35dd9 into main Oct 27, 2026
1 check passed
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.

2 participants