mirror of
https://github.com/RGBCube/serenity
synced 2025-07-27 03:57:44 +00:00
LibJS: Setup host hooks and have promise jobs work out the realm
This allows the host of LibJS (notably LibWeb in this case) to override certain functions such as HostEnqueuePromiseJob, so it can do it's own thing in certain situations. Notably, LibWeb will override HostEnqueuePromiseJob to put promise jobs on the microtask queue. This also makes promise jobs use AK::Function instead of JS::NativeFunction. This removes the need to go through a JavaScript function and it more closely matches the spec's idea of "abstract closures"
This commit is contained in:
parent
5aacec65ab
commit
4c1c6ef91c
12 changed files with 202 additions and 172 deletions
|
@ -1,10 +1,12 @@
|
|||
/*
|
||||
* Copyright (c) 2021, Linus Groh <linusg@serenityos.org>
|
||||
* Copyright (c) 2021, Luke Wilde <lukew@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/Debug.h>
|
||||
#include <LibJS/Runtime/AbstractOperations.h>
|
||||
#include <LibJS/Runtime/GlobalObject.h>
|
||||
#include <LibJS/Runtime/JobCallback.h>
|
||||
#include <LibJS/Runtime/Promise.h>
|
||||
|
@ -13,42 +15,30 @@
|
|||
|
||||
namespace JS {
|
||||
|
||||
PromiseReactionJob* PromiseReactionJob::create(GlobalObject& global_object, PromiseReaction& reaction, Value argument)
|
||||
{
|
||||
return global_object.heap().allocate<PromiseReactionJob>(global_object, reaction, argument, *global_object.function_prototype());
|
||||
}
|
||||
|
||||
PromiseReactionJob::PromiseReactionJob(PromiseReaction& reaction, Value argument, Object& prototype)
|
||||
: NativeFunction(prototype)
|
||||
, m_reaction(reaction)
|
||||
, m_argument(argument)
|
||||
{
|
||||
}
|
||||
|
||||
// 27.2.2.1 NewPromiseReactionJob ( reaction, argument ), https://tc39.es/ecma262/#sec-newpromisereactionjob
|
||||
ThrowCompletionOr<Value> PromiseReactionJob::call()
|
||||
static ThrowCompletionOr<Value> run_reaction_job(GlobalObject& global_object, PromiseReaction& reaction, Value argument)
|
||||
{
|
||||
auto& global_object = this->global_object();
|
||||
auto& vm = global_object.vm();
|
||||
|
||||
// a. Let promiseCapability be reaction.[[Capability]].
|
||||
auto& promise_capability = m_reaction.capability();
|
||||
auto& promise_capability = reaction.capability();
|
||||
|
||||
// b. Let type be reaction.[[Type]].
|
||||
auto type = m_reaction.type();
|
||||
auto type = reaction.type();
|
||||
|
||||
// c. Let handler be reaction.[[Handler]].
|
||||
auto handler = m_reaction.handler();
|
||||
auto& handler = reaction.handler();
|
||||
|
||||
Completion handler_result;
|
||||
|
||||
// d. If handler is empty, then
|
||||
if (!handler.has_value()) {
|
||||
dbgln_if(PROMISE_DEBUG, "[PromiseReactionJob @ {}]: Handler is empty", this);
|
||||
dbgln_if(PROMISE_DEBUG, "run_reaction_job: Handler is empty");
|
||||
|
||||
// i. If type is Fulfill, let handlerResult be NormalCompletion(argument).
|
||||
if (type == PromiseReaction::Type::Fulfill) {
|
||||
dbgln_if(PROMISE_DEBUG, "[PromiseReactionJob @ {}]: Reaction type is Type::Fulfill, setting handler result to {}", this, m_argument);
|
||||
handler_result = normal_completion(m_argument);
|
||||
dbgln_if(PROMISE_DEBUG, "run_reaction_job: Reaction type is Type::Fulfill, setting handler result to {}", argument);
|
||||
handler_result = normal_completion(argument);
|
||||
}
|
||||
// ii. Else,
|
||||
else {
|
||||
|
@ -56,14 +46,16 @@ ThrowCompletionOr<Value> PromiseReactionJob::call()
|
|||
VERIFY(type == PromiseReaction::Type::Reject);
|
||||
|
||||
// 2. Let handlerResult be ThrowCompletion(argument).
|
||||
dbgln_if(PROMISE_DEBUG, "[PromiseReactionJob @ {}]: Reaction type is Type::Reject, throwing exception with argument {}", this, m_argument);
|
||||
handler_result = throw_completion(m_argument);
|
||||
dbgln_if(PROMISE_DEBUG, "run_reaction_job: Reaction type is Type::Reject, throwing exception with argument {}", argument);
|
||||
handler_result = throw_completion(argument);
|
||||
}
|
||||
}
|
||||
// e. Else, let handlerResult be HostCallJobCallback(handler, undefined, « argument »).
|
||||
else {
|
||||
dbgln_if(PROMISE_DEBUG, "[PromiseReactionJob @ {}]: Calling handler callback {} @ {} with argument {}", this, handler.value().callback->class_name(), handler.value().callback, m_argument);
|
||||
handler_result = call_job_callback(global_object, handler.value(), js_undefined(), m_argument);
|
||||
dbgln_if(PROMISE_DEBUG, "run_reaction_job: Calling handler callback {} @ {} with argument {}", handler.value().callback.cell()->class_name(), handler.value().callback.cell(), argument);
|
||||
MarkedValueList arguments(vm.heap());
|
||||
arguments.append(argument);
|
||||
handler_result = vm.host_call_job_callback(global_object, handler.value(), js_undefined(), move(arguments));
|
||||
}
|
||||
|
||||
// f. If promiseCapability is undefined, then
|
||||
|
@ -72,7 +64,7 @@ ThrowCompletionOr<Value> PromiseReactionJob::call()
|
|||
VERIFY(!handler_result.is_abrupt());
|
||||
|
||||
// ii. Return NormalCompletion(empty).
|
||||
dbgln_if(PROMISE_DEBUG, "[PromiseReactionJob @ {}]: Reaction has no PromiseCapability, returning empty value", this);
|
||||
dbgln_if(PROMISE_DEBUG, "run_reaction_job: Reaction has no PromiseCapability, returning empty value");
|
||||
// TODO: This can't return an empty value at the moment, because the implicit conversion to Completion would fail.
|
||||
// Change it back when this is using completions (`return normal_completion({})`)
|
||||
return js_undefined();
|
||||
|
@ -84,57 +76,72 @@ ThrowCompletionOr<Value> PromiseReactionJob::call()
|
|||
if (handler_result.is_abrupt()) {
|
||||
// i. Let status be Call(promiseCapability.[[Reject]], undefined, « handlerResult.[[Value]] »).
|
||||
auto* reject_function = promise_capability.value().reject;
|
||||
dbgln_if(PROMISE_DEBUG, "[PromiseReactionJob @ {}]: Calling PromiseCapability's reject function @ {}", this, reject_function);
|
||||
dbgln_if(PROMISE_DEBUG, "run_reaction_job: Calling PromiseCapability's reject function @ {}", reject_function);
|
||||
return JS::call(global_object, *reject_function, js_undefined(), *handler_result.value());
|
||||
}
|
||||
// i. Else,
|
||||
else {
|
||||
// i. Let status be Call(promiseCapability.[[Resolve]], undefined, « handlerResult.[[Value]] »).
|
||||
auto* resolve_function = promise_capability.value().resolve;
|
||||
dbgln_if(PROMISE_DEBUG, "[PromiseReactionJob @ {}]: Calling PromiseCapability's resolve function @ {}", this, resolve_function);
|
||||
dbgln_if(PROMISE_DEBUG, "[PromiseReactionJob]: Calling PromiseCapability's resolve function @ {}", resolve_function);
|
||||
return JS::call(global_object, *resolve_function, js_undefined(), *handler_result.value());
|
||||
}
|
||||
|
||||
// j. Return Completion(status).
|
||||
}
|
||||
|
||||
void PromiseReactionJob::visit_edges(Visitor& visitor)
|
||||
// 27.2.2.1 NewPromiseReactionJob ( reaction, argument ), https://tc39.es/ecma262/#sec-newpromisereactionjob
|
||||
PromiseJob create_promise_reaction_job(GlobalObject& global_object, PromiseReaction& reaction, Value argument)
|
||||
{
|
||||
Base::visit_edges(visitor);
|
||||
visitor.visit(&m_reaction);
|
||||
visitor.visit(m_argument);
|
||||
}
|
||||
// 1. Let job be a new Job Abstract Closure with no parameters that captures reaction and argument and performs the following steps when called:
|
||||
// See run_reaction_job for "the following steps".
|
||||
auto job = [global_object = JS::make_handle(&global_object), reaction = JS::make_handle(&reaction), argument = JS::make_handle(argument)]() mutable {
|
||||
return run_reaction_job(*global_object.cell(), *reaction.cell(), argument.value());
|
||||
};
|
||||
|
||||
PromiseResolveThenableJob* PromiseResolveThenableJob::create(GlobalObject& global_object, Promise& promise_to_resolve, Value thenable, JobCallback then)
|
||||
{
|
||||
// FIXME: A bunch of stuff regarding realms, see step 2-5 in the spec linked below
|
||||
return global_object.heap().allocate<PromiseResolveThenableJob>(global_object, promise_to_resolve, thenable, then, *global_object.function_prototype());
|
||||
}
|
||||
// 2. Let handlerRealm be null.
|
||||
Realm* handler_realm { nullptr };
|
||||
|
||||
PromiseResolveThenableJob::PromiseResolveThenableJob(Promise& promise_to_resolve, Value thenable, JobCallback then, Object& prototype)
|
||||
: NativeFunction(prototype)
|
||||
, m_promise_to_resolve(promise_to_resolve)
|
||||
, m_thenable(thenable)
|
||||
, m_then(then)
|
||||
{
|
||||
// 3. If reaction.[[Handler]] is not empty, then
|
||||
auto& handler = reaction.handler();
|
||||
if (handler.has_value()) {
|
||||
// a. Let getHandlerRealmResult be GetFunctionRealm(reaction.[[Handler]].[[Callback]]).
|
||||
auto get_handler_realm_result = get_function_realm(global_object, *handler->callback.cell());
|
||||
|
||||
// b. If getHandlerRealmResult is a normal completion, set handlerRealm to getHandlerRealmResult.[[Value]].
|
||||
if (!get_handler_realm_result.is_throw_completion()) {
|
||||
handler_realm = get_handler_realm_result.release_value();
|
||||
} else {
|
||||
// c. Else, set handlerRealm to the current Realm Record.
|
||||
handler_realm = global_object.vm().current_realm();
|
||||
}
|
||||
|
||||
// d. NOTE: handlerRealm is never null unless the handler is undefined. When the handler is a revoked Proxy and no ECMAScript code runs, handlerRealm is used to create error objects.
|
||||
}
|
||||
|
||||
// 4. Return the Record { [[Job]]: job, [[Realm]]: handlerRealm }.
|
||||
return { move(job), handler_realm };
|
||||
}
|
||||
|
||||
// 27.2.2.2 NewPromiseResolveThenableJob ( promiseToResolve, thenable, then ), https://tc39.es/ecma262/#sec-newpromiseresolvethenablejob
|
||||
ThrowCompletionOr<Value> PromiseResolveThenableJob::call()
|
||||
static ThrowCompletionOr<Value> run_resolve_thenable_job(GlobalObject& global_object, Promise& promise_to_resolve, Value thenable, JobCallback& then)
|
||||
{
|
||||
auto& global_object = this->global_object();
|
||||
auto& vm = global_object.vm();
|
||||
|
||||
// a. Let resolvingFunctions be CreateResolvingFunctions(promiseToResolve).
|
||||
auto [resolve_function, reject_function] = m_promise_to_resolve.create_resolving_functions();
|
||||
auto [resolve_function, reject_function] = promise_to_resolve.create_resolving_functions();
|
||||
|
||||
// b. Let thenCallResult be HostCallJobCallback(then, thenable, « resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]] »).
|
||||
dbgln_if(PROMISE_DEBUG, "[PromiseResolveThenableJob @ {}]: Calling then job callback for thenable {}", this, &m_thenable);
|
||||
auto then_call_result = call_job_callback(global_object, m_then, m_thenable, &resolve_function, &reject_function);
|
||||
dbgln_if(PROMISE_DEBUG, "run_resolve_thenable_job: Calling then job callback for thenable {}", &thenable);
|
||||
MarkedValueList arguments(vm.heap());
|
||||
arguments.append(Value(&resolve_function));
|
||||
arguments.append(Value(&reject_function));
|
||||
auto then_call_result = vm.host_call_job_callback(global_object, then, thenable, move(arguments));
|
||||
|
||||
// c. If thenCallResult is an abrupt completion, then
|
||||
if (then_call_result.is_error()) {
|
||||
// i. Let status be Call(resolvingFunctions.[[Reject]], undefined, « thenCallResult.[[Value]] »).
|
||||
dbgln_if(PROMISE_DEBUG, "[PromiseResolveThenableJob @ {}]: then_call_result is an abrupt completion, calling reject function with value {}", this, *then_call_result.throw_completion().value());
|
||||
dbgln_if(PROMISE_DEBUG, "run_resolve_thenable_job: then_call_result is an abrupt completion, calling reject function with value {}", *then_call_result.throw_completion().value());
|
||||
auto status = JS::call(global_object, &reject_function, js_undefined(), *then_call_result.throw_completion().value());
|
||||
|
||||
// ii. Return Completion(status).
|
||||
|
@ -142,16 +149,37 @@ ThrowCompletionOr<Value> PromiseResolveThenableJob::call()
|
|||
}
|
||||
|
||||
// d. Return Completion(thenCallResult).
|
||||
dbgln_if(PROMISE_DEBUG, "[PromiseResolveThenableJob @ {}]: Returning then call result {}", this, then_call_result.value());
|
||||
dbgln_if(PROMISE_DEBUG, "run_resolve_thenable_job: Returning then call result {}", then_call_result.value());
|
||||
return then_call_result;
|
||||
}
|
||||
|
||||
void PromiseResolveThenableJob::visit_edges(Visitor& visitor)
|
||||
// 27.2.2.2 NewPromiseResolveThenableJob ( promiseToResolve, thenable, then ), https://tc39.es/ecma262/#sec-newpromiseresolvethenablejob
|
||||
PromiseJob create_promise_resolve_thenable_job(GlobalObject& global_object, Promise& promise_to_resolve, Value thenable, JobCallback then)
|
||||
{
|
||||
Base::visit_edges(visitor);
|
||||
visitor.visit(&m_promise_to_resolve);
|
||||
visitor.visit(m_thenable);
|
||||
visitor.visit(m_then.callback);
|
||||
// 2. Let getThenRealmResult be GetFunctionRealm(then.[[Callback]]).
|
||||
Realm* then_realm { nullptr };
|
||||
auto get_then_realm_result = get_function_realm(global_object, *then.callback.cell());
|
||||
|
||||
// 3. If getThenRealmResult is a normal completion, let thenRealm be getThenRealmResult.[[Value]].
|
||||
if (!get_then_realm_result.is_throw_completion()) {
|
||||
then_realm = get_then_realm_result.release_value();
|
||||
} else {
|
||||
// 4. Else, let thenRealm be the current Realm Record.
|
||||
then_realm = global_object.vm().current_realm();
|
||||
}
|
||||
|
||||
// 5. NOTE: thenRealm is never null. When then.[[Callback]] is a revoked Proxy and no code runs, thenRealm is used to create error objects.
|
||||
VERIFY(then_realm);
|
||||
|
||||
// 1. Let job be a new Job Abstract Closure with no parameters that captures promiseToResolve, thenable, and then and performs the following steps when called:
|
||||
// See PromiseResolveThenableJob::call() for "the following steps".
|
||||
// NOTE: This is done out of order, since `then` is moved into the lambda and `then` would be invalid if it was done at the start.
|
||||
auto job = [global_object = JS::make_handle(&global_object), promise_to_resolve = JS::make_handle(&promise_to_resolve), thenable = JS::make_handle(thenable), then = move(then)]() mutable {
|
||||
return run_resolve_thenable_job(*global_object.cell(), *promise_to_resolve.cell(), thenable.value(), then);
|
||||
};
|
||||
|
||||
// 6. Return the Record { [[Job]]: job, [[Realm]]: thenRealm }.
|
||||
return { move(job), then_realm };
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue