From fbeb895ca60a73c48b15e57861a1d3aec0806644 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Sun, 4 Jul 2021 17:43:26 -0700 Subject: [PATCH] v8: multi-tenant promise hook api MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/39283 Reviewed-By: Gerhard Stöbich Reviewed-By: Vladimir de Turckheim Reviewed-By: Joyee Cheung Reviewed-By: Benjamin Gruenbaum --- doc/api/v8.md | 264 ++++++++++++++++++ lib/internal/async_hooks.js | 22 +- lib/internal/promise_hooks.js | 125 +++++++++ lib/internal/validators.js | 7 + lib/v8.js | 2 + test/parallel/test-bootstrap-modules.js | 1 + .../parallel/test-promise-hook-create-hook.js | 82 ++++++ test/parallel/test-promise-hook-exceptions.js | 31 ++ test/parallel/test-promise-hook-on-after.js | 29 ++ test/parallel/test-promise-hook-on-before.js | 27 ++ test/parallel/test-promise-hook-on-init.js | 37 +++ test/parallel/test-promise-hook-on-resolve.js | 59 ++++ 12 files changed, 676 insertions(+), 10 deletions(-) create mode 100644 lib/internal/promise_hooks.js create mode 100644 test/parallel/test-promise-hook-create-hook.js create mode 100644 test/parallel/test-promise-hook-exceptions.js create mode 100644 test/parallel/test-promise-hook-on-after.js create mode 100644 test/parallel/test-promise-hook-on-before.js create mode 100644 test/parallel/test-promise-hook-on-init.js create mode 100644 test/parallel/test-promise-hook-on-resolve.js diff --git a/doc/api/v8.md b/doc/api/v8.md index 92edce50fc239d..2ab3d387893167 100644 --- a/doc/api/v8.md +++ b/doc/api/v8.md @@ -564,8 +564,267 @@ added: v8.0.0 A subclass of [`Deserializer`][] corresponding to the format written by [`DefaultSerializer`][]. +## Promise hooks + +The `promiseHooks` interface can be used to track promise lifecycle events. +To track _all_ async activity, see [`async_hooks`][] which internally uses this +module to produce promise lifecycle events in addition to events for other +async resources. For request context management, see [`AsyncLocalStorage`][]. + +```mjs +import { promiseHooks } from 'v8'; + +// There are four lifecycle events produced by promises: + +// The `init` event represents the creation of a promise. This could be a +// direct creation such as with `new Promise(...)` or a continuation such +// as `then()` or `catch()`. It also happens whenever an async function is +// called or does an `await`. If a continuation promise is created, the +// `parent` will be the promise it is a continuation from. +function init(promise, parent) { + console.log('a promise was created', { promise, parent }); +} + +// The `settled` event happens when a promise receives a resolution or +// rejection value. This may happen synchronously such as when using +// `Promise.resolve()` on non-promise input. +function settled(promise) { + console.log('a promise resolved or rejected', { promise }); +} + +// The `before` event runs immediately before a `then()` or `catch()` handler +// runs or an `await` resumes execution. +function before(promise) { + console.log('a promise is about to call a then handler', { promise }); +} + +// The `after` event runs immediately after a `then()` handler runs or when +// an `await` begins after resuming from another. +function after(promise) { + console.log('a promise is done calling a then handler', { promise }); +} + +// Lifecycle hooks may be started and stopped individually +const stopWatchingInits = promiseHooks.onInit(init); +const stopWatchingSettleds = promiseHooks.onSettled(settled); +const stopWatchingBefores = promiseHooks.onBefore(before); +const stopWatchingAfters = promiseHooks.onAfter(after); + +// Or they may be started and stopped in groups +const stopHookSet = promiseHooks.createHook({ + init, + settled, + before, + after +}); + +// To stop a hook, call the function returned at its creation. +stopWatchingInits(); +stopWatchingSettleds(); +stopWatchingBefores(); +stopWatchingAfters(); +stopHookSet(); +``` + +### `promiseHooks.onInit(init)` + + +* `init` {Function} The [`init` callback][] to call when a promise is created. +* Returns: {Function} Call to stop the hook. + +**The `init` hook must be a plain function. Providing an async function will +throw as it would produce an infinite microtask loop.** + +```mjs +import { promiseHooks } from 'v8'; + +const stop = promiseHooks.onInit((promise, parent) => {}); +``` + +```cjs +const { promiseHooks } = require('v8'); + +const stop = promiseHooks.onInit((promise, parent) => {}); +``` + +### `promiseHooks.onSettled(settled)` + + +* `settled` {Function} The [`settled` callback][] to call when a promise + is resolved or rejected. +* Returns: {Function} Call to stop the hook. + +**The `settled` hook must be a plain function. Providing an async function will +throw as it would produce an infinite microtask loop.** + +```mjs +import { promiseHooks } from 'v8'; + +const stop = promiseHooks.onSettled((promise) => {}); +``` + +```cjs +const { promiseHooks } = require('v8'); + +const stop = promiseHooks.onSettled((promise) => {}); +``` + +### `promiseHooks.onBefore(before)` + + +* `before` {Function} The [`before` callback][] to call before a promise + continuation executes. +* Returns: {Function} Call to stop the hook. + +**The `before` hook must be a plain function. Providing an async function will +throw as it would produce an infinite microtask loop.** + +```mjs +import { promiseHooks } from 'v8'; + +const stop = promiseHooks.onBefore((promise) => {}); +``` + +```cjs +const { promiseHooks } = require('v8'); + +const stop = promiseHooks.onBefore((promise) => {}); +``` + +### `promiseHooks.onAfter(after)` + + +* `after` {Function} The [`after` callback][] to call after a promise + continuation executes. +* Returns: {Function} Call to stop the hook. + +**The `after` hook must be a plain function. Providing an async function will +throw as it would produce an infinite microtask loop.** + +```mjs +import { promiseHooks } from 'v8'; + +const stop = promiseHooks.onAfter((promise) => {}); +``` + +```cjs +const { promiseHooks } = require('v8'); + +const stop = promiseHooks.onAfter((promise) => {}); +``` + +### `promiseHooks.createHook(callbacks)` + + +* `callbacks` {Object} The [Hook Callbacks][] to register + * `init` {Function} The [`init` callback][]. + * `before` {Function} The [`before` callback][]. + * `after` {Function} The [`after` callback][]. + * `settled` {Function} The [`settled` callback][]. +* Returns: {Function} Used for disabling hooks + +**The hook callbacks must be plain functions. Providing async functions will +throw as it would produce an infinite microtask loop.** + +Registers functions to be called for different lifetime events of each promise. + +The callbacks `init()`/`before()`/`after()`/`settled()` are called for the +respective events during a promise's lifetime. + +All callbacks are optional. For example, if only promise creation needs to +be tracked, then only the `init` callback needs to be passed. The +specifics of all functions that can be passed to `callbacks` is in the +[Hook Callbacks][] section. + +```mjs +import { promiseHooks } from 'v8'; + +const stopAll = promiseHooks.createHook({ + init(promise, parent) {} +}); +``` + +```cjs +const { promiseHooks } = require('v8'); + +const stopAll = promiseHooks.createHook({ + init(promise, parent) {} +}); +``` + +### Hook callbacks + +Key events in the lifetime of a promise have been categorized into four areas: +creation of a promise, before/after a continuation handler is called or around +an await, and when the promise resolves or rejects. + +While these hooks are similar to those of [`async_hooks`][] they lack a +`destroy` hook. Other types of async resources typically represent sockets or +file descriptors which have a distinct "closed" state to express the `destroy` +lifecycle event while promises remain usable for as long as code can still +reach them. Garbage collection tracking is used to make promises fit into the +`async_hooks` event model, however this tracking is very expensive and they may +not necessarily ever even be garbage collected. + +Because promises are asynchronous resources whose lifecycle is tracked +via the promise hooks mechanism, the `init()`, `before()`, `after()`, and +`settled()` callbacks *must not* be async functions as they create more +promises which would produce an infinite loop. + +While this API is used to feed promise events into [`async_hooks`][], the +ordering between the two is considered undefined. Both APIs are multi-tenant +and therefore could produce events in any order relative to each other. + +#### `init(promise, parent)` + +* `promise` {Promise} The promise being created. +* `parent` {Promise} The promise continued from, if applicable. + +Called when a promise is constructed. This _does not_ mean that corresponding +`before`/`after` events will occur, only that the possibility exists. This will +happen if a promise is created without ever getting a continuation. + +#### `before(promise)` + +* `promise` {Promise} + +Called before a promise continuation executes. This can be in the form of +`then()`, `catch()`, or `finally()` handlers or an `await` resuming. + +The `before` callback will be called 0 to N times. The `before` callback +will typically be called 0 times if no continuation was ever made for the +promise. The `before` callback may be called many times in the case where +many continuations have been made from the same promise. + +#### `after(promise)` + +* `promise` {Promise} + +Called immediately after a promise continuation executes. This may be after a +`then()`, `catch()`, or `finally()` handler or before an `await` after another +`await`. + +#### `settled(promise)` + +* `promise` {Promise} + +Called when the promise receives a resolution or rejection value. This may +occur synchronously in the case of `Promise.resolve()` or `Promise.reject()`. + [HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm +[Hook Callbacks]: #hook_callbacks [V8]: https://developers.google.com/v8/ +[`AsyncLocalStorage`]: async_context.md#class_asynclocalstorage [`Buffer`]: buffer.md [`DefaultDeserializer`]: #class-v8defaultdeserializer [`DefaultSerializer`]: #class-v8defaultserializer @@ -575,15 +834,20 @@ A subclass of [`Deserializer`][] corresponding to the format written by [`GetHeapSpaceStatistics`]: https://v8docs.nodesource.com/node-13.2/d5/dda/classv8_1_1_isolate.html#ac673576f24fdc7a33378f8f57e1d13a4 [`NODE_V8_COVERAGE`]: cli.md#node_v8_coveragedir [`Serializer`]: #class-v8serializer +[`after` callback]: #after_promise +[`async_hooks`]: async_hooks.md +[`before` callback]: #before_promise [`buffer.constants.MAX_LENGTH`]: buffer.md#bufferconstantsmax_length [`deserializer._readHostObject()`]: #deserializer_readhostobject [`deserializer.transferArrayBuffer()`]: #deserializertransferarraybufferid-arraybuffer +[`init` callback]: #init_promise_parent [`serialize()`]: #v8serializevalue [`serializer._getSharedArrayBufferId()`]: #serializer_getsharedarraybufferidsharedarraybuffer [`serializer._writeHostObject()`]: #serializer_writehostobjectobject [`serializer.releaseBuffer()`]: #serializerreleasebuffer [`serializer.transferArrayBuffer()`]: #serializertransferarraybufferid-arraybuffer [`serializer.writeRawBytes()`]: #serializerwriterawbytesbuffer +[`settled` callback]: #settled_promise [`v8.stopCoverage()`]: #v8stopcoverage [`v8.takeCoverage()`]: #v8takecoverage [`vm.Script`]: vm.md#new-vmscriptcode-options diff --git a/lib/internal/async_hooks.js b/lib/internal/async_hooks.js index 9aacf4b3bab7c4..390453ca7b8aa9 100644 --- a/lib/internal/async_hooks.js +++ b/lib/internal/async_hooks.js @@ -8,6 +8,8 @@ const { Symbol, } = primordials; +const promiseHooks = require('internal/promise_hooks'); + const async_wrap = internalBinding('async_wrap'); const { setCallbackTrampoline } = async_wrap; /* async_hook_fields is a Uint32Array wrapping the uint32_t array of @@ -51,8 +53,6 @@ const { executionAsyncResource: executionAsyncResource_, clearAsyncIdStack, } = async_wrap; -// For performance reasons, only track Promises when a hook is enabled. -const { setPromiseHooks } = async_wrap; // 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. @@ -374,6 +374,7 @@ function enableHooks() { async_hook_fields[kCheck] += 1; } +let stopPromiseHook; function updatePromiseHookMode() { wantPromiseHook = true; let initHook; @@ -383,12 +384,13 @@ function updatePromiseHookMode() { } else if (destroyHooksExist()) { initHook = destroyTracking; } - setPromiseHooks( - initHook, - promiseBeforeHook, - promiseAfterHook, - promiseResolveHooksExist() ? promiseResolveHook : undefined, - ); + if (stopPromiseHook) stopPromiseHook(); + stopPromiseHook = promiseHooks.createHook({ + init: initHook, + before: promiseBeforeHook, + after: promiseAfterHook, + settled: promiseResolveHooksExist() ? promiseResolveHook : undefined + }); } function disableHooks() { @@ -402,8 +404,8 @@ function disableHooks() { } function disablePromiseHookIfNecessary() { - if (!wantPromiseHook) { - setPromiseHooks(undefined, undefined, undefined, undefined); + if (!wantPromiseHook && stopPromiseHook) { + stopPromiseHook(); } } diff --git a/lib/internal/promise_hooks.js b/lib/internal/promise_hooks.js new file mode 100644 index 00000000000000..be70879cbcbb93 --- /dev/null +++ b/lib/internal/promise_hooks.js @@ -0,0 +1,125 @@ +'use strict'; + +const { + ArrayPrototypeIndexOf, + ArrayPrototypeSlice, + ArrayPrototypeSplice, + ArrayPrototypePush, + FunctionPrototypeBind +} = primordials; + +const { setPromiseHooks } = internalBinding('async_wrap'); +const { triggerUncaughtException } = internalBinding('errors'); + +const { validatePlainFunction } = require('internal/validators'); + +const hooks = { + init: [], + before: [], + after: [], + settled: [] +}; + +function initAll(promise, parent) { + const hookSet = ArrayPrototypeSlice(hooks.init); + const exceptions = []; + + for (let i = 0; i < hookSet.length; i++) { + const init = hookSet[i]; + try { + init(promise, parent); + } catch (err) { + ArrayPrototypePush(exceptions, err); + } + } + + // Triggering exceptions is deferred to allow other hooks to complete + for (let i = 0; i < exceptions.length; i++) { + const err = exceptions[i]; + triggerUncaughtException(err, false); + } +} + +function makeRunHook(list) { + return (promise) => { + const hookSet = ArrayPrototypeSlice(list); + const exceptions = []; + + for (let i = 0; i < hookSet.length; i++) { + const hook = hookSet[i]; + try { + hook(promise); + } catch (err) { + ArrayPrototypePush(exceptions, err); + } + } + + // Triggering exceptions is deferred to allow other hooks to complete + for (let i = 0; i < exceptions.length; i++) { + const err = exceptions[i]; + triggerUncaughtException(err, false); + } + }; +} + +const beforeAll = makeRunHook(hooks.before); +const afterAll = makeRunHook(hooks.after); +const settledAll = makeRunHook(hooks.settled); + +function maybeFastPath(list, runAll) { + return list.length > 1 ? runAll : list[0]; +} + +function update() { + const init = maybeFastPath(hooks.init, initAll); + const before = maybeFastPath(hooks.before, beforeAll); + const after = maybeFastPath(hooks.after, afterAll); + const settled = maybeFastPath(hooks.settled, settledAll); + setPromiseHooks(init, before, after, settled); +} + +function stop(list, hook) { + const index = ArrayPrototypeIndexOf(list, hook); + if (index >= 0) { + ArrayPrototypeSplice(list, index, 1); + update(); + } +} + +function makeUseHook(name) { + const list = hooks[name]; + return (hook) => { + validatePlainFunction(hook, `${name}Hook`); + ArrayPrototypePush(list, hook); + update(); + return FunctionPrototypeBind(stop, null, list, hook); + }; +} + +const onInit = makeUseHook('init'); +const onBefore = makeUseHook('before'); +const onAfter = makeUseHook('after'); +const onSettled = makeUseHook('settled'); + +function createHook({ init, before, after, settled } = {}) { + const hooks = []; + + if (init) ArrayPrototypePush(hooks, onInit(init)); + if (before) ArrayPrototypePush(hooks, onBefore(before)); + if (after) ArrayPrototypePush(hooks, onAfter(after)); + if (settled) ArrayPrototypePush(hooks, onSettled(settled)); + + return () => { + for (const stop of hooks) { + stop(); + } + }; +} + +module.exports = { + createHook, + onInit, + onBefore, + onAfter, + onSettled +}; diff --git a/lib/internal/validators.js b/lib/internal/validators.js index 81329160f07323..9fed5b363db9cb 100644 --- a/lib/internal/validators.js +++ b/lib/internal/validators.js @@ -28,6 +28,7 @@ const { } = require('internal/errors'); const { normalizeEncoding } = require('internal/util'); const { + isAsyncFunction, isArrayBufferView } = require('internal/util/types'); const { signals } = internalBinding('constants').os; @@ -237,6 +238,11 @@ const validateFunction = hideStackFrames((value, name) => { throw new ERR_INVALID_ARG_TYPE(name, 'Function', value); }); +const validatePlainFunction = hideStackFrames((value, name) => { + if (typeof value !== 'function' || isAsyncFunction(value)) + throw new ERR_INVALID_ARG_TYPE(name, 'Function', value); +}); + const validateUndefined = hideStackFrames((value, name) => { if (value !== undefined) throw new ERR_INVALID_ARG_TYPE(name, 'undefined', value); @@ -256,6 +262,7 @@ module.exports = { validateNumber, validateObject, validateOneOf, + validatePlainFunction, validatePort, validateSignalName, validateString, diff --git a/lib/v8.js b/lib/v8.js index a7ae0cabbb3cd9..381baaebb1f909 100644 --- a/lib/v8.js +++ b/lib/v8.js @@ -57,6 +57,7 @@ const { triggerHeapSnapshot } = internalBinding('heap_utils'); const { HeapSnapshotStream } = require('internal/heap_utils'); +const promiseHooks = require('internal/promise_hooks'); /** * Generates a snapshot of the current V8 heap @@ -361,4 +362,5 @@ module.exports = { stopCoverage: profiler.stopCoverage, serialize, writeHeapSnapshot, + promiseHooks, }; diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 9ed1b90e318e7e..581ca701462fa1 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -96,6 +96,7 @@ const expectedModules = new Set([ 'NativeModule internal/process/signal', 'NativeModule internal/process/task_queues', 'NativeModule internal/process/warning', + 'NativeModule internal/promise_hooks', 'NativeModule internal/querystring', 'NativeModule internal/source_map/source_map_cache', 'NativeModule internal/stream_base_commons', diff --git a/test/parallel/test-promise-hook-create-hook.js b/test/parallel/test-promise-hook-create-hook.js new file mode 100644 index 00000000000000..543f67da486065 --- /dev/null +++ b/test/parallel/test-promise-hook-create-hook.js @@ -0,0 +1,82 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { promiseHooks } = require('v8'); + +for (const hook of ['init', 'before', 'after', 'settled']) { + assert.throws(() => { + promiseHooks.createHook({ + [hook]: async function() { } + }); + }, new RegExp(`The "${hook}Hook" argument must be of type function`)); + + assert.throws(() => { + promiseHooks.createHook({ + [hook]: async function*() { } + }); + }, new RegExp(`The "${hook}Hook" argument must be of type function`)); +} + +let init; +let initParent; +let before; +let after; +let settled; + +const stop = promiseHooks.createHook({ + init: common.mustCall((promise, parent) => { + init = promise; + initParent = parent; + }, 3), + before: common.mustCall((promise) => { + before = promise; + }, 2), + after: common.mustCall((promise) => { + after = promise; + }, 1), + settled: common.mustCall((promise) => { + settled = promise; + }, 2) +}); + +// Clears state on each check so only the delta needs to be checked. +function assertState(expectedInit, expectedInitParent, expectedBefore, + expectedAfter, expectedSettled) { + assert.strictEqual(init, expectedInit); + assert.strictEqual(initParent, expectedInitParent); + assert.strictEqual(before, expectedBefore); + assert.strictEqual(after, expectedAfter); + assert.strictEqual(settled, expectedSettled); + init = undefined; + initParent = undefined; + before = undefined; + after = undefined; + settled = undefined; +} + +const parent = Promise.resolve(1); +// After calling `Promise.resolve(...)`, the returned promise should have +// produced an init event with no parent and a settled event. +assertState(parent, undefined, undefined, undefined, parent); + +const child = parent.then(() => { + // When a callback to `promise.then(...)` is called, the promise it resolves + // to should have produced a before event to mark the start of this callback. + assertState(undefined, undefined, child, undefined, undefined); +}); +// After calling `promise.then(...)`, the returned promise should have +// produced an init event with a parent of the promise the `then(...)` +// was called on. +assertState(child, parent); + +const grandChild = child.then(() => { + // Since the check for the before event in the `then(...)` call producing the + // `child` promise, there should have been both a before event for this + // promise but also settled and after events for the `child` promise. + assertState(undefined, undefined, grandChild, child, child); + stop(); +}); +// After calling `promise.then(...)`, the returned promise should have +// produced an init event with a parent of the promise the `then(...)` +// was called on. +assertState(grandChild, child); diff --git a/test/parallel/test-promise-hook-exceptions.js b/test/parallel/test-promise-hook-exceptions.js new file mode 100644 index 00000000000000..d1251502d840b9 --- /dev/null +++ b/test/parallel/test-promise-hook-exceptions.js @@ -0,0 +1,31 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { promiseHooks } = require('v8'); + +const expected = []; + +function testHook(name) { + const hook = promiseHooks[name]; + const error = new Error(`${name} error`); + + const stop = hook(common.mustCall(() => { + stop(); + throw error; + })); + + expected.push(error); +} + +process.on('uncaughtException', common.mustCall((received) => { + assert.strictEqual(received, expected.shift()); +}, 4)); + +testHook('onInit'); +testHook('onSettled'); +testHook('onBefore'); +testHook('onAfter'); + +const stop = promiseHooks.onInit(common.mustCall(() => {}, 2)); + +Promise.resolve().then(stop); diff --git a/test/parallel/test-promise-hook-on-after.js b/test/parallel/test-promise-hook-on-after.js new file mode 100644 index 00000000000000..5785a8c40dc72a --- /dev/null +++ b/test/parallel/test-promise-hook-on-after.js @@ -0,0 +1,29 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { promiseHooks } = require('v8'); + +assert.throws(() => { + promiseHooks.onAfter(async function() { }); +}, /The "afterHook" argument must be of type function/); + +assert.throws(() => { + promiseHooks.onAfter(async function*() { }); +}, /The "afterHook" argument must be of type function/); + +let seen; + +const stop = promiseHooks.onAfter(common.mustCall((promise) => { + seen = promise; +}, 1)); + +const promise = Promise.resolve().then(() => { + assert.strictEqual(seen, undefined); +}); + +promise.then(() => { + assert.strictEqual(seen, promise); + stop(); +}); + +assert.strictEqual(seen, undefined); diff --git a/test/parallel/test-promise-hook-on-before.js b/test/parallel/test-promise-hook-on-before.js new file mode 100644 index 00000000000000..b732bc2494411a --- /dev/null +++ b/test/parallel/test-promise-hook-on-before.js @@ -0,0 +1,27 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { promiseHooks } = require('v8'); + +assert.throws(() => { + promiseHooks.onBefore(async function() { }); +}, /The "beforeHook" argument must be of type function/); + +assert.throws(() => { + promiseHooks.onBefore(async function*() { }); +}, /The "beforeHook" argument must be of type function/); + +let seen; + +const stop = promiseHooks.onBefore(common.mustCall((promise) => { + seen = promise; +}, 1)); + +const promise = Promise.resolve().then(() => { + assert.strictEqual(seen, promise); + stop(); +}); + +promise.then(); + +assert.strictEqual(seen, undefined); diff --git a/test/parallel/test-promise-hook-on-init.js b/test/parallel/test-promise-hook-on-init.js new file mode 100644 index 00000000000000..de49f7f34dc72d --- /dev/null +++ b/test/parallel/test-promise-hook-on-init.js @@ -0,0 +1,37 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { promiseHooks } = require('v8'); + +assert.throws(() => { + promiseHooks.onInit(async function() { }); +}, /The "initHook" argument must be of type function/); + +assert.throws(() => { + promiseHooks.onInit(async function*() { }); +}, /The "initHook" argument must be of type function/); + +let seenPromise; +let seenParent; + +const stop = promiseHooks.onInit(common.mustCall((promise, parent) => { + seenPromise = promise; + seenParent = parent; +}, 2)); + +const parent = Promise.resolve(); +assert.strictEqual(seenPromise, parent); +assert.strictEqual(seenParent, undefined); + +const child = parent.then(); +assert.strictEqual(seenPromise, child); +assert.strictEqual(seenParent, parent); + +seenPromise = undefined; +seenParent = undefined; + +stop(); + +Promise.resolve(); +assert.strictEqual(seenPromise, undefined); +assert.strictEqual(seenParent, undefined); diff --git a/test/parallel/test-promise-hook-on-resolve.js b/test/parallel/test-promise-hook-on-resolve.js new file mode 100644 index 00000000000000..45bfb8ca409d26 --- /dev/null +++ b/test/parallel/test-promise-hook-on-resolve.js @@ -0,0 +1,59 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { promiseHooks } = require('v8'); + +assert.throws(() => { + promiseHooks.onSettled(async function() { }); +}, /The "settledHook" argument must be of type function/); + +assert.throws(() => { + promiseHooks.onSettled(async function*() { }); +}, /The "settledHook" argument must be of type function/); + +let seen; + +const stop = promiseHooks.onSettled(common.mustCall((promise) => { + seen = promise; +}, 4)); + +// Constructor resolve triggers hook +const promise = new Promise((resolve, reject) => { + assert.strictEqual(seen, undefined); + setImmediate(() => { + resolve(); + assert.strictEqual(seen, promise); + seen = undefined; + + constructorReject(); + }); +}); + +// Constructor reject triggers hook +function constructorReject() { + const promise = new Promise((resolve, reject) => { + assert.strictEqual(seen, undefined); + setImmediate(() => { + reject(); + assert.strictEqual(seen, promise); + seen = undefined; + + simpleResolveReject(); + }); + }); + promise.catch(() => {}); +} + +// Sync resolve/reject helpers trigger hook +function simpleResolveReject() { + const resolved = Promise.resolve(); + assert.strictEqual(seen, resolved); + seen = undefined; + + const rejected = Promise.reject(); + assert.strictEqual(seen, rejected); + seen = undefined; + + stop(); + rejected.catch(() => {}); +}