UNPKG

4.93 kBJavaScriptView Raw
1'use strict';
2
3var assert = require('assert');
4var wrapEmitter = require('emitter-listener');
5
6/*
7 *
8 * CONSTANTS
9 *
10 */
11var CONTEXTS_SYMBOL = 'cls@contexts';
12var ERROR_SYMBOL = 'error@context';
13
14// load polyfill if native support is unavailable
15if (!process.addAsyncListener) require('async-listener');
16
17var namespaces;
18
19function Namespace(name) {
20 this.name = name;
21 // changed in 2.7: no default context
22 this.active = null;
23 this._set = [];
24 this.id = null;
25}
26
27Namespace.prototype.set = function (key, value) {
28 if (!this.active) {
29 throw new Error("No context available. ns.run() or ns.bind() must be called first.");
30 }
31
32 this.active[key] = value;
33 return value;
34};
35
36Namespace.prototype.get = function (key) {
37 if (!this.active) return undefined;
38
39 return this.active[key];
40};
41
42Namespace.prototype.createContext = function () {
43 return Object.create(this.active);
44};
45
46Namespace.prototype.run = function (fn) {
47 var context = this.createContext();
48 this.enter(context);
49 try {
50 fn(context);
51 return context;
52 }
53 catch (exception) {
54 exception[ERROR_SYMBOL] = context;
55 throw exception;
56 }
57 finally {
58 this.exit(context);
59 }
60};
61
62Namespace.prototype.bind = function (fn, context) {
63 if (!context) {
64 if (!this.active) {
65 context = Object.create(this.active);
66 }
67 else {
68 context = this.active;
69 }
70 }
71
72 var self = this;
73 return function () {
74 self.enter(context);
75 try {
76 return fn.apply(this, arguments);
77 }
78 catch (exception) {
79 exception[ERROR_SYMBOL] = context;
80 throw exception;
81 }
82 finally {
83 self.exit(context);
84 }
85 };
86};
87
88Namespace.prototype.enter = function (context) {
89 assert.ok(context, "context must be provided for entering");
90
91 this._set.push(this.active);
92 this.active = context;
93};
94
95Namespace.prototype.exit = function (context) {
96 assert.ok(context, "context must be provided for exiting");
97
98 // Fast path for most exits that are at the top of the stack
99 if (this.active === context) {
100 assert.ok(this._set.length, "can't remove top context");
101 this.active = this._set.pop();
102 return;
103 }
104
105 // Fast search in the stack using lastIndexOf
106 var index = this._set.lastIndexOf(context);
107
108 assert.ok(index >= 0, "context not currently entered; can't exit");
109 assert.ok(index, "can't remove top context");
110
111 this._set.splice(index, 1);
112};
113
114Namespace.prototype.bindEmitter = function (emitter) {
115 assert.ok(emitter.on && emitter.addListener && emitter.emit, "can only bind real EEs");
116
117 var namespace = this;
118 var thisSymbol = 'context@' + this.name;
119
120 // Capture the context active at the time the emitter is bound.
121 function attach(listener) {
122 if (!listener) return;
123 if (!listener[CONTEXTS_SYMBOL]) listener[CONTEXTS_SYMBOL] = Object.create(null);
124
125 listener[CONTEXTS_SYMBOL][thisSymbol] = {
126 namespace : namespace,
127 context : namespace.active
128 };
129 }
130
131 // At emit time, bind the listener within the correct context.
132 function bind(unwrapped) {
133 if (!(unwrapped && unwrapped[CONTEXTS_SYMBOL])) return unwrapped;
134
135 var wrapped = unwrapped;
136 var contexts = unwrapped[CONTEXTS_SYMBOL];
137 Object.keys(contexts).forEach(function (name) {
138 var thunk = contexts[name];
139 wrapped = thunk.namespace.bind(wrapped, thunk.context);
140 });
141 return wrapped;
142 }
143
144 wrapEmitter(emitter, attach, bind);
145};
146
147/**
148 * If an error comes out of a namespace, it will have a context attached to it.
149 * This function knows how to find it.
150 *
151 * @param {Error} exception Possibly annotated error.
152 */
153Namespace.prototype.fromException = function (exception) {
154 return exception[ERROR_SYMBOL];
155};
156
157function get(name) {
158 return namespaces[name];
159}
160
161function create(name) {
162 assert.ok(name, "namespace must be given a name!");
163
164 var namespace = new Namespace(name);
165 namespace.id = process.addAsyncListener({
166 create : function () { return namespace.active; },
167 before : function (context, storage) { if (storage) namespace.enter(storage); },
168 after : function (context, storage) { if (storage) namespace.exit(storage); },
169 error : function (storage) { if (storage) namespace.exit(storage); }
170 });
171
172 namespaces[name] = namespace;
173 return namespace;
174}
175
176function destroy(name) {
177 var namespace = get(name);
178
179 assert.ok(namespace, "can't delete nonexistent namespace!");
180 assert.ok(namespace.id, "don't assign to process.namespaces directly!");
181
182 process.removeAsyncListener(namespace.id);
183 namespaces[name] = null;
184}
185
186function reset() {
187 // must unregister async listeners
188 if (namespaces) {
189 Object.keys(namespaces).forEach(function (name) {
190 destroy(name);
191 });
192 }
193 namespaces = process.namespaces = Object.create(null);
194}
195reset(); // call immediately to set up
196
197module.exports = {
198 getNamespace : get,
199 createNamespace : create,
200 destroyNamespace : destroy,
201 reset : reset
202};