Simplify your Node code with Continuation Local Storage variables
Mike Talbot ⭐
Posted on May 29, 2020
TL;DR
- There's an easy way to have request local context variables through Node code reducing the complexity created by having to constantly forward parameters and route them through other layers like events etc.
- With this technique you can just type
cls.anythingYouLike = somethingElse
and it will be set and found anywhere in the code called by the current request, but will not interfere with other requests. - Significantly reduces clutter and confusion by removing the need to forward variables up and down between subroutines.
- A great feature is to be able to decorate cls with useful functions, such as
audit
that know who the current user is and then you can call them anywhere without needing to pass lots of context.
function someDeepRoutine(param) {
// Audit that the current user has accessed this function
// Without us having to explicitly pass lots of identity
// variables...
cls.audit("deepRoutineExecuted", {param})
}
- I've implemented it as an MIT licensed library you can use in your own code available from GitHub or
npm -i simple-continuation-local-storage
. - I explain how it works below:
The Idea
We have all kinds of ways of managing application state on the front end, but when it comes to the server we can find ourselves lost is a mass of parameters or context variables that need to be forwarded to and through everything in case something needs it later.
This is because we can't have global state on something which is processing many things in parallel for different users. At best we could try to create a context and associate that, but there is an easier way using Continuation Local Storage.
CLS is so named because it's a bit like Thread Local Storage - data specifically owned by a thread. It's a set of data that is scope to the current execution context. So no matter how many continuations are flowing through the server, each is sure to have it's own copy.
Now there have been a number of implementations of this but I found them all too complicated to use (getting namespaces etc) and some have a lot of code going on - I want something that "feels" like a global variable but is managed for me.
My servers all run with this now and while there is a small overhead caused by us using async_hooks
which are called every time you create a "continuation" - as you'll see in a moment the code is pretty tight.
Using my CLS library
To use cls we just need to install it and require it, then use its $init method to wrap our request response, or any other function you want to maintain state for. After that it's just like global
but you know, local
!
const events = require('event-bus');
const cls = require('simple-continuation-local-storage')
app.get('/somepath', cls.$init(async function(req,res) {
cls.jobs = 0;
cls.req = req;
cls.anything = 1;
await someOtherFunction();
res.status(200).send(await doSomeWork());
})
async someOtherFunction() {
await events.raiseAsync('validate-user');
}
events.on('validate-user', async function() {
const token = cls.req.query.token;
cls.authenticated = await validateToken(token);
});
async validateToken(token) {
await new Promise(resolve=>setTimeout(resolve, 100));
return true;
}
async doSomeWork() {
cls.jobs++;
await new Promise(resolve=>setTimeout(resolve, 1000));
return [{work: "was very hard"}];
}
As you can see, it's just like you were using global.something - but it's going to be unique for every request.
How it works
CLS using the async_hooks
feature of Node to allow us to be notified every time a new async context is made. It also uses a Proxy to allow us to have a sweet and simple interface that feels natural and works as expected.
const hooks = require( 'async_hooks' )
const cls = {}
let current = null
const HOLD = "$HOLD"
hooks
.createHook( {
init ( asyncId, type, triggerId ) {
let existing = cls[ triggerId ] || {}
cls[ asyncId ] = existing[HOLD] ? existing : { ...existing, _parent: existing}
},
before ( id ) {
current = cls[ id ] = cls[id] || {}
},
after () {
current = null
},
destroy ( id ) {
delete cls[ id ]
},
} )
.enable()
The hook has 4 callback. init
is called when a new context is created, this is every time you make an async call and every time you return from it (very important that!)
In init
we get the current POJO that represents the current state. Then if it has a $HOLD = true member we just send it along to the child. If it doesn't we make a shallow copy of it and send that.
Everything in this server is running through this hook - we only want to start really sharing the content backwards and forwards through the members of a single request or other entry point. In other words, we want a sub function to be able to set a value we can find at any time, in any called function, until the request ends. That cls.$init(fn)
we set in the function above does this.
The opposite of init
is destroy
- at this point we can throw away our context it will never be seen again.
before
is called before a context is entered - so just before our code runs - we need to grab the one we stored in init
. after
just clear it.
That's all there is to it!
Then the fancy Proxy stuff just makes cls
feel like global
.
function getCurrent () {
return current
}
module.exports = new Proxy( getCurrent, {
get ( obj, prop ) {
if ( prop === '$hold' ) return function(hold) {
current[HOLD] = !!hold
}
if( prop=== '$init') return function(fn) {
current && (current[HOLD] = true)
if(fn) {
return function(...params) {
current && (current[HOLD] = true)
return fn(...params)
}
}
}
if ( current ) {
return current[ prop ]
}
},
set ( obj, prop, value ) {
if ( current ) {
current[ prop ] = value
}
return true
},
has ( obj, prop ) {
return prop in current
},
} )
Setting a property on this, just sets it on the current context, for the currently in play continuation. Getting and has
are the reverse.
You can call cls()
to get the whole current object.
Demo
The sandbox below implements this and provide an Express server for a very boring page. If you don't pass a ?token=magic or ?token=nosomagic then it is Unauthenticated. Otherwise you can see how it decorates cls with a permissions definition that controls what happens.
Posted on May 29, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.