diff options
Diffstat (limited to 'ext/node/polyfills/internal/async_hooks.ts')
-rw-r--r-- | ext/node/polyfills/internal/async_hooks.ts | 420 |
1 files changed, 420 insertions, 0 deletions
diff --git a/ext/node/polyfills/internal/async_hooks.ts b/ext/node/polyfills/internal/async_hooks.ts new file mode 100644 index 000000000..8bf513e46 --- /dev/null +++ b/ext/node/polyfills/internal/async_hooks.ts @@ -0,0 +1,420 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Copyright Joyent and Node contributors. All rights reserved. MIT license. + +// deno-lint-ignore camelcase +import * as async_wrap from "internal:deno_node/polyfills/internal_binding/async_wrap.ts"; +import { ERR_ASYNC_CALLBACK } from "internal:deno_node/polyfills/internal/errors.ts"; +export { + asyncIdSymbol, + ownerSymbol, +} from "internal:deno_node/polyfills/internal_binding/symbols.ts"; + +interface ActiveHooks { + array: AsyncHook[]; + // deno-lint-ignore camelcase + call_depth: number; + // deno-lint-ignore camelcase + tmp_array: AsyncHook[] | null; + // deno-lint-ignore camelcase + tmp_fields: number[] | null; +} + +// Properties in active_hooks are used to keep track of the set of hooks being +// executed in case another hook is enabled/disabled. The new set of hooks is +// then restored once the active set of hooks is finished executing. +// deno-lint-ignore camelcase +const active_hooks: ActiveHooks = { + // Array of all AsyncHooks that will be iterated whenever an async event + // fires. Using var instead of (preferably const) in order to assign + // active_hooks.tmp_array if a hook is enabled/disabled during hook + // execution. + array: [], + // Use a counter to track nested calls of async hook callbacks and make sure + // the active_hooks.array isn't altered mid execution. + // deno-lint-ignore camelcase + call_depth: 0, + // Use to temporarily store and updated active_hooks.array if the user + // enables or disables a hook while hooks are being processed. If a hook is + // enabled() or disabled() during hook execution then the current set of + // active hooks is duplicated and set equal to active_hooks.tmp_array. Any + // subsequent changes are on the duplicated array. When all hooks have + // completed executing active_hooks.tmp_array is assigned to + // active_hooks.array. + // deno-lint-ignore camelcase + tmp_array: null, + // Keep track of the field counts held in active_hooks.tmp_array. Because the + // async_hook_fields can't be reassigned, store each uint32 in an array that + // is written back to async_hook_fields when active_hooks.array is restored. + // deno-lint-ignore camelcase + tmp_fields: null, +}; + +export const registerDestroyHook = async_wrap.registerDestroyHook; +const { + // deno-lint-ignore camelcase + async_hook_fields, + // deno-lint-ignore camelcase + asyncIdFields: async_id_fields, + newAsyncId, + constants, +} = async_wrap; +export { newAsyncId }; +const { + kInit, + kBefore, + kAfter, + kDestroy, + kPromiseResolve, + kTotals, + kCheck, + kDefaultTriggerAsyncId, + kStackLength, +} = constants; + +// deno-lint-ignore camelcase +const resource_symbol = Symbol("resource"); +// deno-lint-ignore camelcase +export const async_id_symbol = Symbol("trigger_async_id"); +// deno-lint-ignore camelcase +export const trigger_async_id_symbol = Symbol("trigger_async_id"); +// deno-lint-ignore camelcase +export const init_symbol = Symbol("init"); +// deno-lint-ignore camelcase +export const before_symbol = Symbol("before"); +// deno-lint-ignore camelcase +export const after_symbol = Symbol("after"); +// deno-lint-ignore camelcase +export const destroy_symbol = Symbol("destroy"); +// deno-lint-ignore camelcase +export const promise_resolve_symbol = Symbol("promiseResolve"); + +export const symbols = { + // deno-lint-ignore camelcase + async_id_symbol, + // deno-lint-ignore camelcase + trigger_async_id_symbol, + // deno-lint-ignore camelcase + init_symbol, + // deno-lint-ignore camelcase + before_symbol, + // deno-lint-ignore camelcase + after_symbol, + // deno-lint-ignore camelcase + destroy_symbol, + // deno-lint-ignore camelcase + promise_resolve_symbol, +}; + +// deno-lint-ignore no-explicit-any +function lookupPublicResource(resource: any) { + if (typeof resource !== "object" || resource === null) return resource; + // TODO(addaleax): Merge this with owner_symbol and use it across all + // AsyncWrap instances. + const publicResource = resource[resource_symbol]; + if (publicResource !== undefined) { + return publicResource; + } + return resource; +} + +// Used by C++ to call all init() callbacks. Because some state can be setup +// from C++ there's no need to perform all the same operations as in +// emitInitScript. +function emitInitNative( + asyncId: number, + // deno-lint-ignore no-explicit-any + type: any, + triggerAsyncId: number, + // deno-lint-ignore no-explicit-any + resource: any, +) { + active_hooks.call_depth += 1; + resource = lookupPublicResource(resource); + // Use a single try/catch for all hooks to avoid setting up one per iteration. + try { + for (let i = 0; i < active_hooks.array.length; i++) { + if (typeof active_hooks.array[i][init_symbol] === "function") { + active_hooks.array[i][init_symbol]( + asyncId, + type, + triggerAsyncId, + resource, + ); + } + } + } catch (e) { + throw e; + } finally { + active_hooks.call_depth -= 1; + } + + // Hooks can only be restored if there have been no recursive hook calls. + // Also the active hooks do not need to be restored if enable()/disable() + // weren't called during hook execution, in which case active_hooks.tmp_array + // will be null. + if (active_hooks.call_depth === 0 && active_hooks.tmp_array !== null) { + restoreActiveHooks(); + } +} + +function getHookArrays(): [AsyncHook[], number[] | Uint32Array] { + if (active_hooks.call_depth === 0) { + return [active_hooks.array, async_hook_fields]; + } + // If this hook is being enabled while in the middle of processing the array + // of currently active hooks then duplicate the current set of active hooks + // and store this there. This shouldn't fire until the next time hooks are + // processed. + if (active_hooks.tmp_array === null) { + storeActiveHooks(); + } + return [active_hooks.tmp_array!, active_hooks.tmp_fields!]; +} + +function storeActiveHooks() { + active_hooks.tmp_array = active_hooks.array.slice(); + // Don't want to make the assumption that kInit to kDestroy are indexes 0 to + // 4. So do this the long way. + active_hooks.tmp_fields = []; + copyHooks(active_hooks.tmp_fields, async_hook_fields); +} + +function copyHooks( + destination: number[] | Uint32Array, + source: number[] | Uint32Array, +) { + destination[kInit] = source[kInit]; + destination[kBefore] = source[kBefore]; + destination[kAfter] = source[kAfter]; + destination[kDestroy] = source[kDestroy]; + destination[kPromiseResolve] = source[kPromiseResolve]; +} + +// Then restore the correct hooks array in case any hooks were added/removed +// during hook callback execution. +function restoreActiveHooks() { + active_hooks.array = active_hooks.tmp_array!; + copyHooks(async_hook_fields, active_hooks.tmp_fields!); + + active_hooks.tmp_array = null; + active_hooks.tmp_fields = null; +} + +// deno-lint-ignore no-unused-vars +let wantPromiseHook = false; +function enableHooks() { + async_hook_fields[kCheck] += 1; + + // TODO(kt3k): Uncomment this + // setCallbackTrampoline(callbackTrampoline); +} + +function disableHooks() { + async_hook_fields[kCheck] -= 1; + + wantPromiseHook = false; + + // TODO(kt3k): Uncomment the below + // setCallbackTrampoline(); + + // Delay the call to `disablePromiseHook()` because we might currently be + // between the `before` and `after` calls of a Promise. + // TODO(kt3k): Uncomment the below + // enqueueMicrotask(disablePromiseHookIfNecessary); +} + +// Return the triggerAsyncId meant for the constructor calling it. It's up to +// the user to safeguard this call and make sure it's zero'd out when the +// constructor is complete. +export function getDefaultTriggerAsyncId() { + const defaultTriggerAsyncId = + async_id_fields[async_wrap.UidFields.kDefaultTriggerAsyncId]; + // If defaultTriggerAsyncId isn't set, use the executionAsyncId + if (defaultTriggerAsyncId < 0) { + return async_id_fields[async_wrap.UidFields.kExecutionAsyncId]; + } + return defaultTriggerAsyncId; +} + +export function defaultTriggerAsyncIdScope( + triggerAsyncId: number | undefined, + // deno-lint-ignore no-explicit-any + block: (...arg: any[]) => void, + ...args: unknown[] +) { + if (triggerAsyncId === undefined) { + return block.apply(null, args); + } + // CHECK(NumberIsSafeInteger(triggerAsyncId)) + // CHECK(triggerAsyncId > 0) + const oldDefaultTriggerAsyncId = async_id_fields[kDefaultTriggerAsyncId]; + async_id_fields[kDefaultTriggerAsyncId] = triggerAsyncId; + + try { + return block.apply(null, args); + } finally { + async_id_fields[kDefaultTriggerAsyncId] = oldDefaultTriggerAsyncId; + } +} + +function hasHooks(key: number) { + return async_hook_fields[key] > 0; +} + +export function enabledHooksExist() { + return hasHooks(kCheck); +} + +export function initHooksExist() { + return hasHooks(kInit); +} + +export function afterHooksExist() { + return hasHooks(kAfter); +} + +export function destroyHooksExist() { + return hasHooks(kDestroy); +} + +export function promiseResolveHooksExist() { + return hasHooks(kPromiseResolve); +} + +function emitInitScript( + asyncId: number, + // deno-lint-ignore no-explicit-any + type: any, + triggerAsyncId: number, + // deno-lint-ignore no-explicit-any + resource: any, +) { + // Short circuit all checks for the common case. Which is that no hooks have + // been set. Do this to remove performance impact for embedders (and core). + if (!hasHooks(kInit)) { + return; + } + + if (triggerAsyncId === null) { + triggerAsyncId = getDefaultTriggerAsyncId(); + } + + emitInitNative(asyncId, type, triggerAsyncId, resource); +} +export { emitInitScript as emitInit }; + +export function hasAsyncIdStack() { + return hasHooks(kStackLength); +} + +export { constants }; + +type Fn = (...args: unknown[]) => unknown; + +export class AsyncHook { + [init_symbol]: Fn; + [before_symbol]: Fn; + [after_symbol]: Fn; + [destroy_symbol]: Fn; + [promise_resolve_symbol]: Fn; + + constructor({ + init, + before, + after, + destroy, + promiseResolve, + }: { + init: Fn; + before: Fn; + after: Fn; + destroy: Fn; + promiseResolve: Fn; + }) { + 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. + // deno-lint-ignore camelcase + const { 0: hooks_array, 1: hook_fields } = getHookArrays(); + + // Each hook is only allowed to be added once. + if (hooks_array.includes(this)) { + return this; + } + + // deno-lint-ignore camelcase + 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(); + } + + // TODO(kt3k): Uncomment the below + // updatePromiseHookMode(); + + return this; + } + + disable() { + // deno-lint-ignore camelcase + const { 0: hooks_array, 1: hook_fields } = getHookArrays(); + + const index = hooks_array.indexOf(this); + if (index === -1) { + return this; + } + + // deno-lint-ignore camelcase + 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; + } +} |