1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-25 15:29:32 -05:00

Reland "feat: add "unhandledrejection" event support" (#15211)

This commit is contained in:
Bartek Iwańczuk 2022-07-20 20:28:19 +02:00 committed by GitHub
parent 6e350b2b7c
commit d53936eb7d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 249 additions and 19 deletions

View file

@ -174,6 +174,7 @@ fn run_exec_time(
benchmark_file, benchmark_file,
"--warmup", "--warmup",
"3", "3",
"--show-output",
] ]
.iter() .iter()
.map(|s| s.to_string()) .map(|s| s.to_string())

View file

@ -400,6 +400,17 @@ declare class ErrorEvent extends Event {
constructor(type: string, eventInitDict?: ErrorEventInit); constructor(type: string, eventInitDict?: ErrorEventInit);
} }
interface PromiseRejectionEventInit extends EventInit {
promise: Promise<any>;
reason?: any;
}
declare class PromiseRejectionEvent extends Event {
readonly promise: Promise<any>;
readonly reason: any;
constructor(type: string, eventInitDict?: PromiseRejectionEventInit);
}
interface AbstractWorkerEventMap { interface AbstractWorkerEventMap {
"error": ErrorEvent; "error": ErrorEvent;
} }

View file

@ -2782,3 +2782,8 @@ itest!(followup_dyn_import_resolved {
args: "run --unstable --allow-read followup_dyn_import_resolves/main.ts", args: "run --unstable --allow-read followup_dyn_import_resolves/main.ts",
output: "followup_dyn_import_resolves/main.ts.out", output: "followup_dyn_import_resolves/main.ts.out",
}); });
itest!(unhandled_rejection {
args: "run --allow-read unhandled_rejection.js",
output: "unhandled_rejection.js.out",
});

View file

@ -0,0 +1,11 @@
globalThis.addEventListener("unhandledrejection", (e) => {
console.log("unhandled rejection at:", e.promise, "reason:", e.reason);
e.preventDefault();
});
function Foo() {
this.bar = Promise.reject(new Error("bar not available"));
}
new Foo();
Promise.reject();

View file

@ -0,0 +1,8 @@
unhandled rejection at: Promise {
<rejected> Error: bar not available
at new Foo (file:///[WILDCARD]/testdata/unhandled_rejection.js:7:29)
at file:///[WILDCARD]/testdata/unhandled_rejection.js:10:1
} reason: Error: bar not available
at new Foo (file:///[WILDCARD]/testdata/unhandled_rejection.js:7:29)
at file:///[WILDCARD]/testdata/unhandled_rejection.js:10:1
unhandled rejection at: Promise { <rejected> undefined } reason: undefined

View file

@ -268,6 +268,10 @@
terminate: opSync.bind(null, "op_terminate"), terminate: opSync.bind(null, "op_terminate"),
opNames: opSync.bind(null, "op_op_names"), opNames: opSync.bind(null, "op_op_names"),
eventLoopHasMoreWork: opSync.bind(null, "op_event_loop_has_more_work"), eventLoopHasMoreWork: opSync.bind(null, "op_event_loop_has_more_work"),
setPromiseRejectCallback: opSync.bind(
null,
"op_set_promise_reject_callback",
),
}); });
ObjectAssign(globalThis.__bootstrap, { core }); ObjectAssign(globalThis.__bootstrap, { core });

View file

