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.
This commit is contained in:
Timothy Flynn 2025-04-15 13:40:46 -04:00 committed by Tim Flynn
parent b324b876f2
commit 525343ba79
Notes: github-actions[bot] 2025-04-16 08:06:27 +00:00
2 changed files with 65 additions and 32 deletions

View file

@ -243,18 +243,10 @@ JS::ThrowCompletionOr<String> 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<JS::Value> this_argument, ExceptionBehavior exception_behavior, GC::RootVector<JS::Value> args)
template<typename ReturnSteps>
static auto invoke_callback_impl(WebIDL::CallbackType& callback, Optional<JS::Value> this_argument, GC::RootVector<JS::Value> args, ReturnSteps&& return_steps)
{
// https://webidl.spec.whatwg.org/#js-invoking-callback-functions
// The exceptionBehavior argument must be supplied if, and only if, callables return type is not a promise type. If callables 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, Optional<JS::Valu
// 3. Let F be the ECMAScript object corresponding to callable.
auto& function_object = callback.callback;
// 5. Let relevant realm be Fs associated realm.
auto& relevant_realm = function_object->shape().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 functions return type.
// FIXME: This does no conversion.
return { JS::js_undefined() };
return return_steps(relevant_realm, JS::js_undefined());
}
// 5. Let relevant realm be Fs associated realm.
auto& relevant_realm = function_object->shape().realm();
// 6. Let stored realm be callables callback context.
auto& stored_realm = callback.callback_context;
@ -291,7 +282,11 @@ JS::Completion invoke_callback(WebIDL::CallbackType& callback, Optional<JS::Valu
auto& vm = function_object->vm();
auto call_result = JS::call(vm, as<JS::FunctionObject>(*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 callables
// 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<JS::Valu
// FIXME: This method follows an older version of the spec, which takes a realm, so we use F's associated realm instead.
HTML::clean_up_after_running_script(relevant_realm);
return return_steps(relevant_realm, move(call_result));
}
}
JS::Completion invoke_callback(WebIDL::CallbackType& callback, Optional<JS::Value> this_argument, ExceptionBehavior exception_behavior, GC::RootVector<JS::Value> args)
{
// https://webidl.spec.whatwg.org/#js-invoking-callback-functions
// The exceptionBehavior argument must be supplied if, and only if, callables return type is not a promise type. If callables 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, Optional<JS::Valu
// 5. If exceptionBehavior is "rethrow", throw completion.[[Value]].
if (exception_behavior == ExceptionBehavior::Rethrow) {
TRY(JS::throw_completion(completion.release_value()));
return JS::throw_completion(completion.release_value());
}
// 6. Otherwise, if exceptionBehavior is "report":
else if (exception_behavior == ExceptionBehavior::Report) {
if (exception_behavior == ExceptionBehavior::Report) {
// FIXME: 1. Assert: callables return type is undefined or any.
// 2. Report an exception completion.[[Value]] for relevant realms global object.
@ -330,20 +341,7 @@ JS::Completion invoke_callback(WebIDL::CallbackType& callback, Optional<JS::Valu
// 9. Return the result of converting rejectedPromise to the callback functions return type.
return JS::Value { rejected_promise->promise() };
};
// 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 callables 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<JS::Value> this_argument, GC::RootVector<JS::Value> args)
@ -351,6 +349,28 @@ JS::Completion invoke_callback(WebIDL::CallbackType& callback, Optional<JS::Valu
return invoke_callback(callback, move(this_argument), ExceptionBehavior::NotSpecified, move(args));
}
// AD-HOC: This may be used as an alternative to WebIDL::invoke_callback when you know the callback returns a promise,
// and the caller needs a WebIDL::Promise rather than a JS::Promise.
GC::Ref<WebIDL::Promise> invoke_promise_callback(WebIDL::CallbackType& callback, Optional<JS::Value> this_argument, GC::RootVector<JS::Value> 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<WebIDL::Promise> {
// 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 functions return type.
return create_rejected_promise(relevant_realm, completion.release_value());
});
}
JS::Completion construct(WebIDL::CallbackType& callback, GC::RootVector<JS::Value> args)
{
// 1. Let completion be an uninitialized variable.

View file

@ -66,6 +66,19 @@ JS::Completion invoke_callback(WebIDL::CallbackType& callback, Optional<JS::Valu
return invoke_callback(callback, move(this_argument), ExceptionBehavior::NotSpecified, forward<Args>(args)...);
}
GC::Ref<WebIDL::Promise> invoke_promise_callback(WebIDL::CallbackType& callback, Optional<JS::Value> this_argument, GC::RootVector<JS::Value> args);
template<typename... Args>
GC::Ref<WebIDL::Promise> invoke_promise_callback(WebIDL::CallbackType& callback, Optional<JS::Value> this_argument, Args&&... args)
{
auto& function_object = callback.callback;
GC::RootVector<JS::Value> arguments_list { function_object->heap() };
(arguments_list.append(forward<Args>(args)), ...);
return invoke_promise_callback(callback, move(this_argument), move(arguments_list));
}
JS::Completion construct(WebIDL::CallbackType& callback, GC::RootVector<JS::Value> args);
// https://webidl.spec.whatwg.org/#construct-a-callback-function