Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): add Deno.core.setPromiseHooks #15475

Merged
merged 14 commits into from
Sep 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions cli/tests/unit/promise_hooks_test.ts
Original file line number Diff line number Diff line change
@@ -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,
);
});
39 changes: 39 additions & 0 deletions core/01_core.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
Map,
Array,
ArrayPrototypeFill,
ArrayPrototypePush,
ArrayPrototypeMap,
ErrorCaptureStackTrace,
Promise,
Expand Down Expand Up @@ -266,6 +267,43 @@
}
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
const core = ObjectAssign(globalThis.Deno.core, {
opAsync,
Expand All @@ -286,6 +324,7 @@
refOp,
unrefOp,
setReportExceptionCallback,
setPromiseHooks,
close: (rid) => ops.op_close(rid),
tryClose: (rid) => ops.op_try_close(rid),
read: opAsync.bind(null, "op_read"),
Expand Down
21 changes: 21 additions & 0 deletions core/lib.deno_core.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,5 +164,26 @@ declare namespace Deno {
* enabled.
*/
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;
}
}
28 changes: 28 additions & 0 deletions core/ops_builtin_v8.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub(crate) fn init_builtins_v8() -> Vec<OpDecl> {
op_decode::decl(),
op_serialize::decl(),
op_deserialize::decl(),
op_set_promise_hooks::decl(),
op_get_promise_details::decl(),
op_get_proxy_details::decl(),
op_memory_usage::decl(),
Expand Down Expand Up @@ -575,6 +576,33 @@ fn op_get_promise_details<'a>(
}
}

#[op(v8)]
fn op_set_promise_hooks<'a>(
lbguilherme marked this conversation as resolved.
Show resolved Hide resolved
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
// Copyright Joyent, Inc. and other Node contributors.
//
Expand Down