From 525343ba793bcac942565fb01ef3431a76775f54 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Tue, 15 Apr 2025 13:40:46 -0400 Subject: [PATCH] LibWeb: Add an alternative to WebIDL::invoke_callback to return promises When we need the callback to return a promise, we can use this alternate invoker to construct the WebIDL::Promise for us. Currently, the Streams API will use WebIDL::invoke_callback to create a JS::Promise, and then wrap that result in a resolved WebIDL::Promise. This results in rejected JS::Promise instances not being propagated. --- .../LibWeb/WebIDL/AbstractOperations.cpp | 84 ++++++++++++------- Libraries/LibWeb/WebIDL/AbstractOperations.h | 13 +++ 2 files changed, 65 insertions(+), 32 deletions(-) diff --git a/Libraries/LibWeb/WebIDL/AbstractOperations.cpp b/Libraries/LibWeb/WebIDL/AbstractOperations.cpp index 1f57f9c3069..30b993bef6f 100644 --- a/Libraries/LibWeb/WebIDL/AbstractOperations.cpp +++ b/Libraries/LibWeb/WebIDL/AbstractOperations.cpp @@ -243,18 +243,10 @@ JS::ThrowCompletionOr to_usv_string(JS::VM& vm, JS::Value value) // https://webidl.spec.whatwg.org/#invoke-a-callback-function // https://whatpr.org/webidl/1437.html#invoke-a-callback-function -JS::Completion invoke_callback(WebIDL::CallbackType& callback, Optional this_argument, ExceptionBehavior exception_behavior, GC::RootVector args) +template +static auto invoke_callback_impl(WebIDL::CallbackType& callback, Optional this_argument, GC::RootVector args, ReturnSteps&& return_steps) { - // https://webidl.spec.whatwg.org/#js-invoking-callback-functions - // The exceptionBehavior argument must be supplied if, and only if, callable’s return type is not a promise type. If callable’s return type is neither undefined nor any, it must be "rethrow". - // NOTE: Until call sites are updated to respect this, specifications which fail to provide a value here when it would be mandatory should be understood as supplying "rethrow". - if (exception_behavior == ExceptionBehavior::NotSpecified && callback.operation_returns_promise == OperationReturnsPromise::No) - exception_behavior = ExceptionBehavior::Rethrow; - - VERIFY(exception_behavior == ExceptionBehavior::NotSpecified || callback.operation_returns_promise == OperationReturnsPromise::No); - // 1. Let completion be an uninitialized variable. - JS::Completion completion; // 2. If thisArg was not given, let thisArg be undefined. if (!this_argument.has_value()) @@ -263,18 +255,17 @@ JS::Completion invoke_callback(WebIDL::CallbackType& callback, Optionalshape().realm(); + // 4. If ! IsCallable(F) is false: if (!function_object->is_function()) { // 1. Note: This is only possible when the callback function came from an attribute marked with [LegacyTreatNonObjectAsNull]. // 2. Return the result of converting undefined to the callback function’s return type. - // FIXME: This does no conversion. - return { JS::js_undefined() }; + return return_steps(relevant_realm, JS::js_undefined()); } - // 5. Let relevant realm be F’s associated realm. - auto& relevant_realm = function_object->shape().realm(); - // 6. Let stored realm be callable’s callback context. auto& stored_realm = callback.callback_context; @@ -291,7 +282,11 @@ JS::Completion invoke_callback(WebIDL::CallbackType& callback, Optionalvm(); auto call_result = JS::call(vm, as(*function_object), this_argument.value(), args.span()); - auto return_steps = [&](JS::Completion completion) -> JS::Completion { + // 11. If callResult is an abrupt completion, set completion to callResult and jump to the step labeled return. + // 12. Set completion to the result of converting callResult.[[Value]] to an IDL value of the same type as callable’s + // return type. If this throws an exception, set completion to the completion value representing the thrown exception. + // 13. Return: at this point completion will be set to an IDL value or an abrupt completion. + { // 1. Clean up after running a callback with stored realm. HTML::clean_up_after_running_callback(stored_realm); @@ -299,6 +294,21 @@ JS::Completion invoke_callback(WebIDL::CallbackType& callback, Optional this_argument, ExceptionBehavior exception_behavior, GC::RootVector args) +{ + // https://webidl.spec.whatwg.org/#js-invoking-callback-functions + // The exceptionBehavior argument must be supplied if, and only if, callable’s return type is not a promise type. If callable’s return type is neither undefined nor any, it must be "rethrow". + // NOTE: Until call sites are updated to respect this, specifications which fail to provide a value here when it would be mandatory should be understood as supplying "rethrow". + if (exception_behavior == ExceptionBehavior::NotSpecified && callback.operation_returns_promise == OperationReturnsPromise::No) + exception_behavior = ExceptionBehavior::Rethrow; + + VERIFY(exception_behavior == ExceptionBehavior::NotSpecified || callback.operation_returns_promise == OperationReturnsPromise::No); + + return invoke_callback_impl(callback, move(this_argument), move(args), [&](JS::Realm& relevant_realm, JS::Completion completion) -> JS::Completion { // 3. If completion is an IDL value, return completion. if (!completion.is_abrupt()) return completion; @@ -308,10 +318,11 @@ JS::Completion invoke_callback(WebIDL::CallbackType& callback, Optionalpromise() }; - }; - - // 11. If callResult is an abrupt completion, set completion to callResult and jump to the step labeled return. - if (call_result.is_throw_completion()) { - completion = call_result.throw_completion(); - return return_steps(completion); - } - - // 12. Set completion to the result of converting callResult.[[Value]] to an IDL value of the same type as callable’s return type. - // If this throws an exception, set completion to the completion value representing the thrown exception. - // FIXME: This does no conversion. - completion = call_result.value(); - - return return_steps(completion); + }); } JS::Completion invoke_callback(WebIDL::CallbackType& callback, Optional this_argument, GC::RootVector args) @@ -351,6 +349,28 @@ JS::Completion invoke_callback(WebIDL::CallbackType& callback, Optional invoke_promise_callback(WebIDL::CallbackType& callback, Optional this_argument, GC::RootVector args) +{ + VERIFY(callback.operation_returns_promise == OperationReturnsPromise::Yes); + + return invoke_callback_impl(callback, move(this_argument), move(args), [&](JS::Realm& relevant_realm, JS::Completion completion) -> GC::Ref { + // 3. If completion is an IDL value, return completion. + if (!completion.is_abrupt()) + return WebIDL::create_resolved_promise(relevant_realm, completion.release_value()); + + // 4. Assert: completion is an abrupt completion. + VERIFY(completion.is_abrupt()); + + // NOTE: The intermediate steps to handle exception behavior are not relevant for promise-returning callbacks. + + // 8. Let rejectedPromise be ! Call(%Promise.reject%, %Promise%, «completion.[[Value]]»). + // 9. Return the result of converting rejectedPromise to the callback function’s return type. + return create_rejected_promise(relevant_realm, completion.release_value()); + }); +} + JS::Completion construct(WebIDL::CallbackType& callback, GC::RootVector args) { // 1. Let completion be an uninitialized variable. diff --git a/Libraries/LibWeb/WebIDL/AbstractOperations.h b/Libraries/LibWeb/WebIDL/AbstractOperations.h index cb582c592d4..f708538decf 100644 --- a/Libraries/LibWeb/WebIDL/AbstractOperations.h +++ b/Libraries/LibWeb/WebIDL/AbstractOperations.h @@ -66,6 +66,19 @@ JS::Completion invoke_callback(WebIDL::CallbackType& callback, Optional(args)...); } +GC::Ref invoke_promise_callback(WebIDL::CallbackType& callback, Optional this_argument, GC::RootVector args); + +template +GC::Ref invoke_promise_callback(WebIDL::CallbackType& callback, Optional this_argument, Args&&... args) +{ + auto& function_object = callback.callback; + + GC::RootVector arguments_list { function_object->heap() }; + (arguments_list.append(forward(args)), ...); + + return invoke_promise_callback(callback, move(this_argument), move(arguments_list)); +} + JS::Completion construct(WebIDL::CallbackType& callback, GC::RootVector args); // https://webidl.spec.whatwg.org/#construct-a-callback-function