'use strict'; const { NumberIsSafeInteger, ReflectApply, Symbol, } = primordials; const { ERR_ASYNC_CALLBACK, ERR_ASYNC_TYPE, ERR_INVALID_ASYNC_ID } = require('internal/errors').codes; const { validateString } = require('internal/validators'); const internal_async_hooks = require('internal/async_hooks'); // Get functions // For userland AsyncResources, make sure to emit a destroy event when the // resource gets gced. const { registerDestroyHook } = internal_async_hooks; const { executionAsyncId, triggerAsyncId, // Private API hasAsyncIdStack, getHookArrays, enableHooks, disableHooks, executionAsyncResource, // Internal Embedder API newAsyncId, getDefaultTriggerAsyncId, emitInit, emitBefore, emitAfter, emitDestroy, enabledHooksExist, initHooksExist, destroyHooksExist, } = internal_async_hooks; // Get symbols const { async_id_symbol, trigger_async_id_symbol, init_symbol, before_symbol, after_symbol, destroy_symbol, promise_resolve_symbol } = internal_async_hooks.symbols; // Get constants const { kInit, kBefore, kAfter, kDestroy, kTotals, kPromiseResolve, } = internal_async_hooks.constants; // Listener API // class AsyncHook { constructor({ init, before, after, destroy, promiseResolve }) { if (init !== undefined && typeof init !== 'function') throw new ERR_ASYNC_CALLBACK('hook.init'); if (before !== undefined && typeof before !== 'function') throw new ERR_ASYNC_CALLBACK('hook.before'); if (after !== undefined && typeof after !== 'function') throw new ERR_ASYNC_CALLBACK('hook.after'); if (destroy !== undefined && typeof destroy !== 'function') throw new ERR_ASYNC_CALLBACK('hook.destroy'); if (promiseResolve !== undefined && typeof promiseResolve !== 'function') throw new ERR_ASYNC_CALLBACK('hook.promiseResolve'); this[init_symbol] = init; this[before_symbol] = before; this[after_symbol] = after; this[destroy_symbol] = destroy; this[promise_resolve_symbol] = promiseResolve; } enable() { // The set of callbacks for a hook should be the same regardless of whether // enable()/disable() are run during their execution. The following // references are reassigned to the tmp arrays if a hook is currently being // processed. const [hooks_array, hook_fields] = getHookArrays(); // Each hook is only allowed to be added once. if (hooks_array.includes(this)) return this; const prev_kTotals = hook_fields[kTotals]; // createHook() has already enforced that the callbacks are all functions, // so here simply increment the count of whether each callbacks exists or // not. hook_fields[kTotals] = hook_fields[kInit] += +!!this[init_symbol]; hook_fields[kTotals] += hook_fields[kBefore] += +!!this[before_symbol]; hook_fields[kTotals] += hook_fields[kAfter] += +!!this[after_symbol]; hook_fields[kTotals] += hook_fields[kDestroy] += +!!this[destroy_symbol]; hook_fields[kTotals] += hook_fields[kPromiseResolve] += +!!this[promise_resolve_symbol]; hooks_array.push(this); if (prev_kTotals === 0 && hook_fields[kTotals] > 0) { enableHooks(); } return this; } disable() { const [hooks_array, hook_fields] = getHookArrays(); const index = hooks_array.indexOf(this); if (index === -1) return this; const prev_kTotals = hook_fields[kTotals]; hook_fields[kTotals] = hook_fields[kInit] -= +!!this[init_symbol]; hook_fields[kTotals] += hook_fields[kBefore] -= +!!this[before_symbol]; hook_fields[kTotals] += hook_fields[kAfter] -= +!!this[after_symbol]; hook_fields[kTotals] += hook_fields[kDestroy] -= +!!this[destroy_symbol]; hook_fields[kTotals] += hook_fields[kPromiseResolve] -= +!!this[promise_resolve_symbol]; hooks_array.splice(index, 1); if (prev_kTotals > 0 && hook_fields[kTotals] === 0) { disableHooks(); } return this; } } function createHook(fns) { return new AsyncHook(fns); } // Embedder API // const destroyedSymbol = Symbol('destroyed'); class AsyncResource { constructor(type, opts = {}) { validateString(type, 'type'); let triggerAsyncId = opts; let requireManualDestroy = false; if (typeof opts !== 'number') { triggerAsyncId = opts.triggerAsyncId === undefined ? getDefaultTriggerAsyncId() : opts.triggerAsyncId; requireManualDestroy = !!opts.requireManualDestroy; } // Unlike emitInitScript, AsyncResource doesn't supports null as the // triggerAsyncId. if (!NumberIsSafeInteger(triggerAsyncId) || triggerAsyncId < -1) { throw new ERR_INVALID_ASYNC_ID('triggerAsyncId', triggerAsyncId); } const asyncId = newAsyncId(); this[async_id_symbol] = asyncId; this[trigger_async_id_symbol] = triggerAsyncId; if (initHooksExist()) { if (enabledHooksExist() && type.length === 0) { throw new ERR_ASYNC_TYPE(type); } emitInit(asyncId, type, triggerAsyncId, this); } if (!requireManualDestroy && destroyHooksExist()) { // This prop name (destroyed) has to be synchronized with C++ const destroyed = { destroyed: false }; this[destroyedSymbol] = destroyed; registerDestroyHook(this, asyncId, destroyed); } } runInAsyncScope(fn, thisArg, ...args) { const asyncId = this[async_id_symbol]; emitBefore(asyncId, this[trigger_async_id_symbol], this); try { const ret = thisArg === undefined ? fn(...args) : ReflectApply(fn, thisArg, args); return ret; } finally { if (hasAsyncIdStack()) emitAfter(asyncId); } } emitDestroy() { if (this[destroyedSymbol] !== undefined) { this[destroyedSymbol].destroyed = true; } emitDestroy(this[async_id_symbol]); return this; } asyncId() { return this[async_id_symbol]; } triggerAsyncId() { return this[trigger_async_id_symbol]; } } const storageList = []; const storageHook = createHook({ init(asyncId, type, triggerAsyncId, resource) { const currentResource = executionAsyncResource(); // Value of currentResource is always a non null object for (let i = 0; i < storageList.length; ++i) { storageList[i]._propagate(resource, currentResource); } } }); class AsyncLocalStorage { constructor() { this.kResourceStore = Symbol('kResourceStore'); this.enabled = false; } disable() { if (this.enabled) { this.enabled = false; // If this.enabled, the instance must be in storageList storageList.splice(storageList.indexOf(this), 1); if (storageList.length === 0) { storageHook.disable(); } } } // Propagate the context from a parent resource to a child one _propagate(resource, triggerResource) { const store = triggerResource[this.kResourceStore]; if (this.enabled) { resource[this.kResourceStore] = store; } } enterWith(store) { if (!this.enabled) { this.enabled = true; storageList.push(this); storageHook.enable(); } const resource = executionAsyncResource(); resource[this.kResourceStore] = store; } run(store, callback, ...args) { const resource = new AsyncResource('AsyncLocalStorage'); return resource.runInAsyncScope(() => { this.enterWith(store); return callback(...args); }); } exit(callback, ...args) { if (!this.enabled) { return callback(...args); } this.enabled = false; try { return callback(...args); } finally { this.enabled = true; } } getStore() { const resource = executionAsyncResource(); if (this.enabled) { return resource[this.kResourceStore]; } } } // Placing all exports down here because the exported classes won't export // otherwise. module.exports = { // Public API AsyncLocalStorage, createHook, executionAsyncId, triggerAsyncId, executionAsyncResource, // Embedder API AsyncResource, };