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