Userland implementation of continuation-local storage for Node.js, providing thread-local storage functionality for asynchronous callback chains
npx @tessl/cli install tessl/npm-continuation-local-storage@3.2.0Continuation-local storage works like thread-local storage in threaded programming, but is based on chains of Node-style callbacks instead of threads. It allows developers to set and get values that are scoped to the lifetime of chains of function calls, making it particularly useful for maintaining context across asynchronous operations without explicitly passing values through callback parameters.
npm install continuation-local-storageconst cls = require('continuation-local-storage');
// or
const { createNamespace, getNamespace, destroyNamespace, reset } = require('continuation-local-storage');const cls = require('continuation-local-storage');
// Create a namespace
const session = cls.createNamespace('my-session');
// Use in callback chains
function handleRequest(req, res) {
session.run(function() {
// Set values in the context
session.set('user', { id: 123, name: 'Alice' });
session.set('requestId', req.headers['x-request-id']);
// Call other functions - they inherit the context
processRequest();
});
}
function processRequest() {
// Retrieve values from context - no need to pass as parameters
const user = session.get('user');
const requestId = session.get('requestId');
console.log(`Processing request ${requestId} for user ${user.name}`);
}Continuation-local storage is built around several key concepts:
Core functions for creating, retrieving, and managing continuation-local storage namespaces.
/**
* Create a new namespace for continuation-local storage
* @param name - Unique name for the namespace
* @returns Namespace instance
* @throws Error if name is not provided
*/
function createNamespace(name: string): Namespace;
/**
* Look up an existing namespace by name
* @param name - Name of the namespace to retrieve
* @returns Namespace instance or undefined if not found
*/
function getNamespace(name: string): Namespace | undefined;
/**
* Dispose of an existing namespace and remove async listeners
* @param name - Name of the namespace to destroy
* @throws Error if namespace doesn't exist or has no async listener ID
*/
function destroyNamespace(name: string): void;
/**
* Completely reset all continuation-local storage namespaces
* WARNING: Existing namespace references will no longer propagate context
*/
function reset(): void;Methods for setting and getting values within a namespace context.
/**
* Set a value on the current continuation context
* Must be called within an active context created by run() or bind()
* @param key - Key to store the value under
* @param value - Value to store
* @returns The stored value
* @throws Error if no context is available
*/
Namespace.prototype.set(key: string, value: any): any;
/**
* Look up a value on the current continuation context
* Recursively searches from innermost to outermost nested context
* @param key - Key to retrieve the value for
* @returns The stored value or undefined if not found
*/
Namespace.prototype.get(key: string): any;Methods for creating and running code within namespace contexts.
/**
* Create a new context and run function within its scope
* All functions called from the callback inherit this context
* @param fn - Callback function that receives the context as argument
* @returns The context object that was created
* @throws Error if exception occurs in callback (context attached to exception)
*/
Namespace.prototype.run(fn: (context: object) => void): object;
/**
* Same as run() but returns the return value of callback instead of context
* @param fn - Callback function that receives the context as argument
* @returns The return value of the callback function
*/
Namespace.prototype.runAndReturn(fn: (context: object) => any): any;
/**
* Bind a function to the namespace context for deferred execution
* @param fn - Function to bind to the context
* @param context - Optional context to bind to (defaults to active or new context)
* @returns Wrapped function that runs in the bound context
*/
Namespace.prototype.bind(fn: Function, context?: object): Function;Advanced methods for creating and managing contexts directly.
/**
* Create a new context cloned from the currently active context
* Useful with bind() for fresh context at invocation time vs binding time
* @returns Context object using active context as prototype
*/
Namespace.prototype.createContext(): object;
/**
* Enter a specific context (internal method)
* @param context - Context object to enter
* @throws Error if context is not provided
*/
Namespace.prototype.enter(context: object): void;
/**
* Exit a specific context (internal method)
* @param context - Context object to exit
* @throws Error if context not provided or not found in context stack
*/
Namespace.prototype.exit(context: object): void;Methods for binding EventEmitters to namespace contexts.
/**
* Bind an EventEmitter to the namespace
* Similar to domain.add but for continuation-local storage
* @param emitter - EventEmitter instance to bind
* @throws Error if emitter doesn't have required methods (on, addListener, emit)
*/
Namespace.prototype.bindEmitter(emitter: EventEmitter): void;Usage Example:
const http = require('http');
const cls = require('continuation-local-storage');
const session = cls.createNamespace('http-session');
http.createServer(function(req, res) {
session.run(function() {
// Bind request and response to the session context
session.bindEmitter(req);
session.bindEmitter(res);
session.set('startTime', Date.now());
// Event handlers will maintain context
req.on('data', function(chunk) {
const startTime = session.get('startTime');
console.log(`Received data ${Date.now() - startTime}ms after start`);
});
// Continue with request handling...
});
});Methods for extracting context information from errors.
/**
* Extract context from an error that was thrown within a namespace
* Errors thrown in namespace contexts have the context attached
* @param exception - Error object that may have attached context
* @returns Context object or undefined if no context attached
*/
Namespace.prototype.fromException(exception: Error): object | undefined;/**
* Dictionary of all active Namespace objects, keyed by name
* Available globally after the module is first loaded
* @type {Object.<string, Namespace>}
*/
process.namespaces: { [name: string]: Namespace };/**
* Namespace class representing an application-specific context
*/
class Namespace {
/** The name of the namespace */
name: string;
/** The currently active context in the namespace */
active: object | null;
/** Async listener ID assigned by process.addAsyncListener */
id: number;
/** Internal context stack for enter/exit operations */
_set: object[];
}set() or get() outside of run() or bind() throws an errorcreateNamespace() without a name throws an errorbindEmitter() with object lacking required methods throws an errorenter()/exit() calls throw assertion errorsdestroyNamespace() on non-existent namespace throws an errorRequest Context in Web Applications:
const session = cls.createNamespace('request');
app.use(function(req, res, next) {
session.run(function() {
session.set('requestId', req.id);
session.set('userId', req.user?.id);
next();
});
});
// Later in any middleware or route handler
function logActivity(action) {
const requestId = session.get('requestId');
const userId = session.get('userId');
console.log(`User ${userId} performed ${action} in request ${requestId}`);
}Nested Contexts:
const tracer = cls.createNamespace('tracer');
tracer.run(function() {
tracer.set('operation', 'parent');
tracer.run(function() {
tracer.set('operation', 'child');
console.log(tracer.get('operation')); // 'child'
});
console.log(tracer.get('operation')); // 'parent'
});