diff options
author | Guilherme Bernal <guilherme@cubos.io> | 2022-09-28 11:09:33 -0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-09-28 16:09:33 +0200 |
commit | 6c55772f0d5101e0d2b8df3b0653a02e1581122f (patch) | |
tree | c63e36c5a7822f0ad7d620f078604b81f5792b82 | |
parent | b8e3f4c71dae5b43a03e8cfb36e71865e8eeaabf (diff) |
feat(core): add Deno.core.setPromiseHooks (#15475)
-rw-r--r-- | cli/tests/unit/promise_hooks_test.ts | 109 | ||||
-rw-r--r-- | core/01_core.js | 39 | ||||
-rw-r--r-- | core/lib.deno_core.d.ts | 21 | ||||
-rw-r--r-- | core/ops_builtin_v8.rs | 28 |
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. // |