/* eslint-disable max-len */ 'use strict'; const util = require('util'); const assert = require('assert'); const wrapEmitter = require('emitter-listener'); const async_hooks = require('async_hooks'); const CONTEXTS_SYMBOL = 'cls@contexts'; const ERROR_SYMBOL = 'error@context'; const DEBUG_CLS_HOOKED = process.env.DEBUG_CLS_HOOKED; let currentUid = -1; module.exports = { getNamespace: getNamespace, createNamespace: createNamespace, destroyNamespace: destroyNamespace, reset: reset, ERROR_SYMBOL: ERROR_SYMBOL }; function Namespace(name) { this.name = name; // changed in 2.7: no default context this.active = null; this._set = []; this.id = null; this._contexts = new Map(); this._indent = 0; } Namespace.prototype.set = function set(key, value) { if (!this.active) { throw new Error('No context available. ns.run() or ns.bind() must be called first.'); } this.active[key] = value; if (DEBUG_CLS_HOOKED) { const indentStr = ' '.repeat(this._indent < 0 ? 0 : this._indent); debug2(indentStr + 'CONTEXT-SET KEY:' + key + '=' + value + ' in ns:' + this.name + ' currentUid:' + currentUid + ' active:' + util.inspect(this.active, {showHidden:true, depth:2, colors:true})); } return value; }; Namespace.prototype.get = function get(key) { if (!this.active) { if (DEBUG_CLS_HOOKED) { const asyncHooksCurrentId = async_hooks.currentId(); const triggerId = async_hooks.triggerAsyncId(); const indentStr = ' '.repeat(this._indent < 0 ? 0 : this._indent); //debug2(indentStr + 'CONTEXT-GETTING KEY NO ACTIVE NS:' + key + '=undefined' + ' (' + this.name + ') currentUid:' + currentUid + ' active:' + util.inspect(this.active, {showHidden:true, depth:2, colors:true})); debug2(`${indentStr}CONTEXT-GETTING KEY NO ACTIVE NS: (${this.name}) ${key}=undefined currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${this._set.length}`); } return undefined; } if (DEBUG_CLS_HOOKED) { const asyncHooksCurrentId = async_hooks.executionAsyncId(); const triggerId = async_hooks.triggerAsyncId(); const indentStr = ' '.repeat(this._indent < 0 ? 0 : this._indent); debug2(indentStr + 'CONTEXT-GETTING KEY:' + key + '=' + this.active[key] + ' (' + this.name + ') currentUid:' + currentUid + ' active:' + util.inspect(this.active, {showHidden:true, depth:2, colors:true})); debug2(`${indentStr}CONTEXT-GETTING KEY: (${this.name}) ${key}=${this.active[key]} currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${this._set.length} active:${util.inspect(this.active)}`); } return this.active[key]; }; Namespace.prototype.createContext = function createContext() { // Prototype inherit existing context if created a new child context within existing context. let context = Object.create(this.active ? this.active : Object.prototype); context._ns_name = this.name; context.id = currentUid; if (DEBUG_CLS_HOOKED) { const asyncHooksCurrentId = async_hooks.executionAsyncId(); const triggerId = async_hooks.triggerAsyncId(); const indentStr = ' '.repeat(this._indent < 0 ? 0 : this._indent); debug2(`${indentStr}CONTEXT-CREATED Context: (${this.name}) currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${this._set.length} context:${util.inspect(context, {showHidden:true, depth:2, colors:true})}`); } return context; }; Namespace.prototype.run = function run(fn) { let context = this.createContext(); this.enter(context); try { if (DEBUG_CLS_HOOKED) { const triggerId = async_hooks.triggerAsyncId(); const asyncHooksCurrentId = async_hooks.executionAsyncId(); const indentStr = ' '.repeat(this._indent < 0 ? 0 : this._indent); debug2(`${indentStr}CONTEXT-RUN BEGIN: (${this.name}) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${this._set.length} context:${util.inspect(context)}`); } fn(context); return context; } catch (exception) { if (exception) { exception[ERROR_SYMBOL] = context; } throw exception; } finally { if (DEBUG_CLS_HOOKED) { const triggerId = async_hooks.triggerAsyncId(); const asyncHooksCurrentId = async_hooks.executionAsyncId(); const indentStr = ' '.repeat(this._indent < 0 ? 0 : this._indent); debug2(`${indentStr}CONTEXT-RUN END: (${this.name}) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${this._set.length} ${util.inspect(context)}`); } this.exit(context); } }; Namespace.prototype.runAndReturn = function runAndReturn(fn) { let value; this.run(function (context) { value = fn(context); }); return value; }; /** * Uses global Promise and assumes Promise is cls friendly or wrapped already. * @param {function} fn * @returns {*} */ Namespace.prototype.runPromise = function runPromise(fn) { let context = this.createContext(); this.enter(context); let promise = fn(context); if (!promise || !promise.then || !promise.catch) { throw new Error('fn must return a promise.'); } if (DEBUG_CLS_HOOKED) { debug2('CONTEXT-runPromise BEFORE: (' + this.name + ') currentUid:' + currentUid + ' len:' + this._set.length + ' ' + util.inspect(context)); } return promise .then(result => { if (DEBUG_CLS_HOOKED) { debug2('CONTEXT-runPromise AFTER then: (' + this.name + ') currentUid:' + currentUid + ' len:' + this._set.length + ' ' + util.inspect(context)); } this.exit(context); return result; }) .catch(err => { err[ERROR_SYMBOL] = context; if (DEBUG_CLS_HOOKED) { debug2('CONTEXT-runPromise AFTER catch: (' + this.name + ') currentUid:' + currentUid + ' len:' + this._set.length + ' ' + util.inspect(context)); } this.exit(context); throw err; }); }; Namespace.prototype.bind = function bindFactory(fn, context) { if (!context) { if (!this.active) { context = this.createContext(); } else { context = this.active; } } let self = this; return function clsBind() { self.enter(context); try { return fn.apply(this, arguments); } catch (exception) { if (exception) { exception[ERROR_SYMBOL] = context; } throw exception; } finally { self.exit(context); } }; }; Namespace.prototype.enter = function enter(context) { assert.ok(context, 'context must be provided for entering'); if (DEBUG_CLS_HOOKED) { const asyncHooksCurrentId = async_hooks.executionAsyncId(); const triggerId = async_hooks.triggerAsyncId(); const indentStr = ' '.repeat(this._indent < 0 ? 0 : this._indent); debug2(`${indentStr}CONTEXT-ENTER: (${this.name}) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${this._set.length} ${util.inspect(context)}`); } this._set.push(this.active); this.active = context; }; Namespace.prototype.exit = function exit(context) { assert.ok(context, 'context must be provided for exiting'); if (DEBUG_CLS_HOOKED) { const asyncHooksCurrentId = async_hooks.executionAsyncId(); const triggerId = async_hooks.triggerAsyncId(); const indentStr = ' '.repeat(this._indent < 0 ? 0 : this._indent); debug2(`${indentStr}CONTEXT-EXIT: (${this.name}) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${this._set.length} ${util.inspect(context)}`); } // Fast path for most exits that are at the top of the stack if (this.active === context) { assert.ok(this._set.length, 'can\'t remove top context'); this.active = this._set.pop(); return; } // Fast search in the stack using lastIndexOf let index = this._set.lastIndexOf(context); if (index < 0) { if (DEBUG_CLS_HOOKED) { debug2('??ERROR?? context exiting but not entered - ignoring: ' + util.inspect(context)); } assert.ok(index >= 0, 'context not currently entered; can\'t exit. \n' + util.inspect(this) + '\n' + util.inspect(context)); } else { assert.ok(index, 'can\'t remove top context'); this._set.splice(index, 1); } }; Namespace.prototype.bindEmitter = function bindEmitter(emitter) { assert.ok(emitter.on && emitter.addListener && emitter.emit, 'can only bind real EEs'); let namespace = this; let thisSymbol = 'context@' + this.name; // Capture the context active at the time the emitter is bound. function attach(listener) { if (!listener) { return; } if (!listener[CONTEXTS_SYMBOL]) { listener[CONTEXTS_SYMBOL] = Object.create(null); } listener[CONTEXTS_SYMBOL][thisSymbol] = { namespace: namespace, context: namespace.active }; } // At emit time, bind the listener within the correct context. function bind(unwrapped) { if (!(unwrapped && unwrapped[CONTEXTS_SYMBOL])) { return unwrapped; } let wrapped = unwrapped; let unwrappedContexts = unwrapped[CONTEXTS_SYMBOL]; Object.keys(unwrappedContexts).forEach(function (name) { let thunk = unwrappedContexts[name]; wrapped = thunk.namespace.bind(wrapped, thunk.context); }); return wrapped; } wrapEmitter(emitter, attach, bind); }; /** * If an error comes out of a namespace, it will have a context attached to it. * This function knows how to find it. * * @param {Error} exception Possibly annotated error. */ Namespace.prototype.fromException = function fromException(exception) { return exception[ERROR_SYMBOL]; }; function getNamespace(name) { return process.namespaces[name]; } function createNamespace(name) { assert.ok(name, 'namespace must be given a name.'); if (DEBUG_CLS_HOOKED) { debug2(`NS-CREATING NAMESPACE (${name})`); } let namespace = new Namespace(name); namespace.id = currentUid; const hook = async_hooks.createHook({ init(asyncId, type, triggerId, resource) { currentUid = async_hooks.executionAsyncId(); //CHAIN Parent's Context onto child if none exists. This is needed to pass net-events.spec // let initContext = namespace.active; // if(!initContext && triggerId) { // let parentContext = namespace._contexts.get(triggerId); // if (parentContext) { // namespace.active = parentContext; // namespace._contexts.set(currentUid, parentContext); // if (DEBUG_CLS_HOOKED) { // const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent); // debug2(`${indentStr}INIT [${type}] (${name}) WITH PARENT CONTEXT asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`); // } // } else if (DEBUG_CLS_HOOKED) { // const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent); // debug2(`${indentStr}INIT [${type}] (${name}) MISSING CONTEXT asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`); // } // }else { // namespace._contexts.set(currentUid, namespace.active); // if (DEBUG_CLS_HOOKED) { // const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent); // debug2(`${indentStr}INIT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`); // } // } if(namespace.active) { namespace._contexts.set(asyncId, namespace.active); if (DEBUG_CLS_HOOKED) { const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent); debug2(`${indentStr}INIT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, {showHidden:true, depth:2, colors:true})} resource:${resource}`); } }else if(currentUid === 0){ // CurrentId will be 0 when triggered from C++. Promise events // https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid const triggerId = async_hooks.triggerAsyncId(); const triggerIdContext = namespace._contexts.get(triggerId); if (triggerIdContext) { namespace._contexts.set(asyncId, triggerIdContext); if (DEBUG_CLS_HOOKED) { const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent); debug2(`${indentStr}INIT USING CONTEXT FROM TRIGGERID [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, { showHidden: true, depth: 2, colors: true })} resource:${resource}`); } } else if (DEBUG_CLS_HOOKED) { const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent); debug2(`${indentStr}INIT MISSING CONTEXT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, { showHidden: true, depth: 2, colors: true })} resource:${resource}`); } } if(DEBUG_CLS_HOOKED && type === 'PROMISE'){ debug2(util.inspect(resource, {showHidden: true})); const parentId = resource.parentId; const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent); debug2(`${indentStr}INIT RESOURCE-PROMISE [${type}] (${name}) parentId:${parentId} asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, {showHidden:true, depth:2, colors:true})} resource:${resource}`); } }, before(asyncId) { currentUid = async_hooks.executionAsyncId(); let context; /* if(currentUid === 0){ // CurrentId will be 0 when triggered from C++. Promise events // https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid //const triggerId = async_hooks.triggerAsyncId(); context = namespace._contexts.get(asyncId); // || namespace._contexts.get(triggerId); }else{ context = namespace._contexts.get(currentUid); } */ //HACK to work with promises until they are fixed in node > 8.1.1 context = namespace._contexts.get(asyncId) || namespace._contexts.get(currentUid); if (context) { if (DEBUG_CLS_HOOKED) { const triggerId = async_hooks.triggerAsyncId(); const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent); debug2(`${indentStr}BEFORE (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, {showHidden:true, depth:2, colors:true})} context:${util.inspect(context)}`); namespace._indent += 2; } namespace.enter(context); } else if (DEBUG_CLS_HOOKED) { const triggerId = async_hooks.triggerAsyncId(); const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent); debug2(`${indentStr}BEFORE MISSING CONTEXT (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, {showHidden:true, depth:2, colors:true})} namespace._contexts:${util.inspect(namespace._contexts, {showHidden:true, depth:2, colors:true})}`); namespace._indent += 2; } }, after(asyncId) { currentUid = async_hooks.executionAsyncId(); let context; // = namespace._contexts.get(currentUid); /* if(currentUid === 0){ // CurrentId will be 0 when triggered from C++. Promise events // https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid //const triggerId = async_hooks.triggerAsyncId(); context = namespace._contexts.get(asyncId); // || namespace._contexts.get(triggerId); }else{ context = namespace._contexts.get(currentUid); } */ //HACK to work with promises until they are fixed in node > 8.1.1 context = namespace._contexts.get(asyncId) || namespace._contexts.get(currentUid); if (context) { if (DEBUG_CLS_HOOKED) { const triggerId = async_hooks.triggerAsyncId(); namespace._indent -= 2; const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent); debug2(`${indentStr}AFTER (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, {showHidden:true, depth:2, colors:true})} context:${util.inspect(context)}`); } namespace.exit(context); } else if (DEBUG_CLS_HOOKED) { const triggerId = async_hooks.triggerAsyncId(); namespace._indent -= 2; const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent); debug2(`${indentStr}AFTER MISSING CONTEXT (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, {showHidden:true, depth:2, colors:true})} context:${util.inspect(context)}`); } }, destroy(asyncId) { currentUid = async_hooks.executionAsyncId(); if (DEBUG_CLS_HOOKED) { const triggerId = async_hooks.triggerAsyncId(); const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent); debug2(`${indentStr}DESTROY (${name}) currentUid:${currentUid} asyncId:${asyncId} triggerId:${triggerId} active:${util.inspect(namespace.active, {showHidden:true, depth:2, colors:true})} context:${util.inspect(namespace._contexts.get(currentUid))}`); } namespace._contexts.delete(asyncId); } }); hook.enable(); process.namespaces[name] = namespace; return namespace; } function destroyNamespace(name) { let namespace = getNamespace(name); assert.ok(namespace, 'can\'t delete nonexistent namespace! "' + name + '"'); assert.ok(namespace.id, 'don\'t assign to process.namespaces directly! ' + util.inspect(namespace)); process.namespaces[name] = null; } function reset() { // must unregister async listeners if (process.namespaces) { Object.keys(process.namespaces).forEach(function (name) { destroyNamespace(name); }); } process.namespaces = Object.create(null); } process.namespaces = {}; //const fs = require('fs'); function debug2(...args) { if (DEBUG_CLS_HOOKED) { //fs.writeSync(1, `${util.format(...args)}\n`); process._rawDebug(`${util.format(...args)}`); } } /*function getFunctionName(fn) { if (!fn) { return fn; } if (typeof fn === 'function') { if (fn.name) { return fn.name; } return (fn.toString().trim().match(/^function\s*([^\s(]+)/) || [])[1]; } else if (fn.constructor && fn.constructor.name) { return fn.constructor.name; } }*/