@ -323,14 +323,6 @@ pub extern "C" fn promise_reject_callback(message: v8::PromiseRejectMessage) {
let state_rc = JsRuntime::state(scope); let state_rc = JsRuntime::state(scope);
let mut state = state_rc.borrow_mut(); let mut state = state_rc.borrow_mut();
// Node compat: perform synchronous process.emit("unhandledRejection").
//
// Note the callback follows the (type, promise, reason) signature of Node's
// internal promiseRejectHandler from lib/internal/process/promises.js, not
// the (promise, reason) signature of the "unhandledRejection" event listener.
//
// Short-circuits Deno's regular unhandled rejection logic because that's
// a) asynchronous, and b) always terminates.
if let Some(js_promise_reject_cb) = state.js_promise_reject_cb.clone() { if let Some(js_promise_reject_cb) = state.js_promise_reject_cb.clone() {
let js_uncaught_exception_cb = state.js_uncaught_exception_cb.clone(); let js_uncaught_exception_cb = state.js_uncaught_exception_cb.clone();
@ -338,14 +330,6 @@ pub extern "C" fn promise_reject_callback(message: v8::PromiseRejectMessage) {
let undefined: v8::Local<v8::Value> = v8::undefined(tc_scope).into(); let undefined: v8::Local<v8::Value> = v8::undefined(tc_scope).into();
let type_ = v8::Integer::new(tc_scope, message.get_event() as i32); let type_ = v8::Integer::new(tc_scope, message.get_event() as i32);
let promise = message.get_promise(); let promise = message.get_promise();
if let Some(pending_mod_evaluate) = state.pending_mod_evaluate.as_mut() {
if !pending_mod_evaluate.has_evaluated {
let promise_global = v8::Global::new(tc_scope, promise);
pending_mod_evaluate
.handled_promise_rejections
.push(promise_global);
}
}
drop(state); // Drop borrow, callbacks can call back into runtime. drop(state); // Drop borrow, callbacks can call back into runtime.
let reason = match message.get_event() { let reason = match message.get_event() {
@ -355,14 +339,45 @@ pub extern "C" fn promise_reject_callback(message: v8::PromiseRejectMessage) {
PromiseHandlerAddedAfterReject => undefined, PromiseHandlerAddedAfterReject => undefined,
}; };
let promise_global = v8::Global::new(tc_scope, promise);
let args = &[type_.into(), promise.into(), reason]; let args = &[type_.into(), promise.into(), reason];
js_promise_reject_cb let maybe_has_unhandled_rejection_handler = js_promise_reject_cb
.open(tc_scope) .open(tc_scope)
.call(tc_scope, undefined, args); .call(tc_scope, undefined, args);
let has_unhandled_rejection_handler =
if let Some(value) = maybe_has_unhandled_rejection_handler {
value.is_true()
} else {
false
};
if has_unhandled_rejection_handler {
let mut state = state_rc.borrow_mut();
if let Some(pending_mod_evaluate) = state.pending_mod_evaluate.as_mut() {
if !pending_mod_evaluate.has_evaluated {
pending_mod_evaluate
.handled_promise_rejections
.push(promise_global.clone());
}
}
}
if let Some(exception) = tc_scope.exception() { if let Some(exception) = tc_scope.exception() {
if let Some(js_uncaught_exception_cb) = js_uncaught_exception_cb { if let Some(js_uncaught_exception_cb) = js_uncaught_exception_cb {
tc_scope.reset(); // Cancel pending exception. tc_scope.reset(); // Cancel pending exception.
{
let mut state = state_rc.borrow_mut();
if let Some(pending_mod_evaluate) =
state.pending_mod_evaluate.as_mut()
{
if !pending_mod_evaluate.has_evaluated {
pending_mod_evaluate
.handled_promise_rejections
.push(promise_global);
}
}
}
js_uncaught_exception_cb.open(tc_scope).call( js_uncaught_exception_cb.open(tc_scope).call(
tc_scope, tc_scope,
undefined, undefined,
@ -372,6 +387,7 @@ pub extern "C" fn promise_reject_callback(message: v8::PromiseRejectMessage) {
} }
if tc_scope.has_caught() { if tc_scope.has_caught() {
// TODO(bartlomieju): ensure that TODO provided below is still valid.
// If we get here, an exception was thrown by the unhandledRejection // If we get here, an exception was thrown by the unhandledRejection
// handler and there is ether no uncaughtException handler or the // handler and there is ether no uncaughtException handler or the
// handler threw an exception of its own. // handler threw an exception of its own.
@ -389,7 +405,6 @@ pub extern "C" fn promise_reject_callback(message: v8::PromiseRejectMessage) {
} else { } else {
let promise = message.get_promise(); let promise = message.get_promise();
let promise_global = v8::Global::new(scope, promise); let promise_global = v8::Global::new(scope, promise);
match message.get_event() { match message.get_event() {
PromiseRejectWithNoHandler => { PromiseRejectWithNoHandler => {
let error = message.get_value().unwrap(); let error = message.get_value().unwrap();

View file

@ -48,6 +48,9 @@ pub(crate) fn init_builtins_v8() -> Vec<OpDecl> {
op_apply_source_map::decl(), op_apply_source_map::decl(),
op_set_format_exception_callback::decl(), op_set_format_exception_callback::decl(),
op_event_loop_has_more_work::decl(), op_event_loop_has_more_work::decl(),
op_store_pending_promise_exception::decl(),
op_remove_pending_promise_exception::decl(),
op_has_pending_promise_exception::decl(),
] ]
} }
@ -792,3 +795,48 @@ fn op_set_format_exception_callback<'a>(
fn op_event_loop_has_more_work(scope: &mut v8::HandleScope) -> bool { fn op_event_loop_has_more_work(scope: &mut v8::HandleScope) -> bool {
JsRuntime::event_loop_pending_state(scope).is_pending() JsRuntime::event_loop_pending_state(scope).is_pending()
} }
#[op(v8)]
fn op_store_pending_promise_exception<'a>(
scope: &mut v8::HandleScope<'a>,
promise: serde_v8::Value<'a>,
reason: serde_v8::Value<'a>,
) {
let state_rc = JsRuntime::state(scope);
let mut state = state_rc.borrow_mut();
let promise_value =
v8::Local::<v8::Promise>::try_from(promise.v8_value).unwrap();
let promise_global = v8::Global::new(scope, promise_value);
let error_global = v8::Global::new(scope, reason.v8_value);
state
.pending_promise_exceptions
.insert(promise_global, error_global);
}
#[op(v8)]
fn op_remove_pending_promise_exception<'a>(
scope: &mut v8::HandleScope<'a>,
promise: serde_v8::Value<'a>,
) {
let state_rc = JsRuntime::state(scope);
let mut state = state_rc.borrow_mut();
let promise_value =
v8::Local::<v8::Promise>::try_from(promise.v8_value).unwrap();
let promise_global = v8::Global::new(scope, promise_value);
state.pending_promise_exceptions.remove(&promise_global);
}
#[op(v8)]
fn op_has_pending_promise_exception<'a>(
scope: &mut v8::HandleScope<'a>,
promise: serde_v8::Value<'a>,
) -> bool {
let state_rc = JsRuntime::state(scope);
let state = state_rc.borrow();
let promise_value =
v8::Local::<v8::Promise>::try_from(promise.v8_value).unwrap();
let promise_global = v8::Global::new(scope, promise_value);
state
.pending_promise_exceptions
.contains_key(&promise_global)
}

View file

@ -1278,6 +1278,58 @@
[SymbolToStringTag] = "ProgressEvent"; [SymbolToStringTag] = "ProgressEvent";
} }
class PromiseRejectionEvent extends Event {
#promise = null;
#reason = null;
get promise() {
return this.#promise;
}
get reason() {
return this.#reason;
}
constructor(
type,
{
bubbles,
cancelable,
composed,
promise,
reason,
} = {},
) {
super(type, {
bubbles: bubbles,
cancelable: cancelable,
composed: composed,
});
this.#promise = promise;
this.#reason = reason;
}
[SymbolFor("Deno.privateCustomInspect")](inspect) {
return inspect(consoleInternal.createFilteredInspectProxy({
object: this,
evaluate: this instanceof PromiseRejectionEvent,
keys: [
...EVENT_PROPS,
"promise",
"reason",
],
}));
}
// TODO(lucacasonato): remove when this interface is spec aligned
[SymbolToStringTag] = "PromiseRejectionEvent";
}
defineEnumerableProps(PromiseRejectionEvent, [
"promise",
"reason",
]);
const _eventHandlers = Symbol("eventHandlers"); const _eventHandlers = Symbol("eventHandlers");
function makeWrappedHandler(handler, isSpecialErrorEventHandler) { function makeWrappedHandler(handler, isSpecialErrorEventHandler) {
@ -1426,6 +1478,7 @@
window.MessageEvent = MessageEvent; window.MessageEvent = MessageEvent;
window.CustomEvent = CustomEvent; window.CustomEvent = CustomEvent;
window.ProgressEvent = ProgressEvent; window.ProgressEvent = ProgressEvent;
window.PromiseRejectionEvent = PromiseRejectionEvent;
window.dispatchEvent = EventTarget.prototype.dispatchEvent; window.dispatchEvent = EventTarget.prototype.dispatchEvent;
window.addEventListener = EventTarget.prototype.addEventListener; window.addEventListener = EventTarget.prototype.addEventListener;
window.removeEventListener = EventTarget.prototype.removeEventListener; window.removeEventListener = EventTarget.prototype.removeEventListener;

View file

@ -11,6 +11,10 @@ delete Intl.v8BreakIterator;
((window) => { ((window) => {
const core = Deno.core; const core = Deno.core;
const { const {
ArrayPrototypeIndexOf,
ArrayPrototypePush,
ArrayPrototypeShift,
ArrayPrototypeSplice,
ArrayPrototypeMap, ArrayPrototypeMap,
DateNow, DateNow,
Error, Error,
@ -27,7 +31,11 @@ delete Intl.v8BreakIterator;
SymbolFor, SymbolFor,
SymbolIterator, SymbolIterator,
PromisePrototypeThen, PromisePrototypeThen,
SafeWeakMap,
TypeError, TypeError,
WeakMapPrototypeDelete,
WeakMapPrototypeGet,
WeakMapPrototypeSet,
} = window.__bootstrap.primordials; } = window.__bootstrap.primordials;
const util = window.__bootstrap.util; const util = window.__bootstrap.util;
const eventTarget = window.__bootstrap.eventTarget; const eventTarget = window.__bootstrap.eventTarget;
@ -233,6 +241,7 @@ delete Intl.v8BreakIterator;
function runtimeStart(runtimeOptions, source) { function runtimeStart(runtimeOptions, source) {
core.setMacrotaskCallback(timers.handleTimerMacrotask); core.setMacrotaskCallback(timers.handleTimerMacrotask);
core.setMacrotaskCallback(promiseRejectMacrotaskCallback);
core.setWasmStreamingCallback(fetch.handleWasmStreaming); core.setWasmStreamingCallback(fetch.handleWasmStreaming);
core.opSync("op_set_format_exception_callback", formatException); core.opSync("op_set_format_exception_callback", formatException);
version.setVersions( version.setVersions(
@ -411,6 +420,7 @@ delete Intl.v8BreakIterator;
PerformanceEntry: util.nonEnumerable(performance.PerformanceEntry), PerformanceEntry: util.nonEnumerable(performance.PerformanceEntry),
PerformanceMark: util.nonEnumerable(performance.PerformanceMark), PerformanceMark: util.nonEnumerable(performance.PerformanceMark),
PerformanceMeasure: util.nonEnumerable(performance.PerformanceMeasure), PerformanceMeasure: util.nonEnumerable(performance.PerformanceMeasure),
PromiseRejectionEvent: util.nonEnumerable(PromiseRejectionEvent),
ProgressEvent: util.nonEnumerable(ProgressEvent), ProgressEvent: util.nonEnumerable(ProgressEvent),
ReadableStream: util.nonEnumerable(streams.ReadableStream), ReadableStream: util.nonEnumerable(streams.ReadableStream),
ReadableStreamDefaultReader: util.nonEnumerable( ReadableStreamDefaultReader: util.nonEnumerable(
@ -553,6 +563,63 @@ delete Intl.v8BreakIterator;
postMessage: util.writable(postMessage), postMessage: util.writable(postMessage),
}; };
const pendingRejections = [];
const pendingRejectionsReasons = new SafeWeakMap();
function promiseRejectCallback(type, promise, reason) {
switch (type) {
case 0: {
core.opSync("op_store_pending_promise_exception", promise, reason);
ArrayPrototypePush(pendingRejections, promise);
WeakMapPrototypeSet(pendingRejectionsReasons, promise, reason);
break;
}
case 1: {
core.opSync("op_remove_pending_promise_exception", promise);
const index = ArrayPrototypeIndexOf(pendingRejections, promise);
if (index > -1) {
ArrayPrototypeSplice(pendingRejections, index, 1);
WeakMapPrototypeDelete(pendingRejectionsReasons, promise);
}
break;
}
default:
return false;
}
return !!globalThis.onunhandledrejection ||
eventTarget.listenerCount(globalThis, "unhandledrejection") > 0;
}
function promiseRejectMacrotaskCallback() {
while (pendingRejections.length > 0) {
const promise = ArrayPrototypeShift(pendingRejections);
const hasPendingException = core.opSync(
"op_has_pending_promise_exception",
promise,
);
const reason = WeakMapPrototypeGet(pendingRejectionsReasons, promise);
WeakMapPrototypeDelete(pendingRejectionsReasons, promise);
if (!hasPendingException) {
return;
}
const event = new PromiseRejectionEvent("unhandledrejection", {
cancelable: true,
promise,
reason,
});
globalThis.dispatchEvent(event);
// If event was not prevented we will let Rust side handle it.
if (event.defaultPrevented) {
core.opSync("op_remove_pending_promise_exception", promise);
}
}
return true;
}
let hasBootstrapped = false; let hasBootstrapped = false;
function bootstrapMainRuntime(runtimeOptions) { function bootstrapMainRuntime(runtimeOptions) {
@ -585,6 +652,10 @@ delete Intl.v8BreakIterator;
defineEventHandler(window, "load"); defineEventHandler(window, "load");
defineEventHandler(window, "beforeunload"); defineEventHandler(window, "beforeunload");
defineEventHandler(window, "unload"); defineEventHandler(window, "unload");
defineEventHandler(window, "unhandledrejection");
core.setPromiseRejectCallback(promiseRejectCallback);
const isUnloadDispatched = SymbolFor("isUnloadDispatched"); const isUnloadDispatched = SymbolFor("isUnloadDispatched");
// Stores the flag for checking whether unload is dispatched or not. // Stores the flag for checking whether unload is dispatched or not.
// This prevents the recursive dispatches of unload events. // This prevents the recursive dispatches of unload events.
@ -685,6 +756,10 @@ delete Intl.v8BreakIterator;
defineEventHandler(self, "message"); defineEventHandler(self, "message");
defineEventHandler(self, "error", undefined, true); defineEventHandler(self, "error", undefined, true);
defineEventHandler(self, "unhandledrejection");
core.setPromiseRejectCallback(promiseRejectCallback);
// `Deno.exit()` is an alias to `self.close()`. Setting and exit // `Deno.exit()` is an alias to `self.close()`. Setting and exit
// code using an op in worker context is a no-op. // code using an op in worker context is a no-op.
os.setExitHandler((_exitCode) => { os.setExitHandler((_exitCode) => {

View file

@ -4360,7 +4360,6 @@
"The CanvasPath interface object should be exposed.", "The CanvasPath interface object should be exposed.",
"The TextMetrics interface object should be exposed.", "The TextMetrics interface object should be exposed.",
"The Path2D interface object should be exposed.", "The Path2D interface object should be exposed.",
"The PromiseRejectionEvent interface object should be exposed.",
"The EventSource interface object should be exposed.", "The EventSource interface object should be exposed.",
"The XMLHttpRequestEventTarget interface object should be exposed.", "The XMLHttpRequestEventTarget interface object should be exposed.",
"The XMLHttpRequestUpload interface object should be exposed.", "The XMLHttpRequestUpload interface object should be exposed.",