Research
Security News
Malicious npm Package Targets Solana Developers and Hijacks Funds
A malicious npm package targets Solana developers, rerouting funds in 2% of transactions to a hardcoded address.
cls-hooked
Advanced tools
The cls-hooked package is a Node.js module that allows you to create namespace-like contexts that persist across asynchronous operations. It uses the async_hooks module to track the context of asynchronous calls, enabling you to store and access data throughout the lifecycle of a request or any other asynchronous execution flow. This is particularly useful for logging, tracing, and passing request-specific data without the need to explicitly pass parameters through each function call.
Creating and using a namespace
This feature allows you to create a namespace and use it to set and get context values across asynchronous boundaries.
const createNamespace = require('cls-hooked').createNamespace;
const namespace = createNamespace('myNamespace');
namespace.run(() => {
namespace.set('key', 'value');
asyncFunction();
});
function asyncFunction() {
process.nextTick(() => {
console.log(namespace.get('key')); // Outputs: 'value'
});
}
Maintaining context in async/await
This feature demonstrates how cls-hooked can be used with async/await to maintain context across asynchronous calls.
const createNamespace = require('cls-hooked').createNamespace;
const namespace = createNamespace('myNamespace');
async function main() {
namespace.runPromise(async () => {
namespace.set('key', 'value');
await asyncFunction();
});
}
async function asyncFunction() {
await new Promise(resolve => setTimeout(resolve, 100));
console.log(namespace.get('key')); // Outputs: 'value'
}
main();
Integrating with Express middleware
This feature shows how cls-hooked can be integrated with Express middleware to maintain request-specific data across middleware and route handlers.
const createNamespace = require('cls-hooked').createNamespace;
const namespace = createNamespace('myNamespace');
const express = require('express');
const app = express();
app.use((req, res, next) => {
namespace.run(() => {
namespace.set('key', req.query.key);
next();
});
});
app.get('/', (req, res) => {
res.send(`Key: ${namespace.get('key')}`);
});
app.listen(3000);
This package provides similar functionality to cls-hooked, allowing you to manage context across asynchronous calls. However, it is based on the deprecated async-listener API and may not be compatible with newer versions of Node.js.
While not a direct alternative, async_hooks is the built-in Node.js module that cls-hooked leverages to track asynchronous resources. It provides a lower-level API for tracking the state and context of async operations.
Zone.js is a library that enables asynchronous context management similar to cls-hooked. It is often used in the Angular framework for managing execution contexts across async tasks. It is not specific to Node.js and can be used in browser environments as well.
Experimental
by Nodejs.AsyncWrap
is unofficial and undocumented but is currently in Nodejs versions 6 & 7cls-hooked
uses AsyncWrap
when run in Node < 8.cls-hooked
uses async_hooks API when run in Node >= 8.2.1Continuation-local storage works like thread-local storage in threaded programming, but is based on chains of Node-style callbacks instead of threads. The standard Node convention of functions calling functions is very similar to something called "continuation-passing style" in functional programming, and the name comes from the way this module allows you to set and get values that are scoped to the lifetime of these chains of function calls.
Suppose you're writing a module that fetches a user and adds it to a session before calling a function passed in by a user to continue execution:
// setup.js
var createNamespace = require('cls-hooked').createNamespace;
var session = createNamespace('my session');
var db = require('./lib/db.js');
function start(options, next) {
db.fetchUserById(options.id, function (error, user) {
if (error) return next(error);
session.set('user', user);
next();
});
}
Later on in the process of turning that user's data into an HTML page, you call another function (maybe defined in another module entirely) that wants to fetch the value you set earlier:
// send_response.js
var getNamespace = require('cls-hooked').getNamespace;
var session = getNamespace('my session');
var render = require('./lib/render.js')
function finish(response) {
var user = session.get('user');
render({user: user}).pipe(response);
}
When you set values in continuation-local storage, those values are accessible
until all functions called from the original function – synchronously or
asynchronously – have finished executing. This includes callbacks passed to
process.nextTick
and the timer functions (setImmediate,
setTimeout, and setInterval), as well as callbacks passed to
asynchronous functions that call native functions (such as those exported from
the fs
, dns
, zlib
and crypto
modules).
A simple rule of thumb is anywhere where you might have set a property on the
request
or response
objects in an HTTP handler, you can (and should) now
use continuation-local storage. This API is designed to allow you extend the
scope of a variable across a sequence of function calls, but with values
specific to each sequence of calls.
Values are grouped into namespaces, created with createNamespace()
. Sets of
function calls are grouped together by calling them within the function passed
to .run()
on the namespace object. Calls to .run()
can be nested, and each
nested context this creates has its own copy of the set of values from the
parent context. When a function is making multiple asynchronous calls, this
allows each child call to get, set, and pass along its own context without
overwriting the parent's.
A simple, annotated example of how this nesting behaves:
var createNamespace = require('cls-hooked').createNamespace;
var writer = createNamespace('writer');
writer.run(function () {
writer.set('value', 0);
requestHandler();
});
function requestHandler() {
writer.run(function(outer) {
// writer.get('value') returns 0
// outer.value is 0
writer.set('value', 1);
// writer.get('value') returns 1
// outer.value is 1
process.nextTick(function() {
// writer.get('value') returns 1
// outer.value is 1
writer.run(function(inner) {
// writer.get('value') returns 1
// outer.value is 1
// inner.value is 1
writer.set('value', 2);
// writer.get('value') returns 2
// outer.value is 1
// inner.value is 2
});
});
});
setTimeout(function() {
// runs with the default context, because nested contexts have ended
console.log(writer.get('value')); // prints 0
}, 1000);
}
Each application wanting to use continuation-local values should create its own namespace. Reading from (or, more significantly, writing to) namespaces that don't belong to you is a faux pas.
Look up an existing namespace.
Dispose of an existing namespace. WARNING: be sure to dispose of any references to destroyed namespaces in your old code, as contexts associated with them will no longer be propagated.
Completely reset all continuation-local storage namespaces. WARNING: while this will stop the propagation of values in any existing namespaces, if there are remaining references to those namespaces in code, the associated storage will still be reachable, even though the associated state is no longer being updated. Make sure you clean up any references to destroyed namespaces yourself.
Continuation-local storage has a performance cost, and so it isn't enabled
until the module is loaded for the first time. Once the module is loaded, the
current set of namespaces is available in process.namespaces
, so library code
that wants to use continuation-local storage only when it's active should test
for the existence of process.namespaces
.
Application-specific namespaces group values local to the set of functions
whose calls originate from a callback passed to namespace.run()
or
namespace.bind()
.
value
Set a value on the current continuation context. Must be set within an active
continuation chain started with namespace.run()
or namespace.bind()
.
undefined
Look up a value on the current continuation context. Recursively searches from
the innermost to outermost nested continuation context for a value associated
with a given key. Must be set within an active continuation chain started with
namespace.run()
or namespace.bind()
.
Create a new context on which values can be set or read. Run all the functions that are called (either directly, or indirectly through asynchronous functions that take callbacks themselves) from the provided callback within the scope of that namespace. The new context is passed as an argument to the callback when it's called.
Create a new context on which values can be set or read. Run all the functions that are called (either directly, or indirectly through asynchronous functions that take callbacks themselves) from the provided callback within the scope of that namespace. The new context is passed as an argument to the callback when it's called.
Same as namespace.run()
but returns the return value of the callback rather
than the context.
Bind a function to the specified namespace. Works analogously to
Function.bind()
or domain.bind()
. If context is omitted, it will default to
the currently active context in the namespace, or create a new context if none
is currently defined.
Bind an EventEmitter to a namespace. Operates similarly to domain.add
, with a
less generic name and the additional caveat that unlike domains, namespaces
never implicitly bind EventEmitters to themselves when they're created within
the context of an active namespace.
The most likely time you'd want to use this is when you're using Express or Connect and want to make sure your middleware execution plays nice with CLS, or are doing other things with HTTP listeners:
http.createServer(function (req, res) {
writer.bindEmitter(req);
writer.bindEmitter(res);
// do other stuff, some of which is asynchronous
});
Use this with namespace.bind()
, if you want to have a fresh context at invocation time,
as opposed to binding time:
function doSomething(p) {
console.log("%s = %s", p, ns.get(p));
}
function bindLater(callback) {
return writer.bind(callback, writer.createContext());
}
setInterval(function () {
var bound = bindLater(doSomething);
bound('test');
}, 100);
A context is a plain object created using the enclosing context as its prototype.
See LICENSE
for the details of the BSD 2-clause "simplified" license used by
continuation-local-storage
. This package was developed in 2012-2013 (and is
maintained now) by Forrest L Norvell, @othiym23,
with considerable help from Timothy Caswell,
@creationix, working for The Node Firm. This
work was underwritten by New Relic for use in their Node.js instrumentation
agent, so maybe give that a look if you have some Node.js
performance-monitoring needs.
FAQs
CLS using AsynWrap instead of async-listener - Node >= 4.7.0
The npm package cls-hooked receives a total of 1,511,871 weekly downloads. As such, cls-hooked popularity was classified as popular.
We found that cls-hooked demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
A malicious npm package targets Solana developers, rerouting funds in 2% of transactions to a hardcoded address.
Security News
Research
Socket researchers have discovered malicious npm packages targeting crypto developers, stealing credentials and wallet data using spyware delivered through typosquats of popular cryptographic libraries.
Security News
Socket's package search now displays weekly downloads for npm packages, helping developers quickly assess popularity and make more informed decisions.