@webfill/async-context

1.0.0 • Public • Published

AsyncContext polyfill for Node.js and the browser

🗺️ An experimental AsyncContext polyfill

⚠️ Experimental API
🎣 Uses Node.js' AsyncLocalStorage if possible
🧅 Works with Bun via Zone.js
🦕 Works with Deno via their Node.js compat layer
🌐 Works in the browser via Zone.js!

Installation

npm Yarn pnpm jsDelivr

This package can be installed locally using npm, Yarn, pnpm, or your other favorite package manager of choice:

npm install @webfill/async-context

If you're using Deno, you can install this package using the new npm: specifiers, or directly from a Deno-compatible npm CDN like esm.sh:

import {} from "npm:@webfill/async-context";
import {} from "https://esm.sh/@webfill/async-context";

If you want to use this package in the browser without needing a build tool to bundle your npm dependencies, you can use an npm CDN like esm.sh or jsDelivr to import it directly from a URL:

import {} from "https://esm.sh/@webfill/async-context";
import {} from "https://esm.run/@webfill/async-context";

Usage

Node.js Deno Browser Bun

This package exports the AsyncContext namespace. To get started, you can create a new AsyncContext.Variable and use it in various places. When you use the .run(value, f) method, it will cascade that value throughout the entire (possibly asynchronous) execution of any subsequent functions. Here's a quick demo:

import AsyncContext from "@webfill/async-context";

const message = new AsyncContext.Variable({ defaultValue: "Hello" });

message.run("Hi", async () => {
  await fetch("https://jsonplaceholder.typicode.com/todos/1");
  console.log(message.get());
  //=> "Hi"
});

message.run("Hey", () => {
  setTimeout(() => {
    console.log(message.get());
    //=> "Hey"
  }, 10);
});

console.log(message.get());
//=> "Hello"

For a more practical example, you could use an AsyncContext.Variable to track a Request's ID across many different asynchronous functions without resorting to "argument drilling":

const id = new AsyncContext.Variable();
let i = 0;
globalThis.addEventListener("fetch", (event) => {
  id.run(++i, () => {
    event.respondWith(handleRequest(event.request));
  });
});

function logError(message) {
  // Note that this is two calls deep in an async chain! Yet we still get the
  // correct ID that was set via 'id.run()'
  console.error(id.get(), message);
  //=> '1' 'Not found'
  //=> '2' 'Not found'
}

async function handleRequest(request) {
  if (request.url === "/") {
    await doThing();
    return new Response(`Hello, ${id.get()} 👋`);
    //=> 'Hello, 1 👋'
    //=> 'Hello, 2 👋'
  } else {
    await doThing();
    logError("Not found");
    return new Response(`${id.get()} not found.`, { status: 404 });
    //=> '1 not found.'
    //=> '2 not found.'
  }
}

Here's the example from the proposal for reference.

const asyncVar = new AsyncContext.Variable();

// Sets the current value to 'top', and executes the `main` function.
asyncVar.run("top", main);

function main() {
  // AsyncContext.Variable is maintained through other platform queueing.
  setTimeout(() => {
    console.log(asyncVar.get()); // => 'top'

    asyncVar.run("A", () => {
      console.log(asyncVar.get()); // => 'A'

      setTimeout(() => {
        console.log(asyncVar.get()); // => 'A'
      }, randomTimeout());
    });
  }, randomTimeout());

  // AsyncContext.Variable runs can be nested.
  asyncVar.run("B", () => {
    console.log(asyncVar.get()); // => 'B'

    setTimeout(() => {
      console.log(asyncVar.get()); // => 'B'
    }, randomTimeout());
  });

  // AsyncContext.Variable was restored after the previous run.
  console.log(asyncVar.get()); // => 'top'

  // Captures the state of all AsyncContext.Variable's at this moment.
  const snapshotDuringTop = new AsyncContext.Snapshot();

  asyncVar.run("C", () => {
    console.log(asyncVar.get()); // => 'C'

    // The snapshotDuringTop will restore all AsyncContext.Variable to their snapshot
    // state and invoke the wrapped function. We pass a function which it will
    // invoke.
    snapshotDuringTop.run(() => {
      // Despite being lexically nested inside 'C', the snapshot restored us to
      // to the 'top' state.
      console.log(asyncVar.get()); // => 'top'
    });
  });
}

function randomTimeout() {
  return Math.random() * 1000;
}

Proposed Solution | Async Context for JavaScript

Package Sidebar

Install

npm i @webfill/async-context

Weekly Downloads

9

Version

1.0.0

License

MIT

Unpacked Size

11.4 kB

Total Files

9

Last publish

Collaborators

  • jcbhmr