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.
Top comments (8)
I was asked this by a colleague: what about
setInterval()
. The answer is "yes" it works as a continuation:Bookmarking.
I wonder the use-case and usage context.
At first glance it looks like something related to HTTP server middleware, and/or at leadt Node.js runtime side.
If that's the case, in KoaJS, there's ctx.state, and Express has locals and some way of abstracting/storing state such as shoring in a cookie or external, like koa-session does.
Before (ECMAScript 5) we had to use magic names and enumeration, then we got defineProperties. There are a few implementation of that idea. Now that Proxy is more common, it's great.
Gotta get back to this
You are right, it's pretty like
response.locals
in Express (app.locals are really rather global), I haven't used KoaJS. The difference being that you don't have to passresponse
through to every function you call as cls is defined as a require.Koa with ctx.state is unique per request per user. Look the source up, it's great. I prefer it to Express. It's actually from some folks from Express. But the API is more modern. I imagine that Express 5 (in beta?) is inspired by it
Looks from the docs that you are still supposed to pass it on though? It's not a global like cls unless I'm missing something. If that's the case then you put it in cls and never pass it through function layers, callback and events etc.
Many time come across CLS but didn't understand the use case but your article helps!. Thanks for writing!
Agreed it's one of those things that's "interesting" until you really get it. In my real app we basically have a phase of decorating CLS with data and closure functions that know who the user is etc. Then all of our updating of audit logs is:
I'm curious how this compares to the original
continuation-local-storage
which seems to do the same thing, just with a perhaps clearer api?