mirror of
https://github.com/denoland/deno.git
synced 2024-11-28 16:20:57 -05:00
feat(core): add Deno.core.setPromiseHooks (#15475)
This commit is contained in:
parent
b8e3f4c71d
commit
6c55772f0d
4 changed files with 197 additions and 0 deletions
109
cli/tests/unit/promise_hooks_test.ts
Normal file
109
cli/tests/unit/promise_hooks_test.ts
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
import { assertEquals } from "./test_util.ts";
|
||||||
|
|
||||||
|
function monitorPromises(outputArray: string[]) {
|
||||||
|
const promiseIds = new Map<Promise<unknown>, string>();
|
||||||
|
|
||||||
|
function identify(promise: Promise<unknown>) {
|
||||||
|
if (!promiseIds.has(promise)) {
|
||||||
|
promiseIds.set(promise, "p" + (promiseIds.size + 1));
|
||||||
|
}
|
||||||
|
return promiseIds.get(promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore: Deno.core allowed
|
||||||
|
Deno.core.setPromiseHooks(
|
||||||
|
(promise: Promise<unknown>, parentPromise?: Promise<unknown>) => {
|
||||||
|
outputArray.push(
|
||||||
|
`init ${identify(promise)}` +
|
||||||
|
(parentPromise ? ` from ${identify(parentPromise)}` : ``),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(promise: Promise<unknown>) => {
|
||||||
|
outputArray.push(`before ${identify(promise)}`);
|
||||||
|
},
|
||||||
|
(promise: Promise<unknown>) => {
|
||||||
|
outputArray.push(`after ${identify(promise)}`);
|
||||||
|
},
|
||||||
|
(promise: Promise<unknown>) => {
|
||||||
|
outputArray.push(`resolve ${identify(promise)}`);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Deno.test(async function promiseHookBasic() {
|
||||||
|
// Bogus await here to ensure any pending promise resolution from the
|
||||||
|
// test runtime has a chance to run and avoid contaminating our results.
|
||||||
|
await Promise.resolve(null);
|
||||||
|
|
||||||
|
const hookResults: string[] = [];
|
||||||
|
monitorPromises(hookResults);
|
||||||
|
|
||||||
|
async function asyncFn() {
|
||||||
|
await Promise.resolve(15);
|
||||||
|
await Promise.resolve(20);
|
||||||
|
Promise.reject(new Error()).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// The function above is equivalent to:
|
||||||
|
// function asyncFn() {
|
||||||
|
// return new Promise(resolve => {
|
||||||
|
// Promise.resolve(15).then(() => {
|
||||||
|
// Promise.resolve(20).then(() => {
|
||||||
|
// Promise.reject(new Error()).catch(() => {});
|
||||||
|
// resolve();
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
await asyncFn();
|
||||||
|
|
||||||
|
assertEquals(hookResults, [
|
||||||
|
"init p1", // Creates the promise representing the return of `asyncFn()`.
|
||||||
|
"init p2", // Creates the promise representing `Promise.resolve(15)`.
|
||||||
|
"resolve p2", // The previous promise resolves to `15` immediately.
|
||||||
|
"init p3 from p2", // Creates the promise that is resolved after the first `await` of the function. Equivalent to `p2.then(...)`.
|
||||||
|
"init p4 from p1", // The resolution above gives time for other pending code to run. Creates the promise that is resolved
|
||||||
|
// from the `await` at `await asyncFn()`, the last code to run. Equivalent to `asyncFn().then(...)`.
|
||||||
|
"before p3", // Begins executing the code after `await Promise.resolve(15)`.
|
||||||
|
"init p5", // Creates the promise representing `Promise.resolve(20)`.
|
||||||
|
"resolve p5", // The previous promise resolves to `20` immediately.
|
||||||
|
"init p6 from p5", // Creates the promise that is resolved after the second `await` of the function. Equivalent to `p5.then(...)`.
|
||||||
|
"resolve p3", // The promise representing the code right after the first await is marked as resolved.
|
||||||
|
"after p3", // We are now after the resolution code of the promise above.
|
||||||
|
"before p6", // Begins executing the code after `await Promise.resolve(20)`.
|
||||||
|
"init p7", // Creates a new promise representing `Promise.reject(new Error())`.
|
||||||
|
"resolve p7", // This promise is "resolved" immediately to a rejection with an error instance.
|
||||||
|
"init p8 from p7", // Creates a new promise for the `.catch` of the previous promise.
|
||||||
|
"resolve p1", // At this point the promise of the function is resolved.
|
||||||
|
"resolve p6", // This concludes the resolution of the code after `await Promise.resolve(20)`.
|
||||||
|
"after p6", // We are now after the resolution code of the promise above.
|
||||||
|
"before p8", // The `.catch` block is pending execution, it begins to execute.
|
||||||
|
"resolve p8", // It does nothing and resolves to `undefined`.
|
||||||
|
"after p8", // We are after the resolution of the `.catch` block.
|
||||||
|
"before p4", // Now we begin the execution of the code that happens after `await asyncFn();`.
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test(async function promiseHookMultipleConsumers() {
|
||||||
|
const hookResultsFirstConsumer: string[] = [];
|
||||||
|
const hookResultsSecondConsumer: string[] = [];
|
||||||
|
|
||||||
|
monitorPromises(hookResultsFirstConsumer);
|
||||||
|
monitorPromises(hookResultsSecondConsumer);
|
||||||
|
|
||||||
|
async function asyncFn() {
|
||||||
|
await Promise.resolve(15);
|
||||||
|
await Promise.resolve(20);
|
||||||
|
Promise.reject(new Error()).catch(() => {});
|
||||||
|
}
|
||||||
|
await asyncFn();
|
||||||
|
|
||||||
|
// Two invocations of `setPromiseHooks` should yield the exact same results, in the same order.
|
||||||
|
assertEquals(
|
||||||
|
hookResultsFirstConsumer,
|
||||||
|
hookResultsSecondConsumer,
|
||||||
|
);
|
||||||
|
});
|
|
@ -12,6 +12,7 @@
|
||||||
Map,
|
Map,
|
||||||
Array,
|
Array,
|
||||||
ArrayPrototypeFill,
|
ArrayPrototypeFill,
|
||||||
|
ArrayPrototypePush,
|
||||||
ArrayPrototypeMap,
|
ArrayPrototypeMap,
|
||||||
ErrorCaptureStackTrace,
|
ErrorCaptureStackTrace,
|
||||||
Promise,
|
Promise,
|
||||||
|
@ -266,6 +267,43 @@
|
||||||
}
|
}
|
||||||
const InterruptedPrototype = Interrupted.prototype;
|
const InterruptedPrototype = Interrupted.prototype;
|
||||||
|
|
||||||
|
const promiseHooks = {
|
||||||
|
init: [],
|
||||||
|
before: [],
|
||||||
|
after: [],
|
||||||
|
resolve: [],
|
||||||
|
hasBeenSet: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
function setPromiseHooks(init, before, after, resolve) {
|
||||||
|
if (init) ArrayPrototypePush(promiseHooks.init, init);
|
||||||
|
if (before) ArrayPrototypePush(promiseHooks.before, before);
|
||||||
|
if (after) ArrayPrototypePush(promiseHooks.after, after);
|
||||||
|
if (resolve) ArrayPrototypePush(promiseHooks.resolve, resolve);
|
||||||
|
|
||||||
|
if (!promiseHooks.hasBeenSet) {
|
||||||
|
promiseHooks.hasBeenSet = true;
|
||||||
|
|
||||||
|
ops.op_set_promise_hooks((promise, parentPromise) => {
|
||||||
|
for (let i = 0; i < promiseHooks.init.length; ++i) {
|
||||||
|
promiseHooks.init[i](promise, parentPromise);
|
||||||
|
}
|
||||||
|
}, (promise) => {
|
||||||
|
for (let i = 0; i < promiseHooks.before.length; ++i) {
|
||||||
|
promiseHooks.before[i](promise);
|
||||||
|
}
|
||||||
|
}, (promise) => {
|
||||||
|
for (let i = 0; i < promiseHooks.after.length; ++i) {
|
||||||
|
promiseHooks.after[i](promise);
|
||||||
|
}
|
||||||
|
}, (promise) => {
|
||||||
|
for (let i = 0; i < promiseHooks.resolve.length; ++i) {
|
||||||
|
promiseHooks.resolve[i](promise);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Extra Deno.core.* exports
|
// Extra Deno.core.* exports
|
||||||
const core = ObjectAssign(globalThis.Deno.core, {
|
const core = ObjectAssign(globalThis.Deno.core, {
|
||||||
opAsync,
|
opAsync,
|
||||||
|
@ -286,6 +324,7 @@
|
||||||
refOp,
|
refOp,
|
||||||
unrefOp,
|
unrefOp,
|
||||||
setReportExceptionCallback,
|
setReportExceptionCallback,
|
||||||
|
setPromiseHooks,
|
||||||
close: (rid) => ops.op_close(rid),
|
close: (rid) => ops.op_close(rid),
|
||||||
tryClose: (rid) => ops.op_try_close(rid),
|
tryClose: (rid) => ops.op_try_close(rid),
|
||||||
read: opAsync.bind(null, "op_read"),
|
read: opAsync.bind(null, "op_read"),
|
||||||
|
|
21
core/lib.deno_core.d.ts
vendored
21
core/lib.deno_core.d.ts
vendored
|
@ -164,5 +164,26 @@ declare namespace Deno {
|
||||||
* enabled.
|
* enabled.
|
||||||
*/
|
*/
|
||||||
const opCallTraces: Map<number, OpCallTrace>;
|
const opCallTraces: Map<number, OpCallTrace>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a callback for the given Promise event. If this function is called
|
||||||
|
* multiple times, the callbacks are called in the order they were added.
|
||||||
|
* - `init_hook` is called when a new promise is created. When a new promise
|
||||||
|
* is created as part of the chain in the case of `Promise.then` or in the
|
||||||
|
* intermediate promises created by `Promise.{race, all}`/`AsyncFunctionAwait`,
|
||||||
|
* we pass the parent promise otherwise we pass undefined.
|
||||||
|
* - `before_hook` is called at the beginning of the promise reaction.
|
||||||
|
* - `after_hook` is called at the end of the promise reaction.
|
||||||
|
* - `resolve_hook` is called at the beginning of resolve or reject function.
|
||||||
|
*/
|
||||||
|
function setPromiseHooks(
|
||||||
|
init_hook?: (
|
||||||
|
promise: Promise<unknown>,
|
||||||
|
parentPromise?: Promise<unknown>,
|
||||||
|
) => void,
|
||||||
|
before_hook?: (promise: Promise<unknown>) => void,
|
||||||
|
after_hook?: (promise: Promise<unknown>) => void,
|
||||||
|
resolve_hook?: (promise: Promise<unknown>) => void,
|
||||||
|
): void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ pub(crate) fn init_builtins_v8() -> Vec<OpDecl> {
|
||||||
op_decode::decl(),
|
op_decode::decl(),
|
||||||
op_serialize::decl(),
|
op_serialize::decl(),
|
||||||
op_deserialize::decl(),
|
op_deserialize::decl(),
|
||||||
|
op_set_promise_hooks::decl(),
|
||||||
op_get_promise_details::decl(),
|
op_get_promise_details::decl(),
|
||||||
op_get_proxy_details::decl(),
|
op_get_proxy_details::decl(),
|
||||||
op_memory_usage::decl(),
|
op_memory_usage::decl(),
|
||||||
|
@ -575,6 +576,33 @@ fn op_get_promise_details<'a>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[op(v8)]
|
||||||
|
fn op_set_promise_hooks<'a>(
|
||||||
|
scope: &mut v8::HandleScope<'a>,
|
||||||
|
init_cb: serde_v8::Value,
|
||||||
|
before_cb: serde_v8::Value,
|
||||||
|
after_cb: serde_v8::Value,
|
||||||
|
resolve_cb: serde_v8::Value,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let init_hook_global = to_v8_fn(scope, init_cb)?;
|
||||||
|
let before_hook_global = to_v8_fn(scope, before_cb)?;
|
||||||
|
let after_hook_global = to_v8_fn(scope, after_cb)?;
|
||||||
|
let resolve_hook_global = to_v8_fn(scope, resolve_cb)?;
|
||||||
|
let init_hook = v8::Local::new(scope, init_hook_global);
|
||||||
|
let before_hook = v8::Local::new(scope, before_hook_global);
|
||||||
|
let after_hook = v8::Local::new(scope, after_hook_global);
|
||||||
|
let resolve_hook = v8::Local::new(scope, resolve_hook_global);
|
||||||
|
|
||||||
|
scope.get_current_context().set_promise_hooks(
|
||||||
|
init_hook,
|
||||||
|
before_hook,
|
||||||
|
after_hook,
|
||||||
|
resolve_hook,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// Based on https://github.com/nodejs/node/blob/1e470510ff74391d7d4ec382909ea8960d2d2fbc/src/node_util.cc
|
// Based on https://github.com/nodejs/node/blob/1e470510ff74391d7d4ec382909ea8960d2d2fbc/src/node_util.cc
|
||||||
// Copyright Joyent, Inc. and other Node contributors.
|
// Copyright Joyent, Inc. and other Node contributors.
|
||||||
//
|
//
|
||||||
|
|
Loading…
Reference in a new issue