ladybird/Userland/Libraries/LibWeb/WebIDL/Promise.cpp
Andreas Kling ffac32d20e LibWeb: Use JS::HeapFunction for WebIDL promise reaction steps
Switching away from SafeFunction immediately backfired here, as we're
dealing with two layers of captures, not one.

Let's do the correct fix, which is to use HeapFunction. This makes the
API and its behavior explicit, and keeps captures alive as long as the
HeapFunction is alive.

Fixes #23819.
2024-04-03 18:14:33 +02:00

297 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Function.h>
#include <AK/TypeCasts.h>
#include <LibJS/Heap/HeapFunction.h>
#include <LibJS/Runtime/PromiseCapability.h>
#include <LibJS/Runtime/PromiseConstructor.h>
#include <LibJS/Runtime/Realm.h>
#include <LibWeb/Bindings/ExceptionOrUtils.h>
#include <LibWeb/Bindings/HostDefined.h>
#include <LibWeb/HTML/Scripting/ExceptionReporter.h>
#include <LibWeb/WebIDL/Promise.h>
namespace Web::WebIDL {
// https://webidl.spec.whatwg.org/#a-new-promise
JS::NonnullGCPtr<Promise> create_promise(JS::Realm& realm)
{
auto& vm = realm.vm();
// 1. Let constructor be realm.[[Intrinsics]].[[%Promise%]].
auto constructor = realm.intrinsics().promise_constructor();
// Return ? NewPromiseCapability(constructor).
// NOTE: When called with %Promise%, NewPromiseCapability can't throw.
return MUST(JS::new_promise_capability(vm, constructor));
}
// https://webidl.spec.whatwg.org/#a-promise-resolved-with
JS::NonnullGCPtr<Promise> create_resolved_promise(JS::Realm& realm, JS::Value value)
{
auto& vm = realm.vm();
// 1. Let value be the result of converting x to an ECMAScript value.
// 2. Let constructor be realm.[[Intrinsics]].[[%Promise%]].
auto constructor = realm.intrinsics().promise_constructor();
// 3. Let promiseCapability be ? NewPromiseCapability(constructor).
// NOTE: When called with %Promise%, NewPromiseCapability can't throw.
auto promise_capability = MUST(JS::new_promise_capability(vm, constructor));
// 4. Perform ! Call(promiseCapability.[[Resolve]], undefined, « value »).
MUST(JS::call(vm, *promise_capability->resolve(), JS::js_undefined(), value));
// 5. Return promiseCapability.
return promise_capability;
}
// https://webidl.spec.whatwg.org/#a-promise-rejected-with
JS::NonnullGCPtr<Promise> create_rejected_promise(JS::Realm& realm, JS::Value reason)
{
auto& vm = realm.vm();
// 1. Let constructor be realm.[[Intrinsics]].[[%Promise%]].
auto constructor = realm.intrinsics().promise_constructor();
// 2. Let promiseCapability be ? NewPromiseCapability(constructor).
// NOTE: When called with %Promise%, NewPromiseCapability can't throw.
auto promise_capability = MUST(JS::new_promise_capability(vm, constructor));
// 3. Perform ! Call(promiseCapability.[[Reject]], undefined, « r »).
MUST(JS::call(vm, *promise_capability->reject(), JS::js_undefined(), reason));
// 4. Return promiseCapability.
return promise_capability;
}
// https://webidl.spec.whatwg.org/#resolve
void resolve_promise(JS::Realm& realm, Promise const& promise, JS::Value value)
{
auto& vm = realm.vm();
// 1. If x is not given, then let it be the undefined value.
// NOTE: This is done via the default argument.
// 2. Let value be the result of converting x to an ECMAScript value.
// 3. Perform ! Call(p.[[Resolve]], undefined, « value »).
MUST(JS::call(vm, *promise.resolve(), JS::js_undefined(), value));
}
// https://webidl.spec.whatwg.org/#reject
void reject_promise(JS::Realm& realm, Promise const& promise, JS::Value reason)
{
auto& vm = realm.vm();
// 1. Perform ! Call(p.[[Reject]], undefined, « r »).
MUST(JS::call(vm, *promise.reject(), JS::js_undefined(), reason));
}
// https://webidl.spec.whatwg.org/#dfn-perform-steps-once-promise-is-settled
JS::NonnullGCPtr<JS::Promise> react_to_promise(Promise const& promise, JS::GCPtr<ReactionSteps> on_fulfilled_callback, JS::GCPtr<ReactionSteps> on_rejected_callback)
{
auto& realm = promise.promise()->shape().realm();
auto& vm = realm.vm();
// 1. Let onFulfilledSteps be the following steps given argument V:
auto on_fulfilled_steps = [on_fulfilled_callback = move(on_fulfilled_callback)](JS::VM& vm) -> JS::ThrowCompletionOr<JS::Value> {
// 1. Let value be the result of converting V to an IDL value of type T.
auto value = vm.argument(0);
// 2. If there is a set of steps to be run if the promise was fulfilled, then let result be the result of performing them, given value if T is not undefined. Otherwise, let result be value.
auto result = on_fulfilled_callback
? TRY(Bindings::throw_dom_exception_if_needed(vm, [&] { return on_fulfilled_callback->function()(value); }))
: value;
// 3. Return result, converted to an ECMAScript value.
return result;
};
// 2. Let onFulfilled be CreateBuiltinFunction(onFulfilledSteps, « »):
auto on_fulfilled = JS::NativeFunction::create(realm, move(on_fulfilled_steps), 1, "");
// 3. Let onRejectedSteps be the following steps given argument R:
auto on_rejected_steps = [&realm, on_rejected_callback = move(on_rejected_callback)](JS::VM& vm) -> JS::ThrowCompletionOr<JS::Value> {
// 1. Let reason be the result of converting R to an IDL value of type any.
auto reason = vm.argument(0);
// 2. If there is a set of steps to be run if the promise was rejected, then let result be the result of performing them, given reason. Otherwise, let result be a promise rejected with reason.
auto result = on_rejected_callback
? TRY(Bindings::throw_dom_exception_if_needed(vm, [&] { return on_rejected_callback->function()(reason); }))
: WebIDL::create_rejected_promise(realm, reason)->promise();
// 3. Return result, converted to an ECMAScript value.
return result;
};
// 4. Let onRejected be CreateBuiltinFunction(onRejectedSteps, « »):
auto on_rejected = JS::NativeFunction::create(realm, move(on_rejected_steps), 1, "");
// 5. Let constructor be promise.[[Promise]].[[Realm]].[[Intrinsics]].[[%Promise%]].
auto constructor = realm.intrinsics().promise_constructor();
// 6. Let newCapability be ? NewPromiseCapability(constructor).
// NOTE: When called with %Promise%, NewPromiseCapability can't throw.
auto new_capability = MUST(JS::new_promise_capability(vm, constructor));
// 7. Return PerformPromiseThen(promise.[[Promise]], onFulfilled, onRejected, newCapability).
auto promise_object = verify_cast<JS::Promise>(promise.promise().ptr());
auto value = promise_object->perform_then(on_fulfilled, on_rejected, new_capability);
return verify_cast<JS::Promise>(value.as_object());
}
// https://webidl.spec.whatwg.org/#upon-fulfillment
JS::NonnullGCPtr<JS::Promise> upon_fulfillment(Promise const& promise, JS::NonnullGCPtr<ReactionSteps> steps)
{
// 1. Return the result of reacting to promise:
return react_to_promise(promise,
// - If promise was fulfilled with value v, then:
// 1. Perform steps with v.
steps,
{});
}
// https://webidl.spec.whatwg.org/#upon-rejection
JS::NonnullGCPtr<JS::Promise> upon_rejection(Promise const& promise, JS::NonnullGCPtr<ReactionSteps> steps)
{
// 1. Return the result of reacting to promise:
return react_to_promise(promise, {},
// - If promise was rejected with reason r, then:
// 1. Perform steps with r.
steps);
}
// https://webidl.spec.whatwg.org/#mark-a-promise-as-handled
void mark_promise_as_handled(Promise const& promise)
{
// To mark as handled a Promise<T> promise, set promise.[[Promise]].[[PromiseIsHandled]] to true.
auto promise_object = verify_cast<JS::Promise>(promise.promise().ptr());
promise_object->set_is_handled();
}
struct WaitForAllResults : JS::Cell {
JS_CELL(WaitForAllResults, JS::Cell);
WaitForAllResults(JS::NonnullGCPtr<JS::HeapFunction<void(Vector<JS::Value> const&)>> s, size_t t)
: success_steps(s)
, total(t)
{
// 8. Let result be a list containing total null values.
result.ensure_capacity(total);
for (size_t i = 0; i < total; ++i)
result.unchecked_append(JS::js_null());
}
virtual void visit_edges(JS::Cell::Visitor& visitor) override
{
Base::visit_edges(visitor);
visitor.visit(success_steps);
for (auto& value : result) {
visitor.visit(value);
}
}
JS::NonnullGCPtr<JS::HeapFunction<void(Vector<JS::Value> const&)>> success_steps;
Vector<JS::Value> result;
size_t total = 0;
size_t fulfilled_count = 0;
};
// https://webidl.spec.whatwg.org/#wait-for-all
void wait_for_all(JS::Realm& realm, Vector<JS::NonnullGCPtr<Promise>> const& promises, Function<void(Vector<JS::Value> const&)> success_steps, Function<void(JS::Value)> failure_steps)
{
// FIXME: Fix spec typo, fullfilled --> fulfilled
// 1. Let fullfilledCount be 0.
// Handled later in WaitForAllResults
// 2. Let rejected be false.
auto rejected = false;
// 3. Let rejectionHandlerSteps be the following steps given arg:
auto rejection_handler_steps = [rejected, failure_steps = JS::create_heap_function(realm.heap(), move(failure_steps))](JS::VM& vm) mutable -> JS::ThrowCompletionOr<JS::Value> {
// 1. If rejected is true, abort these steps.
if (rejected)
return JS::js_undefined();
// 2. Set rejected to true.
rejected = true;
// 3. Perform failureSteps given arg.
failure_steps->function()(vm.argument(0));
return JS::js_undefined();
};
// 4. Let rejectionHandler be CreateBuiltinFunction(rejectionHandlerSteps, « »):
auto rejection_handler = JS::NativeFunction::create(realm, move(rejection_handler_steps), 1, "");
// 5. Let total be promisess size.
auto total = promises.size();
// 6. If total is 0, then:
if (total == 0) {
// 1. Queue a microtask to perform successSteps given « ».
HTML::queue_a_microtask(nullptr, [success_steps = JS::create_heap_function(realm.heap(), move(success_steps))] {
success_steps->function()({});
});
// 2. Return.
return;
}
// 7. Let index be 0.
auto index = 0;
// 8. Let result be a list containing total null values.
// Handled in WaitForAllResults
auto results = realm.heap().allocate<WaitForAllResults>(realm, JS::create_heap_function(realm.heap(), move(success_steps)), total);
// 9. For each promise of promises:
for (auto const& promise : promises) {
// 1. Let promiseIndex be index.
auto promise_index = index;
// FIXME: This should be fulfillmentHandlerSteps
// 2. Let fulfillmentHandler be the following steps given arg:
auto fulfillment_handler_steps = [results, promise_index](JS::VM& vm) -> JS::ThrowCompletionOr<JS::Value> {
auto arg = vm.argument(0);
// 1. Set result[promiseIndex] to arg.
results->result[promise_index] = arg;
// 2. Set fullfilledCount to fullfilledCount + 1.
++results->fulfilled_count;
// 3. If fullfilledCount equals total, then perform successSteps given result.
if (results->fulfilled_count == results->total)
results->success_steps->function()(results->result);
return JS::js_undefined();
};
// 3. Let fulfillmentHandler be CreateBuiltinFunction(fulfillmentHandler, « »):
auto fulfillment_handler = JS::NativeFunction::create(realm, move(fulfillment_handler_steps), 1, "");
// 4. Perform PerformPromiseThen(promise, fulfillmentHandler, rejectionHandler).
static_cast<JS::Promise&>(*promise->promise()).perform_then(fulfillment_handler, rejection_handler, nullptr);
// 5. Set index to index + 1.
++index;
}
}
JS::NonnullGCPtr<JS::Promise> create_rejected_promise_from_exception(JS::Realm& realm, Exception exception)
{
auto throw_completion = Bindings::dom_exception_to_throw_completion(realm.vm(), move(exception));
auto promise_capability = WebIDL::create_rejected_promise(realm, *throw_completion.value());
return JS::NonnullGCPtr { verify_cast<JS::Promise>(*promise_capability->promise().ptr()) };
}
}