DEV Community

Cover image for Simplify your Node code with Continuation Local Storage variables
Mike Talbot ⭐
Mike Talbot ⭐

Posted on • Edited on

Simplify your Node code with Continuation Local Storage variables

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})
   }
Enter fullscreen mode Exit fullscreen mode
  • 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"}];
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
    },
} )

Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
miketalbot profile image
Mike Talbot ⭐

I was asked this by a colleague: what about setInterval(). The answer is "yes" it works as a continuation:

      app.get('/path', cls.$init(function handler(req,res) {
             cls.audit = async function(message, content) {
                  await mysql.insert(...);
             }
             ...
             cls.timerInterval = setInterval(checkSomething, 60000);
      }))

     function checkSomething() { 
          cls.audit("checked", {time: Date.now()})
          if(somethingHappenend) clearInterval(cls.timerInterval);
     }

Collapse
 
renoirb profile image
Renoir • Edited

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

Collapse
 
miketalbot profile image
Mike Talbot ⭐

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 pass response through to every function you call as cls is defined as a require.

Collapse
 
renoirb profile image
Renoir

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

Thread Thread
 
miketalbot profile image
Mike Talbot ⭐

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.

Collapse
 
rajajaganathan profile image
Raja Jaganathan

Many time come across CLS but didn't understand the use case but your article helps!. Thanks for writing!

Collapse
 
miketalbot profile image
Mike Talbot ⭐ • Edited

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:

     function whoKnowsHowIGotHere(param) {
           cls.audit("gotHere", {param});
     }
Collapse
 
marvingreenberg profile image
marvin greenberg

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?