context.js 5.0 KB

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