TC39 AsyncContext · Node.js AsyncLocalStorage
AsyncLocalStorage for the browser
Set a value one time, for example a request ID. All the code of that operation gets the value: after await, in promise callbacks, timers, event listeners and generators.
A Vite plugin or a Babel preset changes your async functions. A small runtime keeps the context. On Node.js, the package uses the native AsyncLocalStorage.
npm install async-browser-contextimport { AsyncLocalStorage } from "async-browser-context"; const requestId = new AsyncLocalStorage(); async function handle(id, user) { await requestId.run(id, async () => { log(`[${requestId.getStore()}] load ${user}`); const name = await fetchUser(user); log(`[${requestId.getStore()}] hello, ${name}`); await save(name); });} async function save(name) { await sleep(5); log(`[${requestId.getStore()}] saved ${name}`);} await Promise.all([ handle("r-1", "ada"), handle("r-2", "grace"),]); - No output yet.
The page starts this code with the real library. Each step is a statement or the end of an await. Open the debugger, or write your own code in the playground.
A global variable is not sufficient
Two requests run at the same time. Each request keeps its ID in a variable, and writes the ID to the log after each await.
With one global variable, the second request changes the value while the first request waits. After the await, the first request writes the ID of the second request. The library keeps one value for each request.
The page starts the two runs when it loads, with the same code. The lines below are the real output.
- ✓
[r-1] load ada - ✓
[r-2] load grace - ✓
[r-2] hello, Grace - ✓
[r-2] saved Grace - ✗
[r-2] hello, Ada - ✗
[r-2] saved Ada
- ✓
[r-1] load ada - ✓
[r-2] load grace - ✓
[r-2] hello, Grace - ✓
[r-2] saved Grace - ✓
[r-1] hello, Ada - ✓
[r-1] saved Ada
How the library operates
- 1
The transform
A native
awaitdoes not tell JavaScript code when the function continues. Thus, the Vite plugin or the Babel preset changes each async function into a generator. Eachawaitbecomes ayield, and the runtime operates the generator.async function handle(id) { const user = await load(id); log(requestId.getStore(), user); }import { coroutine as _coroutine } from "async-browser-context/runtime"; function handle(_x) { return _handle.apply(this, arguments); } function _handle() { _handle = _coroutine(function* (id) { const user = yield load(id); log(requestId.getStore(), user); }); return _handle.apply(this, arguments); } - 2
The frames
Each
run()makes a frame below the current frame. Before each step of a function, the runtime makes the frame of the function current. After the step, it sets the previous frame again.get()searches from the current frame up to the root.get() of requestId searched F2 → F1 and found "r-1" in F1. - 3
The patches
A callback gets the context of the code that registered it. The runtime patches the APIs that call callbacks later. The patches keep the names, the lengths and the source text of the native functions.
- Promises
thencatchfinallyPromise.all - Timers
setTimeoutsetIntervalqueueMicrotaskrequestAnimationFramerequestIdleCallbackscheduler.postTask - Events
addEventListeneron… propertiesMediaQueryList - Observers
MutationObserverResizeObserverIntersectionObserverPerformanceObserverFinalizationRegistry - Streams
ReadableStreamWritableStreamTransformStream - Other callbacks
navigator.lockstoBlobgeolocationstartViewTransitionArray.fromAsync
- Promises
- 4
The boundaries
Code without the transform cannot get the context of a different operation. At most, it gets no context after its own native
await. Where your code gives a callback to that code, one function keeps the context.- Gives a promise that your code awaitsNothing to do
- Starts your callback from a patched APINothing to do
- Starts your callback after its own awaitBind the callback
AsyncLocalStorage.bind(callback) - Keeps your callback in a list for laterBind the callback
AsyncLocalStorage.bind(callback) - Operates in a worker or an iframeSend the values, then
run(value, fn)
What this site holds
Docs
How to install and use the library, the context rules, the API and the guides.
16 pages. Start with:
Explore
Interactive pages that start the real library in this page: timelines, frames, the transform and events.
7 pages. Start with:
Testing
The test strategy, the results of the probes and the mutants, and the benchmarks.
3 pages. Start with: