Continuation-local storage using Node.js AsyncLocalStorage with fallback to a modified implementation of cls-hooked.
When running Nodejs version 8.12.0 to version before (12.17.0
or 13.10.0
), this module uses Async-Hook API from node.js inspired from cls-hooked.
This module aims to provide a consistent API to user of Nodejs before introduction of AsyncLocalStorage which is available in Nodejs from version 12.17.0
or 13.10.0
onwards.
Continuation-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"][cps] 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.
Calls to .run()
can be nested, and each nested context this creates its own context (Here is the difference to cls-hook
). 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:
// const ALS = require('alscontext').default;
import ALS from 'alscontext';
const store = new ALS();
store.run({}, () => {
// store.get('value') return undefined
store.set('value', 0);
// store.get('value') return 0
requestHandler();
});
function requestHandler() {
store.run({}, () => {
// store.get('value') return undefined
store.set('value', 1);
// store.get('value') return 1
process.nextTick(() => {
// store.get('value') return 1
store.run({}, () => {
// store.get('value') return undefined
store.set('value', 2);
// store.get('value') return 2
});
// store.get('value') return 1
});
});
setTimeout(function () {
// store.get('value') return 0
}, 1000);
}
$ npm install alscontext
Importing Module
// mjs
import ALS, { CLS } from 'alscontext';
// cjs
const ALS = require('alscontext').default;
const ALS = require('alscontext').ALS;
const CLS = require('alscontext').CLS;
// ALS will automatically determine whether to use node's AsyncLocalStorage or the custom implemented CLS that is consistent with node
// If you want to use CLS, it can be imported as CLS like above
As an Express
middleware
const ALS = require('alscontext').default;
const express = require('express');
const als = new ALS();
const app = express();
const port = 3000;
app.use((req, res, next) => {
// als.run() creates a new context with a default object here
als.run({ user: 'John Doe' }, () => {
// You may do other operation here before passing the the next middleware
next();
});
});
app.use((req, res, next) => {
als.get('user'); // Return "John Doe"
als.set('user', 'Max'); // Set the "user" key to "Max"
next();
});
app.get('/', (req, res) => {
// als.getStore() returns a Map
res.json({ store: als.getStore() });
});
app.listen(port, () => {
console.log(`Running on http://localhost:${port}/`);
});
While working with EventEmitter
like req
or res
from Express
like the following, it is best to use als.bind()
or als.bindEmitter()
to bind it to the right async context
.
req.on(
'close',
als.bind(() => {
console.log(als.getStore()); //returns the store
})
);
req.on('close', () => {
console.log(als.getStore()); //returns undefined
});
This is because EventEmitter
is not part of the asyncResource
and it will run outside the context
. More on the discussion here
This module supports having multiple instance nested together, for example:
const ALS = require('alscontext').ALS;
const store1 = new ALS();
const store2 = new ALS();
let test1Val;
let test2Val;
let test3Val;
let test4Val;
store1.run({}, () => {
// Store 1 - outer run starts
console.log('Store 1 - outer run starts');
store2.run({}, () => {
// Store 2 - run starts
console.log('Store 2 - run starts');
store1.set('name', 'store1');
store2.set('name', 'store2');
setTimeout(() => {
store1.run({}, () => {
// Store 1 - inner run starts
console.log('Store 1 - inner run starts');
process.nextTick(() => {
console.log('Store 1 - next Tick starts');
store1.set('name', 'bob');
store2.set('name', 'alice');
setTimeout(() => {
console.log('Store 1 - timeout starts');
test3Val = store1.get('name');
test4Val = store2.get('name');
console.log(test3Val);
console.log(test4Val);
console.log('Store 1 - timeout ends');
});
console.log('Store 1 - next Tick ends');
});
console.log('Store 1 - inner run ends');
// Store 1 - inner run ends
});
});
process.nextTick(() => {
test1Val = store1.get('name');
test2Val = store2.get('name');
console.log(test1Val);
console.log(test2Val);
});
console.log('Store 2 - run ends');
// Store 2 - run ends
});
console.log('Store 1 - outer run ends');
// Store 1 - outer run ends
});
// Take note at the sequence
// Store 1 - outer run starts
// Store 2 - run starts
// Store 2 - run ends
// Store 1 - outer run ends
// store1
// store2
// Store 1 - inner run starts
// Store 1 - inner run ends
// Store 1 - next Tick starts
// Store 1 - next Tick ends
// Store 1 - timeout starts
// bob
// alice
// Store 1 - timeout ends
Runs a function synchronously and start the boundary of a context, anything set to be run from within the callback will have the same context.
Parameters
- defaults: Optional
Map
orRecord
containing default values for the context - callback: Function that will be the boundary of the said context, anything set to be run from within the callback will have the same context
- ...args: Option arguments to be passed to
callback
Return
- Return the return value from callback
Get the stored value in context or undefined
Parameters
- key: a string key to retrieve the stored value in context
Return
- Return the stored value in context or undefined
Set key & value to the current context
Parameters
-
key: a string key to store value in context
-
value: any value to be stored under the key for lookup
Get the entire context in Map object or undefined
Return
- Return the entire context in
Map
object or undefined (if it is called outsideals.run
)
Disable the instance of als
. All subsequent calls to als.getStore()
& als.get()
will return undefined until new context is created using als.run()
. It is developer’s responsibility to ensure that als
is disabled so the instance of als
can be garbage-collected. This does not applies to the store
or asyncResource
(Which is used to achieve this functionality) as these objects are garbage collected when the async resources is completed (after
hook)
Use this method when als
is no longer in use.
Runs a function synchronously outside of a context and returns its return value. The store is not accessible within the function or the asynchronous operations created within the function. Any als.getStore()
or als.get()
call done within the function will always return undefined
.
Parameters
- fn: Function to be bind to the current execution context
- ...args: Option arguments to be passed to
fn
Return
fn
's return value
Bind a function to the current execution context. This is useful especially when you need to access the context outside of als.run()
or when dealing when EventEmitters
Parameters
- fn: Function to be bind to the current execution context
- ...args: Option arguments to be passed to
fn
Return
fn
's return value
const ALS = require('alscontext').default;
const store = ALS();
let bindedFunc;
const someFunc = () => {
return store.get('test');
};
store.run({ test: 'something' }, () => {
bindedFunc = store.bind(someFunc);
});
bindedFunc(); // return "something" - able to get the context outside of run
someFunc(); // return undefined
An extension to the als.bind
which allow developers to specify the execution context to be bind to the function. This is useful especially when you need to access the context outside of als.run()
or when dealing when EventEmitters
Parameters
- asyncResource: AsyncResource object from Nodejs (const asyncResource = new AsyncResource)
- fn: Function to be bind to that
asyncResource
execution context - ...args: Option arguments to be passed to
fn
Return
fn
's return value
const ALS = require('alscontext').default;
const { AsyncResource } = require('async_hooks');
const store = ALS();
let asyncResource1;
let asyncResource2;
const otherFunc = () => {
return store.get('test');
};
store.run({ test: 1 }, () => {
asyncResource1 = new AsyncResource('a');
store.run({ test: 2 }, () => {
asyncResource2 = new AsyncResource('b');
});
});
const bindedFunc1 = store.bindEmitter(asyncResource1, otherFunc);
const bindedFunc2 = store.bindEmitter(asyncResource2, otherFunc);
bindedFunc1(); // return 1 - the context for the outer run
bindedFunc2(); // return 2 - the context for the inner run
otherFunc(); // return undefined