Skip to content

Asynchronous Chained Execution Context for Node

License

BSD-2-Clause, BSD-2-Clause licenses found

Licenses found

BSD-2-Clause
LICENSE
BSD-2-Clause
license.js
Notifications You must be signed in to change notification settings

solarwinds/ace-context

ace-context

Asynchronous Chains of Execution or Asynchronously Chained Execution refer to the chaining together of a single execution context across asynchronous function calls and event. Of course it works across synchronous calls as well, but nothing special is required to maintain context across synchronous calls.

ace-context provides context across asynchronous execution chains. It is analogous to the thread-local-storage in a threaded environment in that it provides storage for each "thread" of execution.

This is derived from Jeff Lewis' cls-hooked which is a fork of continuation-local-storage. cls-hooked uses async_hooks OR, for node prior to v8.1.1, AsyncWrap instead of async-listener which continuation-local-storage uses.

Warnings

When running Nodejs version < 8.2.1, this module uses AsyncWrap which is an unsupported Nodejs API, so please consider the risk before using it.

When running Nodejs version >= 8.2.1, this module uses the newer async_hooks API which is considered Experimental by Nodejs. This has been supported by node versions 8 through 13 and is the more reliable of the two approaches.

Shout out

Thanks to @trevnorris for AsyncWrap, async_hooks and all the async work in Node, and @AndreasMadsen for async-hook.

A little history of "AsyncWrap/async_hooks" and its predecessors

  1. The first implementation was implemented in node v0.11 and was called AsyncListener. It was removed from core prior to Nodejs v0.12.
  2. The second implementation was called AsyncWrap, async-wrap or async_wrap and was included with Nodejs v0.12.
    • AsyncWrap is internal, undocumented, an unoffical but is still in Nodejs version 13
    • ace-context uses AsyncWrap when run in Node < 8.2.1
  3. The third implementation is called AsyncHooks (async_hooks) and was introduced in Nodejs version 8.

A Quick Introduction to Asynchronously Chained Execution Context

Asynchronously Chained Execution Context (ace-context) works like thread-local storage in threaded programming but it based on chains of callbacks and promise-resolutions instead of threads.

The original module that this is derived from was named continuation-local-storage because it is similar to "continuation passing style" in functional programming. The name ace-context refers to the target need that this module addresses - it provides context scoped to the lifetime of the chain of asynchronous functions being executed.

An example

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('ace-context').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('ace-context').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 ace-context, 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, the timer functions (setImmediate, setTimeout, and setInterval), callbacks passed to asynchronous functions such as those exported from the fs, dns, zlib and crypto modules, as well as native Promises.

A simple rule of thumb is that anywhere where you set a property on the request or response objects in an HTTP handler in order to maintain context, you can, and probably should, use ace-context. This API is designed to allow you to maintain context of your choosing across a sequence of function calls, with values specific to each sequence of calls.

Contexts are grouped into namespaces, created with createNamespace(). Each namespace can hold multiple contexts each representing an asynchronous chain of execution (ace). An ace-context is created by calling .run() on a namespace object. Calls to .run() can be nested and each nested context holds its own copy of any values set by the parent context. This allows each child call to get and set its own values without overwriting the parent's.

An annotated example of how this nesting behaves:

var createNamespace = require('ace-context').createNamespace;

var example = createNamespace('example');

// this creates an ace-context
example.run(function () {
  example.set('value', 0);

  requestHandler();
});

// namespace.run's callback function is passed its context
// when it is run.
function requestHandler () {

  // create a nested context within the current ace.
  example.run(function (outer) {
    // example.get('value') returns 0
    // outer.value is 0
    example.set('value', 1);
    // example.get('value') returns 1
    // outer.value is 1

    // invoke a function asynchronously
    process.nextTick(function () {
      // example.get('value') returns 1
      // outer.value is 1

      // create another nested context within the current ace.
      example.run(function (inner) {
        // example.get('value') returns 1
        // outer.value is 1
        // inner.value is 1
        example.set('value', 2);
        // example.get('value') returns 2
        // outer.value is 1
        // inner.value is 2
      });
    });
  });

  setTimeout(function () {
    // runs with the default context, because it is not in the scope of
    // the nested contexts.
    console.log(example.get('value')); // prints 0
  }, 1000);
}

API

ace.createNamespace(name)

  • return: {Namespace}

Each application using ace-context should create its own namespace. Reading from or, more worrisome, writing to, namespaces that don't belong to you is a faux pas.

ace.getNamespace(name)

  • return: {Namespace}

Look up an existing namespace. This can be used to verify that the name you plan to use is not already in use.

ace.destroyNamespace(name)

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.

ace.reset()

Completely reset all ace-context 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.

process.namespaces

  • return: dictionary of {Namespace} objects

ace-context has a performance cost, 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 ace-context only when it's active should test for the existence of process.namespaces.

Class: Namespace

A namespace is container for an application's ace-contexts. Each ace-context holds values specific to a single chain of execution. Each ace-context is originated by a call to one of the ace-context originators: namespace.run(), namespace.runAndReturn(), namespace.runPromise(), or namespace.bind().

namespace.active

  • return: the currently active context for a namespace

namespace.set(key, value)

  • return: value

Set a value on the current ace-context. Must be set within an active continuation chain started with an ace-context originator. If there is no context an error will be thrown.

namespace.get(key)

  • return: the requested value, or undefined

Look up a value on the current ace-context. Recursively searches from the innermost to outermost nested ace-context for a value associated with a given key. Must be set within an active ace started with an ace-context originator.

namespace.run(callback [, contextOptions])

  • return: the context associated with that callback

Create a new ace-context (or descend from an existing context) on which values can be set or read. Run the callback and all the functions that are called either directly or indirectly through asynchronous functions and promises within that ace-context. The context is passed as an argument to the callback.

namespace.runAndReturn(callback [, contextOptions])

  • return: the return value of the callback

Same as namespace.run() but returns the return value of the callback rather than the context.

namespace.runPromise(callback [, contextOptions])

  • return: the promise returned by the callback

Same as namespace.run() but returns the promise returned by callback after exiting the ace-context when the promise is resolved or rejected. It propagates either the promise's resolved value or throws the error.

namespace.bind(callback, [context])

  • return: a callback wrapped up in a context closure

Bind a function to the specified namespace. Works analogously to Function.bind(). If context is omitted, it will use the namespace's currently active context or create a new context if no context is active.

namespace.bindEmitter(emitter)

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.

You might want to use this when you need to maintain ace-context across your own or other software's event handlers.

http.createServer(function (req, res) {
  writer.bindEmitter(req);
  writer.bindEmitter(res);

  req.on('error', function errorHandler () {...});

  // do other stuff. when errorHandler() is invoked the context will be the
  // context when writer.bindEmitter(req) was called.
});

namespace.createContext([contextOptions])

  • return: a context cloned from the currently active context

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);

contextOptions

contextOptions are a plain object. contextOptions.newContext is a boolean. If truthy a new, empty ace-context is created. The context will not inherit from any currently active context. This is useful when you want to ignore any existing context and start a new asynchronous chain of execution.

context

A context is a plain object created using the enclosing context as its prototype.

copyright & license

See LICENSE for the details of the BSD 2-clause "simplified" license used by continuation-local-storage which 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.

About

Asynchronous Chained Execution Context for Node

Resources

License

BSD-2-Clause, BSD-2-Clause licenses found

Licenses found

BSD-2-Clause
LICENSE
BSD-2-Clause
license.js

Stars

Watchers

Forks

Packages

No packages published