node/lib/async_hooks.js
Matteo Collina 2c4356cedf
domain: port to AsyncLocalStorage
Port the domain module from createHook (async_hooks) to
AsyncLocalStorage using the AsyncContextFrame-based implementation.

Key changes:
- Use AsyncLocalStorage for domain context propagation instead of
  async_hooks.createHook()
- Lazy initialization that triggers AsyncContextFrame prototype swap
  on first domain use
- Use enterWith instead of ALS.run() so domain context is NOT
  automatically restored on exception - this matches the original
  domain.run() behavior where exit() only runs on success
- Add ERR_ASYNC_RESOURCE_DOMAIN_REMOVED error for AsyncResource.domain
- Update DEP0097 to End-of-Life status
- Remove tests that relied on the removed MakeCallback domain property

The domain module now uses the AsyncContextFrame version of
AsyncLocalStorage directly for proper context propagation across
async boundaries.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: https://github.com/nodejs/node/pull/61095
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
Reviewed-By: Robert Nagy <ronagy@icloud.com>
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
2026-06-21 19:42:03 +00:00

301 lines
8.5 KiB
JavaScript

'use strict';
const {
ArrayPrototypeIncludes,
ArrayPrototypeIndexOf,
ArrayPrototypePush,
ArrayPrototypeSplice,
ArrayPrototypeUnshift,
FunctionPrototypeBind,
NumberIsSafeInteger,
ObjectDefineProperties,
ObjectFreeze,
ReflectApply,
Symbol,
} = primordials;
const {
ERR_ASYNC_CALLBACK,
ERR_ASYNC_RESOURCE_DOMAIN_REMOVED,
ERR_ASYNC_TYPE,
ERR_INVALID_ASYNC_ID,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
} = require('internal/errors').codes;
const {
kEmptyObject,
} = require('internal/util');
const {
validateFunction,
validateString,
} = require('internal/validators');
const internal_async_hooks = require('internal/async_hooks');
const AsyncContextFrame = require('internal/async_context_frame');
// Get functions
// For userland AsyncResources, make sure to emit a destroy event when the
// resource gets gced.
const { registerDestroyHook, kNoPromiseHook } = internal_async_hooks;
const {
asyncWrap,
executionAsyncId,
triggerAsyncId,
// Private API
hasAsyncIdStack,
getHookArrays,
enableHooks,
disableHooks,
updatePromiseHookMode,
executionAsyncResource,
// Internal Embedder API
newAsyncId,
getDefaultTriggerAsyncId,
emitInit,
emitBefore,
emitAfter,
emitDestroy,
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, trackPromises }) {
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');
if (trackPromises !== undefined && typeof trackPromises !== 'boolean') {
throw new ERR_INVALID_ARG_TYPE('trackPromises', 'boolean', trackPromises);
}
this[init_symbol] = init;
this[before_symbol] = before;
this[after_symbol] = after;
this[destroy_symbol] = destroy;
this[promise_resolve_symbol] = promiseResolve;
if (trackPromises === false) {
if (promiseResolve) {
throw new ERR_INVALID_ARG_VALUE('trackPromises',
trackPromises, 'must not be false when promiseResolve is enabled');
}
this[kNoPromiseHook] = true;
} else {
// Default to tracking promises for now.
this[kNoPromiseHook] = false;
}
}
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 { 0: hooks_array, 1: hook_fields } = getHookArrays();
// Each hook is only allowed to be added once.
if (ArrayPrototypeIncludes(hooks_array, 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];
ArrayPrototypePush(hooks_array, this);
if (prev_kTotals === 0 && hook_fields[kTotals] > 0) {
enableHooks();
}
if (!this[kNoPromiseHook]) {
updatePromiseHookMode();
}
return this;
}
disable() {
const { 0: hooks_array, 1: hook_fields } = getHookArrays();
const index = ArrayPrototypeIndexOf(hooks_array, 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];
ArrayPrototypeSplice(hooks_array, 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');
const contextFrameSymbol = Symbol('context_frame');
class AsyncResource {
constructor(type, opts = kEmptyObject) {
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);
}
this[contextFrameSymbol] = AsyncContextFrame.current();
const asyncId = newAsyncId();
this[async_id_symbol] = asyncId;
this[trigger_async_id_symbol] = triggerAsyncId;
if (initHooksExist()) {
if (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);
const contextFrame = this[contextFrameSymbol];
const prior = AsyncContextFrame.exchange(contextFrame);
try {
return ReflectApply(fn, thisArg, args);
} finally {
AsyncContextFrame.set(prior);
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];
}
bind(fn, thisArg) {
validateFunction(fn, 'fn');
let bound;
if (thisArg === undefined) {
const resource = this;
bound = function(...args) {
ArrayPrototypeUnshift(args, fn, this);
return ReflectApply(resource.runInAsyncScope, resource, args);
};
} else {
bound = FunctionPrototypeBind(this.runInAsyncScope, this, fn, thisArg);
}
ObjectDefineProperties(bound, {
'length': {
__proto__: null,
configurable: true,
enumerable: false,
value: fn.length,
writable: false,
},
});
return bound;
}
static bind(fn, type, thisArg) {
type ||= fn.name;
return (new AsyncResource(type || 'bound-anonymous-fn')).bind(fn, thisArg);
}
get domain() {
throw new ERR_ASYNC_RESOURCE_DOMAIN_REMOVED();
}
}
// Placing all exports down here because the exported classes won't export
// otherwise.
module.exports = {
// Public API
get AsyncLocalStorage() {
return AsyncContextFrame.enabled ?
require('internal/async_local_storage/async_context_frame') :
require('internal/async_local_storage/async_hooks');
},
createHook,
executionAsyncId,
triggerAsyncId,
executionAsyncResource,
asyncWrapProviders: ObjectFreeze({ __proto__: null, ...asyncWrap.Providers }),
// Embedder API
AsyncResource,
};