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