// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // Copyright Joyent and Node contributors. All rights reserved. MIT license. // TODO(petamoriken): enable prefer-primordials for node polyfills // deno-lint-ignore-file prefer-primordials // deno-lint-ignore camelcase import * as async_wrap from "ext:deno_node/internal_binding/async_wrap.ts"; import { ERR_ASYNC_CALLBACK } from "ext:deno_node/internal/errors.ts"; export { asyncIdSymbol, ownerSymbol, } from "ext:deno_node/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 { 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; } }