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