• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const {
4  NumberIsSafeInteger,
5  ObjectDefineProperties,
6  ObjectIs,
7  ReflectApply,
8  Symbol,
9} = primordials;
10
11const {
12  ERR_ASYNC_CALLBACK,
13  ERR_ASYNC_TYPE,
14  ERR_INVALID_ARG_TYPE,
15  ERR_INVALID_ASYNC_ID
16} = require('internal/errors').codes;
17const { validateString } = require('internal/validators');
18const internal_async_hooks = require('internal/async_hooks');
19
20// Get functions
21// For userland AsyncResources, make sure to emit a destroy event when the
22// resource gets gced.
23const { registerDestroyHook } = internal_async_hooks;
24const {
25  executionAsyncId,
26  triggerAsyncId,
27  // Private API
28  hasAsyncIdStack,
29  getHookArrays,
30  enableHooks,
31  disableHooks,
32  executionAsyncResource,
33  // Internal Embedder API
34  newAsyncId,
35  getDefaultTriggerAsyncId,
36  emitInit,
37  emitBefore,
38  emitAfter,
39  emitDestroy,
40  enabledHooksExist,
41  initHooksExist,
42  destroyHooksExist,
43} = internal_async_hooks;
44
45// Get symbols
46const {
47  async_id_symbol, trigger_async_id_symbol,
48  init_symbol, before_symbol, after_symbol, destroy_symbol,
49  promise_resolve_symbol
50} = internal_async_hooks.symbols;
51
52// Get constants
53const {
54  kInit, kBefore, kAfter, kDestroy, kTotals, kPromiseResolve,
55} = internal_async_hooks.constants;
56
57// Listener API //
58
59class AsyncHook {
60  constructor({ init, before, after, destroy, promiseResolve }) {
61    if (init !== undefined && typeof init !== 'function')
62      throw new ERR_ASYNC_CALLBACK('hook.init');
63    if (before !== undefined && typeof before !== 'function')
64      throw new ERR_ASYNC_CALLBACK('hook.before');
65    if (after !== undefined && typeof after !== 'function')
66      throw new ERR_ASYNC_CALLBACK('hook.after');
67    if (destroy !== undefined && typeof destroy !== 'function')
68      throw new ERR_ASYNC_CALLBACK('hook.destroy');
69    if (promiseResolve !== undefined && typeof promiseResolve !== 'function')
70      throw new ERR_ASYNC_CALLBACK('hook.promiseResolve');
71
72    this[init_symbol] = init;
73    this[before_symbol] = before;
74    this[after_symbol] = after;
75    this[destroy_symbol] = destroy;
76    this[promise_resolve_symbol] = promiseResolve;
77  }
78
79  enable() {
80    // The set of callbacks for a hook should be the same regardless of whether
81    // enable()/disable() are run during their execution. The following
82    // references are reassigned to the tmp arrays if a hook is currently being
83    // processed.
84    const [hooks_array, hook_fields] = getHookArrays();
85
86    // Each hook is only allowed to be added once.
87    if (hooks_array.includes(this))
88      return this;
89
90    const prev_kTotals = hook_fields[kTotals];
91
92    // createHook() has already enforced that the callbacks are all functions,
93    // so here simply increment the count of whether each callbacks exists or
94    // not.
95    hook_fields[kTotals] = hook_fields[kInit] += +!!this[init_symbol];
96    hook_fields[kTotals] += hook_fields[kBefore] += +!!this[before_symbol];
97    hook_fields[kTotals] += hook_fields[kAfter] += +!!this[after_symbol];
98    hook_fields[kTotals] += hook_fields[kDestroy] += +!!this[destroy_symbol];
99    hook_fields[kTotals] +=
100        hook_fields[kPromiseResolve] += +!!this[promise_resolve_symbol];
101    hooks_array.push(this);
102
103    if (prev_kTotals === 0 && hook_fields[kTotals] > 0) {
104      enableHooks();
105    }
106
107    return this;
108  }
109
110  disable() {
111    const [hooks_array, hook_fields] = getHookArrays();
112
113    const index = hooks_array.indexOf(this);
114    if (index === -1)
115      return this;
116
117    const prev_kTotals = hook_fields[kTotals];
118
119    hook_fields[kTotals] = hook_fields[kInit] -= +!!this[init_symbol];
120    hook_fields[kTotals] += hook_fields[kBefore] -= +!!this[before_symbol];
121    hook_fields[kTotals] += hook_fields[kAfter] -= +!!this[after_symbol];
122    hook_fields[kTotals] += hook_fields[kDestroy] -= +!!this[destroy_symbol];
123    hook_fields[kTotals] +=
124        hook_fields[kPromiseResolve] -= +!!this[promise_resolve_symbol];
125    hooks_array.splice(index, 1);
126
127    if (prev_kTotals > 0 && hook_fields[kTotals] === 0) {
128      disableHooks();
129    }
130
131    return this;
132  }
133}
134
135
136function createHook(fns) {
137  return new AsyncHook(fns);
138}
139
140
141// Embedder API //
142
143const destroyedSymbol = Symbol('destroyed');
144
145class AsyncResource {
146  constructor(type, opts = {}) {
147    validateString(type, 'type');
148
149    let triggerAsyncId = opts;
150    let requireManualDestroy = false;
151    if (typeof opts !== 'number') {
152      triggerAsyncId = opts.triggerAsyncId === undefined ?
153        getDefaultTriggerAsyncId() : opts.triggerAsyncId;
154      requireManualDestroy = !!opts.requireManualDestroy;
155    }
156
157    // Unlike emitInitScript, AsyncResource doesn't supports null as the
158    // triggerAsyncId.
159    if (!NumberIsSafeInteger(triggerAsyncId) || triggerAsyncId < -1) {
160      throw new ERR_INVALID_ASYNC_ID('triggerAsyncId', triggerAsyncId);
161    }
162
163    const asyncId = newAsyncId();
164    this[async_id_symbol] = asyncId;
165    this[trigger_async_id_symbol] = triggerAsyncId;
166
167    if (initHooksExist()) {
168      if (enabledHooksExist() && type.length === 0) {
169        throw new ERR_ASYNC_TYPE(type);
170      }
171
172      emitInit(asyncId, type, triggerAsyncId, this);
173    }
174
175    if (!requireManualDestroy && destroyHooksExist()) {
176      // This prop name (destroyed) has to be synchronized with C++
177      const destroyed = { destroyed: false };
178      this[destroyedSymbol] = destroyed;
179      registerDestroyHook(this, asyncId, destroyed);
180    }
181  }
182
183  runInAsyncScope(fn, thisArg, ...args) {
184    const asyncId = this[async_id_symbol];
185    emitBefore(asyncId, this[trigger_async_id_symbol], this);
186
187    try {
188      const ret = thisArg === undefined ?
189        fn(...args) :
190        ReflectApply(fn, thisArg, args);
191
192      return ret;
193    } finally {
194      if (hasAsyncIdStack())
195        emitAfter(asyncId);
196    }
197  }
198
199  emitDestroy() {
200    if (this[destroyedSymbol] !== undefined) {
201      this[destroyedSymbol].destroyed = true;
202    }
203    emitDestroy(this[async_id_symbol]);
204    return this;
205  }
206
207  asyncId() {
208    return this[async_id_symbol];
209  }
210
211  triggerAsyncId() {
212    return this[trigger_async_id_symbol];
213  }
214
215  bind(fn) {
216    if (typeof fn !== 'function')
217      throw new ERR_INVALID_ARG_TYPE('fn', 'Function', fn);
218    const ret = this.runInAsyncScope.bind(this, fn);
219    ObjectDefineProperties(ret, {
220      'length': {
221        configurable: true,
222        enumerable: false,
223        value: fn.length,
224        writable: false,
225      },
226      'asyncResource': {
227        configurable: true,
228        enumerable: true,
229        value: this,
230        writable: true,
231      }
232    });
233    return ret;
234  }
235
236  static bind(fn, type) {
237    type = type || fn.name;
238    return (new AsyncResource(type || 'bound-anonymous-fn')).bind(fn);
239  }
240}
241
242const storageList = [];
243const storageHook = createHook({
244  init(asyncId, type, triggerAsyncId, resource) {
245    const currentResource = executionAsyncResource();
246    // Value of currentResource is always a non null object
247    for (let i = 0; i < storageList.length; ++i) {
248      storageList[i]._propagate(resource, currentResource);
249    }
250  }
251});
252
253const defaultAlsResourceOpts = { requireManualDestroy: true };
254class AsyncLocalStorage {
255  constructor() {
256    this.kResourceStore = Symbol('kResourceStore');
257    this.enabled = false;
258  }
259
260  disable() {
261    if (this.enabled) {
262      this.enabled = false;
263      // If this.enabled, the instance must be in storageList
264      storageList.splice(storageList.indexOf(this), 1);
265      if (storageList.length === 0) {
266        storageHook.disable();
267      }
268    }
269  }
270
271  _enable() {
272    if (!this.enabled) {
273      this.enabled = true;
274      storageList.push(this);
275      storageHook.enable();
276    }
277  }
278
279  // Propagate the context from a parent resource to a child one
280  _propagate(resource, triggerResource) {
281    const store = triggerResource[this.kResourceStore];
282    if (this.enabled) {
283      resource[this.kResourceStore] = store;
284    }
285  }
286
287  enterWith(store) {
288    this._enable();
289    const resource = executionAsyncResource();
290    resource[this.kResourceStore] = store;
291  }
292
293  run(store, callback, ...args) {
294    // Avoid creation of an AsyncResource if store is already active
295    if (ObjectIs(store, this.getStore())) {
296      return callback(...args);
297    }
298    const resource = new AsyncResource('AsyncLocalStorage',
299                                       defaultAlsResourceOpts);
300    // Calling emitDestroy before runInAsyncScope avoids a try/finally
301    // It is ok because emitDestroy only schedules calling the hook
302    return resource.emitDestroy().runInAsyncScope(() => {
303      this.enterWith(store);
304      return callback(...args);
305    });
306  }
307
308  exit(callback, ...args) {
309    if (!this.enabled) {
310      return callback(...args);
311    }
312    this.disable();
313    try {
314      return callback(...args);
315    } finally {
316      this._enable();
317    }
318  }
319
320  getStore() {
321    if (this.enabled) {
322      const resource = executionAsyncResource();
323      return resource[this.kResourceStore];
324    }
325  }
326}
327
328// Placing all exports down here because the exported classes won't export
329// otherwise.
330module.exports = {
331  // Public API
332  AsyncLocalStorage,
333  createHook,
334  executionAsyncId,
335  triggerAsyncId,
336  executionAsyncResource,
337  // Embedder API
338  AsyncResource,
339};
340