|
'use strict'; |
|
|
|
var assert = require('assert'); |
|
var wrapEmitter = require('emitter-listener'); |
|
var shimmer = require('shimmer'); |
|
var tracing = require('tracing'); |
|
|
|
/* |
|
* |
|
* CONSTANTS |
|
* |
|
*/ |
|
var CONTEXTS_SYMBOL = 'cls@contexts'; |
|
var ERROR_SYMBOL = 'error@context'; |
|
|
|
var namespaces; |
|
|
|
function Namespace(name) { |
|
this.name = name; |
|
// every namespace has a default / "global" context |
|
this.active = Object.create(null); |
|
this._set = []; |
|
this.id = null; |
|
} |
|
|
|
Namespace.prototype.set = function (key, value) { |
|
this.active[key] = value; |
|
return value; |
|
}; |
|
|
|
Namespace.prototype.get = function (key) { |
|
return this.active[key]; |
|
}; |
|
|
|
Namespace.prototype.createContext = function () { |
|
return Object.create(this.active); |
|
}; |
|
|
|
Namespace.prototype.run = function (fn) { |
|
var context = this.createContext(); |
|
this.enter(context); |
|
try { |
|
fn(context); |
|
return context; |
|
} |
|
catch (exception) { |
|
exception[ERROR_SYMBOL] = context; |
|
throw exception; |
|
} |
|
finally { |
|
this.exit(context); |
|
} |
|
}; |
|
|
|
Namespace.prototype.bind = function (fn, context) { |
|
if (!context) context = this.active; |
|
var self = this; |
|
return function () { |
|
self.enter(context); |
|
try { |
|
return fn.apply(this, arguments); |
|
} |
|
catch (exception) { |
|
exception[ERROR_SYMBOL] = context; |
|
throw exception; |
|
} |
|
finally { |
|
self.exit(context); |
|
} |
|
}; |
|
}; |
|
|
|
Namespace.prototype.enter = function (context) { |
|
assert.ok(context, "context must be provided for entering"); |
|
|
|
this._set.push(this.active); |
|
this.active = context; |
|
}; |
|
|
|
Namespace.prototype.exit = function (context) { |
|
assert.ok(context, "context must be provided for exiting"); |
|
|
|
// 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 |
|
var index = this._set.lastIndexOf(context); |
|
|
|
assert.ok(index >= 0, "context not currently entered; can't exit"); |
|
assert.ok(index, "can't remove top context"); |
|
|
|
this._set.splice(index, 1); |
|
}; |
|
|
|
Namespace.prototype.bindEmitter = function (emitter) { |
|
assert.ok(emitter.on && emitter.addListener && emitter.emit, "can only bind real EEs"); |
|
|
|
var namespace = this; |
|
var 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; |
|
|
|
var wrapped = unwrapped; |
|
var contexts = unwrapped[CONTEXTS_SYMBOL]; |
|
Object.keys(contexts).forEach(function (name) { |
|
var thunk = contexts[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 (exception) { |
|
return exception[ERROR_SYMBOL]; |
|
}; |
|
|
|
function get(name) { |
|
return namespaces[name]; |
|
} |
|
|
|
var j = 0; |
|
function create(name) { |
|
j++; |
|
assert.ok(name, "namespace must be given a name!"); |
|
|
|
var namespace = new Namespace(name); |
|
var i = 0; |
|
namespace.id = tracing.addAsyncListener( |
|
{ |
|
create : function ME() { |
|
var x = {} |
|
Error.captureStackTrace(x, ME); |
|
process._rawDebug(' + CREATE', j, ++i); |
|
process._rawDebug(x.stack.split('\n').slice(1,5).join('\n')); |
|
return { |
|
d: namespace.active, |
|
i: i |
|
}; |
|
}, |
|
before : function (context, domain) { |
|
process._rawDebug('\n--> BEFORE', j, domain.i); |
|
namespace.enter(domain.d); |
|
}, |
|
after : function (context, domain) { |
|
process._rawDebug('<-- AFTER ', j, domain.i); |
|
namespace.exit(domain.d); |
|
}, |
|
error : function (ctx, domain, err) { |
|
if (domain) namespace.exit(domain); |
|
} |
|
} |
|
); |
|
|
|
namespaces[name] = namespace; |
|
return namespace; |
|
} |
|
|
|
function destroy(name) { |
|
var namespace = get(name); |
|
|
|
assert.ok(namespace, "can't delete nonexistent namespace!"); |
|
assert.ok(namespace.id, "don't assign to process.namespaces directly!"); |
|
|
|
tracing.removeAsyncListener(namespace.id); |
|
namespaces[name] = null; |
|
} |
|
|
|
function reset() { |
|
// must unregister async listeners |
|
if (namespaces) { |
|
Object.keys(namespaces).forEach(function (name) { |
|
destroy(name); |
|
}); |
|
} |
|
namespaces = process.namespaces = Object.create(null); |
|
} |
|
reset(); // call immediately to set up |
|
|
|
module.exports = { |
|
getNamespace : get, |
|
createNamespace : create, |
|
destroyNamespace : destroy, |
|
reset : reset |
|
}; |