unctx

repository·main·Indexed 20 days ago

https://github.com/unjs/unctx

A library that implements the Composition API pattern in vanilla JavaScript, allowing developers to organize complex logic using reusable functions that access a shared context without manual prop drilling. It provides utilities for context creation, global namespaces, and native async context support via AsyncLocalStorage in Node.js. Version 3.0.0 includes a transform API and unplugin integration for build tools like Vite, Rollup, and Webpack.

Tokens
4.9K
Snippets
16
Records
18
Agent score
70%

What's inside unctx

  1. How Async Context works and its limitations

    main

    By default, unctx context is only available in synchronous execution and only before the first await statement. This ensures that context is not accidentally shared between concurrent asynchronous calls.

    If you attempt to use the context after an await or inside a setTimeout, it will return null (or throw if using use).

    Workaround: Cache the context in a local variable at the start of your function:

    async function setup() {
      const ctx = useAwesome(); // Cache the context immediately
      await new Promise((resolve) => setTimeout(resolve, 1000));
      console.log(ctx); // Still works because it's a local variable
    }
    async function setup() {
      console.log(useAwesome()); // Returns context
      setTimeout(() => {
        console.log(useAwesome());
      }, 1); // Returns null
      await new Promise((resolve) => setTimeout(resolve, 1000));
      console.log(useAwesome()); // Returns null
    }
  2. Implement the Composition API pattern with createContext

    main

    You can implement a pattern similar to Vue's Composition API by creating a context. This allows functions to access a shared instance without explicitly passing it through every function call.

    1. Create a context using createContext().
    2. Export a use function (e.g., ctx.use) for consumers to access the context.
    3. Use ctx.call(instance, callback) to wrap the logic where the context should be available. Any function called within the callback can then use the exported use function to retrieve the instance.

    Note: ctx.use throws an error if no context is present. Use ctx.tryUse if you want a tolerant version that returns null instead of throwing.

    import { createContext } from "unctx";
    
    const ctx = createContext();
    
    export const useAwesome = ctx.use;
    
    // ...
    ctx.call({ test: 1 }, () => {
      // Any function called here can use `useAwesome` to get { test: 1 }
    });
  3. Avoid Context Conflict errors

    main

    A Context conflict error occurs if you attempt to run nested ctx.call() operations with different instances. You should only have one call() running at a time for a specific context.

    Example of what causes an error:

    ctx.call({ test: 1 }, () => {
      ctx.call({ test: 2 }, () => {
        // Throws error!
      });
    });
  4. Configure the unctx Transform API

    main

    If you are building your own tool or not using a standard bundler, you can use the standalone transformer from unctx/transform. It requires magic-string and an oxc parser (oxc-parser or rolldown).

    Transformer Options

    OptionDefaultDescription
    asyncFunctions["withAsyncContext"]Function names whose arguments should be transformed. Add "callAsync" to transform ctx.callAsync usages.
    helperModule"unctx"Module the async helper is imported from.
    helperName"executeAsync"Name of the async helper exported by helperModule.
    objectDefinitions{}Object properties to transform, keyed by the defining function. e.g. { defineMeta: ["middleware"] } transforms the middleware key passed to defineMeta.

    Usage Example

    import { createTransformer } from "unctx/transform";
    
    const transformer = await createTransformer({
      // asyncFunctions: ["withAsyncContext"],
    });
    
    const result = transformer.transform(code);
    
    if (result) {
      console.log(result.code); // Transformed code
      console.log(result.magicString.generateMap()); // Source map
    }
    import { createTransformer } from "unctx/transform";
    
    const transformer = await createTransformer({
      // asyncFunctions: ["withAsyncContext"],
      // helperModule: "unctx",
      // helperName: "executeAsync",
      // objectDefinitions: {},
    });
    
    const result = transformer.transform(code);
    
    if (result) {
      console.log(result.code); // Transformed code
      console.log(result.magicString.generateMap()); // Source map
    }
  5. Use Typed Context with TypeScript

    main

    unctx provides generic support for all utilities to enable full TypeScript support for your context/instance types.

    // The return type of useAwesome will be Awesome | null
    const { use: useAwesome } = createContext<Awesome>();
    // Return type of useAwesome is Awesome | null
    const { use: useAwesome } = createContext<Awesome>();
  6. Access context via Namespaces

    main

    To avoid conflicts between different libraries, unctx provides a global namespace mechanism using globalThis. This allows you to access a specific context by a unique key (it is recommended to use your npm package name as the key).

    Use useContext(key) to get a way to access the context, or getContext(key) to retrieve the context directly.

    import { useContext, getContext } from "unctx";
    
    // Access context by a unique key
    const useAwesome = useContext("awesome-lib");
    
    // or
    // const awesomeContext = getContext('awesome-lib')
  7. Implement the Singleton Pattern with unctx

    main

    If you need a shared instance that does not depend on a specific request/call context, you can use ctx.set and ctx.unset to implement a singleton pattern.

    Warning: You cannot combine set with call. Always use unset before replacing an instance to avoid a Context conflict error.

    import { createContext } from "unctx";
    
    const ctx = createContext();
    ctx.set(new Awesome());
    
    export const useAwesome = ctx.use;
    import { createContext } from "unctx";
    const ctx = createContext();
    ctx.set(new Awesome());
    
    // Replacing instance without unset
    // ctx.set(new Awesome(), true)
    
    export const useAwesome = ctx.use;
  8. Enable Native Async Context with AsyncLocalStorage

    main

    In Node.js environments, you can preserve and track async contexts across await boundaries by enabling AsyncLocalStorage.

    To use this, set the asyncContext: true option in createContext. You can also explicitly provide a custom AsyncLocalStorage implementation.

    import { createContext } from "unctx";
    import { AsyncLocalStorage } from "node:async_hooks";
    
    const ctx = createContext({
      asyncContext: true,
      AsyncLocalStorage,
    });
    
    ctx.call("123", () => {
      setTimeout(() => {
        // Prints 123 even after the timeout
        console.log(ctx.use());
      }, 100);
    });
    import { createContext } from "unctx";
    
    const ctx = createContext({ asyncContext: true });
    
    ctx.call("123", () => {
      setTimeout(() => {
        // Prints 123
        console.log(ctx.use());
      }, 100);
    });
  9. Use Async Transform for build-time context restoration

    main

    If your environment doesn't support native async context (like most browsers), unctx provides a build-time transform that automatically restores context after await statements. This requires a bundler (Rollup, Vite, or Webpack) and specific peer dependencies.

    1. Install Peer Dependencies

    npx nypm i -D unplugin magic-string oxc-parser
    # or if using rolldown:
    # npx nypm i -D unplugin magic-string rolldown

    2. Register the Plugin

    Import unctxPlugin from unctx/plugin and register it in your bundler configuration (e.g., unctxPlugin.vite() for Vite).

    3. Usage

    Wrap async functions that require context with withAsyncContext from unctx:

    import { withAsyncContext } from "unctx";
    
    const setup = withAsyncContext(async () => {
      console.log(useAwesome()); // Returns context
      await new Promise((resolve) => setTimeout(resolve, 1000));
      console.log(useAwesome()); // Still returns context thanks to the transform!
    });

    Note: If you use ctx.callAsync, you must add `

  10. Use the Transformer API

    main

    A Transformer object returned by createTransformer provides methods to inspect and transform code strings during the build process.

    Methods

    • transform(code: string, options?: { force?: boolean }): { code: string; magicString: MagicString } | undefined Transforms the provided code. If the code does not match the filter or does not contain transformable patterns, it returns undefined. If options.force is true, it bypasses the shouldTransform check.
    • shouldTransform(code: string): boolean A predicate that returns true if the code contains both a target function call (from asyncFunctions or objectDefinitions) and an await expression.
    • filter.code: RegExp The regex used for fast pre-filtering.
    const transformer = await createTransformer();
    const code = 'withAsyncContext(() => { await doSomething(); });';
    
    const result = transformer.transform(code);
    if (result) {
      console.log(result.code);
      // result.magicString is a MagicString instance for source map generation
    }
  11. Get the unctx transform filter

    main

    The getTransformFilter function returns a synchronous regex filter used to quickly determine if a file contains any code that might require transformation. This is useful for build plugins to skip files that do not contain the target function calls, avoiding the overhead of a full AST parse.

    It generates a regex based on the asyncFunctions and the keys in objectDefinitions provided in the TransformerOptions.

    import { getTransformFilter } from 'unctx/transform';
    
    const filter = getTransformFilter({
      asyncFunctions: ['withAsyncContext'],
      objectDefinitions: { defineMeta: ['middleware'] }
    });
    
    // filter.code is a RegExp used to test file contents
    console.log(filter.code);