als-unhooked
    Preparing search index...

    als-unhooked

    Asynchronous Local Storage ( UN-Hooked )

    This is a fork of cls-hooked using AsyncLocalStorage instead of async_hooks.

    Asynchronous context tracking has come a long way in the last decade. AsyncLocalStorage, the official stable implementation of async_hooks, provides many optimizations we can benefit from. And with the introduction of AsyncContextFrame in v22, async_hooks can be left behind completely. Therefore, this package provides a drop-in replacement for cls-hooked based on AsyncLocalStorage, so it can continue to benefit from ongoing development by the Node.js team.

    API Docs

    One common use case is tracking a user for the duration of an http request. Suppose you use a middleware to authenticate your users:

    // app.js
    import express from 'express'
    import als from 'als-unhooked/legacy';

    const app = express();
    const namespace = als.createNamespace('user_session');

    // enter the namespace context
    app.use((req, res, next) => {
    namespace.run(() => {
    next();
    });
    });

    // auth middleware
    app.use(async (req, res, next) => {
    const user = await db.getUserFromReq(req);
    namespace.set('user', user);
    next();
    });

    Later on in the lifetime of the request, you need to record which user created some database record:

    // thing.module.js

    import als from 'als-unhooked/legacy';
    import db from './lib/db.js';

    const namespace = als.getNamespace('user_session');

    async function createThing(_thing) {
    _thing.createdBy = namespace.get('user').id;
    await db.thing.create(_thing);
    }

    A major motivator in creating this package was for use with sequelize v6, which uses cls-hooked for automatic transaction passing. This package is not officially supported by sequelize at this time, but the Modern API has been designed to be compatible with sequelize's implementation of cls-hooked.

    // app.js

    import { Sequelize } from 'sequelize';
    import ALS from 'als-unhooked/modern';

    Sequelize.useCLS(new ALS());

    The Legacy API is, of course, also compatible with sequelize. But the modern API is recommended as it is more performant.

    If you don't use cls-hooked directly, but one of your (sub-)dependencies does, you can add an override to your package.json:

    // package.json
    {
    // ...
    "overrides": {
    "cls-hooked": "npm:als-unhooked"
    }
    }

    The "overrides" field works with npm and bun. If you use yarn or pnpm, the syntax is slightly different:

    With yarn:

    // package.json
    {
    // ...
    "resolutions": {
    "cls-hooked": "npm:als-unhooked"
    }
    }

    With pnpm:

    // pnpm-workspace.yaml
    overrides:
    "cls-hooked": "npm:als-unhooked"

    TL;DR If you're using sequelize v6 or just want a nice AsyncLocalStorage implementation, use the modern API. If you need a drop-in replacement for cls-hooked for use with something other than sequelize, you probably want the Legacy API.

    The package has 2 different APIs: modern and legacy. The legacy API is a total drop-in replacement for cls-hooked. In accomplishing this, it creates overhead that, while minimal, can be avoided for many applications. It also inherits some mostly-outdated design patterns, such as process-level global variables. To eliminate these, a minimal wrapper of AsyncLocalStorage was created to be:

    1. Easy to use
    2. Compatible with sequelize

    JS benchmarks should always be taken with a grain of salt. That said, the following results should hopefully show that both the the Modern API and Legacy API of als-unhooked are more performant than cls-hooked. Further, the Modern API is generally more performant than the Legacy API.

    benchmark

    Note: Only the average run times are shown. See benchmark/_exec.js for more details on how the benchmarks are run.

    Note 2: The Modern API was retrofitted with some functionality from the legacy interface for the purpose of these tests.

    Listeners triggered by event-driven classes like EventEmitter will sometimes be called in a different execution context, resulting in so-called "context loss." To mitigate this, cls-hooked provides the Namespace#bindEmitter method, which uses emitter-listener to monkeypatch the EventEmitter, automatically binding every listener to the current Namespace context. This method is convenient, and may even be necessary in some cases. In most cases, however, avoiding context loss is as simple is binding the listener before adding it to the emitter:

    namespace.run(() => {
    // ⬇️ bound. Retains current context
    eventEmitter.on('my_event', namespace.bind(() => {
    namespace.get('color'); // === 'red'
    }));
    // ⬇️ NOT bound. Receives context in which emit() is called.
    eventEmitter.on('my_event', () => {
    namespace.get('color'); // === 'blue'
    });

    // set value for bound handler
    namespace.set('color', 'red');

    // enter new context
    namespace.run(() => {
    // set value for unbound handler
    namespace.set('color', 'blue');

    // emit
    eventEmitter.emit('my_event');
    });
    });

    If you determine that you do need to use Namespace#bindEmitter, be sure to install emitter-listener, as it is not installed alongside als-unhooked by default.

    Thanks to @jeff-lewis for cls-hooked, and to the many others who have contributed to async context tracking in node over the years.

    See LICENSE for the details of the BSD 2-clause "simplified" license used by als-unhooked.