mirror of
https://github.com/nodejs/node.git
synced 2024-11-21 10:59:27 +00:00
v8: multi-tenant promise hook api
PR-URL: https://github.com/nodejs/node/pull/39283 Reviewed-By: Gerhard Stöbich <deb2001-github@yahoo.de> Reviewed-By: Vladimir de Turckheim <vlad2t@hotmail.com> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
This commit is contained in:
parent
b2aff85485
commit
da82844493
264
doc/api/v8.md
264
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)`
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* `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)`
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* `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)`
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* `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)`
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* `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)`
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* `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
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
125
lib/internal/promise_hooks.js
Normal file
125
lib/internal/promise_hooks.js
Normal file
@ -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
|
||||
};
|
@ -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,
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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',
|
||||
|
82
test/parallel/test-promise-hook-create-hook.js
Normal file
82
test/parallel/test-promise-hook-create-hook.js
Normal file
@ -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);
|
31
test/parallel/test-promise-hook-exceptions.js
Normal file
31
test/parallel/test-promise-hook-exceptions.js
Normal file
@ -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);
|
29
test/parallel/test-promise-hook-on-after.js
Normal file
29
test/parallel/test-promise-hook-on-after.js
Normal file
@ -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);
|
27
test/parallel/test-promise-hook-on-before.js
Normal file
27
test/parallel/test-promise-hook-on-before.js
Normal file
@ -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);
|
37
test/parallel/test-promise-hook-on-init.js
Normal file
37
test/parallel/test-promise-hook-on-init.js
Normal file
@ -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);
|
59
test/parallel/test-promise-hook-on-resolve.js
Normal file
59
test/parallel/test-promise-hook-on-resolve.js
Normal file
@ -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(() => {});
|
||||
}
|
Loading…
Reference in New Issue
Block a user