summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cli/tests/unit/promise_hooks_test.ts109
-rw-r--r--core/01_core.js39
-rw-r--r--core/lib.deno_core.d.ts21
-rw-r--r--core/ops_builtin_v8.rs28
4 files changed, 197 insertions, 0 deletions
diff --git a/cli/tests/unit/promise_hooks_test.ts b/cli/tests/unit/promise_hooks_test.ts
new file mode 100644
index 000000000..647fa98cb
--- /dev/null
+++ b/cli/tests/unit/promise_hooks_test.ts
@@ -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,
+ );
+});
diff --git a/core/01_core.js b/core/01_core.js
index 08f839c98..655b4219e 100644
--- a/core/01_core.js
+++ b/core/01_core.js
@@ -12,6 +12,7 @@
Map,
Array,
ArrayPrototypeFill,
+ ArrayPrototypePush,
ArrayPrototypeMap,
ErrorCaptureStackTrace,
Promise,
@@ -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,
@@ -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"),
diff --git a/core/lib.deno_core.d.ts b/core/lib.deno_core.d.ts
index c5662794a..7e46d0f14 100644
--- a/core/lib.deno_core.d.ts
+++ b/core/lib.deno_core.d.ts
@@ -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;
}
}
diff --git a/core/ops_builtin_v8.rs b/core/ops_builtin_v8.rs
index 3900c0641..e626c03c1 100644
--- a/core/ops_builtin_v8.rs
+++ b/core/ops_builtin_v8.rs
@@ -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(),
@@ -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
// Copyright Joyent, Inc. and other Node contributors.
//