/src/serenity/Userland/Libraries/LibWeb/WebIDL/Promise.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <AK/Function.h> |
8 | | #include <AK/TypeCasts.h> |
9 | | #include <LibJS/Heap/HeapFunction.h> |
10 | | #include <LibJS/Runtime/PromiseCapability.h> |
11 | | #include <LibJS/Runtime/PromiseConstructor.h> |
12 | | #include <LibJS/Runtime/Realm.h> |
13 | | #include <LibWeb/Bindings/ExceptionOrUtils.h> |
14 | | #include <LibWeb/Bindings/HostDefined.h> |
15 | | #include <LibWeb/HTML/Scripting/ExceptionReporter.h> |
16 | | #include <LibWeb/WebIDL/Promise.h> |
17 | | |
18 | | namespace Web::WebIDL { |
19 | | |
20 | | // https://webidl.spec.whatwg.org/#a-new-promise |
21 | | JS::NonnullGCPtr<Promise> create_promise(JS::Realm& realm) |
22 | 0 | { |
23 | 0 | auto& vm = realm.vm(); |
24 | | |
25 | | // 1. Let constructor be realm.[[Intrinsics]].[[%Promise%]]. |
26 | 0 | auto constructor = realm.intrinsics().promise_constructor(); |
27 | | |
28 | | // Return ? NewPromiseCapability(constructor). |
29 | | // NOTE: When called with %Promise%, NewPromiseCapability can't throw. |
30 | 0 | return MUST(JS::new_promise_capability(vm, constructor)); |
31 | 0 | } |
32 | | |
33 | | // https://webidl.spec.whatwg.org/#a-promise-resolved-with |
34 | | JS::NonnullGCPtr<Promise> create_resolved_promise(JS::Realm& realm, JS::Value value) |
35 | 0 | { |
36 | 0 | auto& vm = realm.vm(); |
37 | | |
38 | | // 1. Let value be the result of converting x to an ECMAScript value. |
39 | | |
40 | | // 2. Let constructor be realm.[[Intrinsics]].[[%Promise%]]. |
41 | 0 | auto constructor = realm.intrinsics().promise_constructor(); |
42 | | |
43 | | // 3. Let promiseCapability be ? NewPromiseCapability(constructor). |
44 | | // NOTE: When called with %Promise%, NewPromiseCapability can't throw. |
45 | 0 | auto promise_capability = MUST(JS::new_promise_capability(vm, constructor)); |
46 | | |
47 | | // 4. Perform ! Call(promiseCapability.[[Resolve]], undefined, « value »). |
48 | 0 | MUST(JS::call(vm, *promise_capability->resolve(), JS::js_undefined(), value)); |
49 | | |
50 | | // 5. Return promiseCapability. |
51 | 0 | return promise_capability; |
52 | 0 | } |
53 | | |
54 | | // https://webidl.spec.whatwg.org/#a-promise-rejected-with |
55 | | JS::NonnullGCPtr<Promise> create_rejected_promise(JS::Realm& realm, JS::Value reason) |
56 | 0 | { |
57 | 0 | auto& vm = realm.vm(); |
58 | | |
59 | | // 1. Let constructor be realm.[[Intrinsics]].[[%Promise%]]. |
60 | 0 | auto constructor = realm.intrinsics().promise_constructor(); |
61 | | |
62 | | // 2. Let promiseCapability be ? NewPromiseCapability(constructor). |
63 | | // NOTE: When called with %Promise%, NewPromiseCapability can't throw. |
64 | 0 | auto promise_capability = MUST(JS::new_promise_capability(vm, constructor)); |
65 | | |
66 | | // 3. Perform ! Call(promiseCapability.[[Reject]], undefined, « r »). |
67 | 0 | MUST(JS::call(vm, *promise_capability->reject(), JS::js_undefined(), reason)); |
68 | | |
69 | | // 4. Return promiseCapability. |
70 | 0 | return promise_capability; |
71 | 0 | } |
72 | | |
73 | | // https://webidl.spec.whatwg.org/#resolve |
74 | | void resolve_promise(JS::Realm& realm, Promise const& promise, JS::Value value) |
75 | 0 | { |
76 | 0 | auto& vm = realm.vm(); |
77 | | |
78 | | // 1. If x is not given, then let it be the undefined value. |
79 | | // NOTE: This is done via the default argument. |
80 | | |
81 | | // 2. Let value be the result of converting x to an ECMAScript value. |
82 | | // 3. Perform ! Call(p.[[Resolve]], undefined, « value »). |
83 | 0 | MUST(JS::call(vm, *promise.resolve(), JS::js_undefined(), value)); |
84 | 0 | } |
85 | | |
86 | | // https://webidl.spec.whatwg.org/#reject |
87 | | void reject_promise(JS::Realm& realm, Promise const& promise, JS::Value reason) |
88 | 0 | { |
89 | 0 | auto& vm = realm.vm(); |
90 | | |
91 | | // 1. Perform ! Call(p.[[Reject]], undefined, « r »). |
92 | 0 | MUST(JS::call(vm, *promise.reject(), JS::js_undefined(), reason)); |
93 | 0 | } |
94 | | |
95 | | // https://webidl.spec.whatwg.org/#dfn-perform-steps-once-promise-is-settled |
96 | | JS::NonnullGCPtr<JS::Promise> react_to_promise(Promise const& promise, JS::GCPtr<ReactionSteps> on_fulfilled_callback, JS::GCPtr<ReactionSteps> on_rejected_callback) |
97 | 0 | { |
98 | 0 | auto& realm = promise.promise()->shape().realm(); |
99 | 0 | auto& vm = realm.vm(); |
100 | | |
101 | | // 1. Let onFulfilledSteps be the following steps given argument V: |
102 | 0 | auto on_fulfilled_steps = [on_fulfilled_callback = move(on_fulfilled_callback)](JS::VM& vm) -> JS::ThrowCompletionOr<JS::Value> { |
103 | | // 1. Let value be the result of converting V to an IDL value of type T. |
104 | 0 | auto value = vm.argument(0); |
105 | | |
106 | | // 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. |
107 | 0 | auto result = on_fulfilled_callback |
108 | 0 | ? TRY(Bindings::throw_dom_exception_if_needed(vm, [&] { return on_fulfilled_callback->function()(value); })) |
109 | 0 | : value; |
110 | | |
111 | | // 3. Return result, converted to an ECMAScript value. |
112 | 0 | return result; |
113 | 0 | }; |
114 | | |
115 | | // 2. Let onFulfilled be CreateBuiltinFunction(onFulfilledSteps, « »): |
116 | 0 | auto on_fulfilled = JS::NativeFunction::create(realm, move(on_fulfilled_steps), 1, ""); |
117 | | |
118 | | // 3. Let onRejectedSteps be the following steps given argument R: |
119 | 0 | auto on_rejected_steps = [&realm, on_rejected_callback = move(on_rejected_callback)](JS::VM& vm) -> JS::ThrowCompletionOr<JS::Value> { |
120 | | // 1. Let reason be the result of converting R to an IDL value of type any. |
121 | 0 | auto reason = vm.argument(0); |
122 | | |
123 | | // 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. |
124 | 0 | auto result = on_rejected_callback |
125 | 0 | ? TRY(Bindings::throw_dom_exception_if_needed(vm, [&] { return on_rejected_callback->function()(reason); })) |
126 | 0 | : WebIDL::create_rejected_promise(realm, reason)->promise(); |
127 | | |
128 | | // 3. Return result, converted to an ECMAScript value. |
129 | 0 | return result; |
130 | 0 | }; |
131 | | |
132 | | // 4. Let onRejected be CreateBuiltinFunction(onRejectedSteps, « »): |
133 | 0 | auto on_rejected = JS::NativeFunction::create(realm, move(on_rejected_steps), 1, ""); |
134 | | |
135 | | // 5. Let constructor be promise.[[Promise]].[[Realm]].[[Intrinsics]].[[%Promise%]]. |
136 | 0 | auto constructor = realm.intrinsics().promise_constructor(); |
137 | | |
138 | | // 6. Let newCapability be ? NewPromiseCapability(constructor). |
139 | | // NOTE: When called with %Promise%, NewPromiseCapability can't throw. |
140 | 0 | auto new_capability = MUST(JS::new_promise_capability(vm, constructor)); |
141 | | |
142 | | // 7. Return PerformPromiseThen(promise.[[Promise]], onFulfilled, onRejected, newCapability). |
143 | 0 | auto promise_object = verify_cast<JS::Promise>(promise.promise().ptr()); |
144 | 0 | auto value = promise_object->perform_then(on_fulfilled, on_rejected, new_capability); |
145 | 0 | return verify_cast<JS::Promise>(value.as_object()); |
146 | 0 | } |
147 | | |
148 | | // https://webidl.spec.whatwg.org/#upon-fulfillment |
149 | | JS::NonnullGCPtr<JS::Promise> upon_fulfillment(Promise const& promise, JS::NonnullGCPtr<ReactionSteps> steps) |
150 | 0 | { |
151 | | // 1. Return the result of reacting to promise: |
152 | 0 | return react_to_promise(promise, |
153 | | // - If promise was fulfilled with value v, then: |
154 | | // 1. Perform steps with v. |
155 | 0 | steps, |
156 | 0 | {}); |
157 | 0 | } |
158 | | |
159 | | // https://webidl.spec.whatwg.org/#upon-rejection |
160 | | JS::NonnullGCPtr<JS::Promise> upon_rejection(Promise const& promise, JS::NonnullGCPtr<ReactionSteps> steps) |
161 | 0 | { |
162 | | // 1. Return the result of reacting to promise: |
163 | 0 | return react_to_promise(promise, {}, |
164 | | // - If promise was rejected with reason r, then: |
165 | | // 1. Perform steps with r. |
166 | 0 | steps); |
167 | 0 | } |
168 | | |
169 | | // https://webidl.spec.whatwg.org/#mark-a-promise-as-handled |
170 | | void mark_promise_as_handled(Promise const& promise) |
171 | 0 | { |
172 | | // To mark as handled a Promise<T> promise, set promise.[[Promise]].[[PromiseIsHandled]] to true. |
173 | 0 | auto promise_object = verify_cast<JS::Promise>(promise.promise().ptr()); |
174 | 0 | promise_object->set_is_handled(); |
175 | 0 | } |
176 | | |
177 | | struct WaitForAllResults : JS::Cell { |
178 | | JS_CELL(WaitForAllResults, JS::Cell); |
179 | | JS_DECLARE_ALLOCATOR(WaitForAllResults); |
180 | | |
181 | | WaitForAllResults(JS::NonnullGCPtr<JS::HeapFunction<void(Vector<JS::Value> const&)>> s, size_t t) |
182 | 0 | : success_steps(s) |
183 | 0 | , total(t) |
184 | 0 | { |
185 | | // 8. Let result be a list containing total null values. |
186 | 0 | result.ensure_capacity(total); |
187 | 0 | for (size_t i = 0; i < total; ++i) |
188 | 0 | result.unchecked_append(JS::js_null()); |
189 | 0 | } |
190 | | |
191 | | virtual void visit_edges(JS::Cell::Visitor& visitor) override |
192 | 0 | { |
193 | 0 | Base::visit_edges(visitor); |
194 | 0 | visitor.visit(success_steps); |
195 | 0 | visitor.visit(result); |
196 | 0 | } |
197 | | |
198 | | JS::NonnullGCPtr<JS::HeapFunction<void(Vector<JS::Value> const&)>> success_steps; |
199 | | Vector<JS::Value> result; |
200 | | size_t total = 0; |
201 | | size_t fulfilled_count = 0; |
202 | | }; |
203 | | |
204 | | JS_DEFINE_ALLOCATOR(WaitForAllResults); |
205 | | |
206 | | // https://webidl.spec.whatwg.org/#wait-for-all |
207 | | 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) |
208 | 0 | { |
209 | | // FIXME: Fix spec typo, fullfilled --> fulfilled |
210 | | // 1. Let fullfilledCount be 0. |
211 | | // Handled later in WaitForAllResults |
212 | | |
213 | | // 2. Let rejected be false. |
214 | 0 | auto rejected = false; |
215 | | |
216 | | // 3. Let rejectionHandlerSteps be the following steps given arg: |
217 | 0 | auto rejection_handler_steps = [rejected, failure_steps = JS::create_heap_function(realm.heap(), move(failure_steps))](JS::VM& vm) mutable -> JS::ThrowCompletionOr<JS::Value> { |
218 | | // 1. If rejected is true, abort these steps. |
219 | 0 | if (rejected) |
220 | 0 | return JS::js_undefined(); |
221 | | |
222 | | // 2. Set rejected to true. |
223 | 0 | rejected = true; |
224 | | |
225 | | // 3. Perform failureSteps given arg. |
226 | 0 | failure_steps->function()(vm.argument(0)); |
227 | |
|
228 | 0 | return JS::js_undefined(); |
229 | 0 | }; |
230 | | |
231 | | // 4. Let rejectionHandler be CreateBuiltinFunction(rejectionHandlerSteps, « »): |
232 | 0 | auto rejection_handler = JS::NativeFunction::create(realm, move(rejection_handler_steps), 1, ""); |
233 | | |
234 | | // 5. Let total be promises’s size. |
235 | 0 | auto total = promises.size(); |
236 | | |
237 | | // 6. If total is 0, then: |
238 | 0 | if (total == 0) { |
239 | | // 1. Queue a microtask to perform successSteps given « ». |
240 | 0 | HTML::queue_a_microtask(nullptr, JS::create_heap_function(realm.heap(), [success_steps = JS::create_heap_function(realm.heap(), move(success_steps))] { |
241 | 0 | success_steps->function()({}); |
242 | 0 | })); |
243 | | |
244 | | // 2. Return. |
245 | 0 | return; |
246 | 0 | } |
247 | | |
248 | | // 7. Let index be 0. |
249 | 0 | auto index = 0; |
250 | | |
251 | | // 8. Let result be a list containing total null values. |
252 | | // Handled in WaitForAllResults |
253 | |
|
254 | 0 | auto results = realm.heap().allocate<WaitForAllResults>(realm, JS::create_heap_function(realm.heap(), move(success_steps)), total); |
255 | | |
256 | | // 9. For each promise of promises: |
257 | 0 | for (auto const& promise : promises) { |
258 | | // 1. Let promiseIndex be index. |
259 | 0 | auto promise_index = index; |
260 | | |
261 | | // FIXME: This should be fulfillmentHandlerSteps |
262 | | // 2. Let fulfillmentHandler be the following steps given arg: |
263 | 0 | auto fulfillment_handler_steps = [results, promise_index](JS::VM& vm) -> JS::ThrowCompletionOr<JS::Value> { |
264 | 0 | auto arg = vm.argument(0); |
265 | | |
266 | | // 1. Set result[promiseIndex] to arg. |
267 | 0 | results->result[promise_index] = arg; |
268 | | |
269 | | // 2. Set fullfilledCount to fullfilledCount + 1. |
270 | 0 | ++results->fulfilled_count; |
271 | | |
272 | | // 3. If fullfilledCount equals total, then perform successSteps given result. |
273 | 0 | if (results->fulfilled_count == results->total) |
274 | 0 | results->success_steps->function()(results->result); |
275 | |
|
276 | 0 | return JS::js_undefined(); |
277 | 0 | }; |
278 | | |
279 | | // 3. Let fulfillmentHandler be CreateBuiltinFunction(fulfillmentHandler, « »): |
280 | 0 | auto fulfillment_handler = JS::NativeFunction::create(realm, move(fulfillment_handler_steps), 1, ""); |
281 | | |
282 | | // 4. Perform PerformPromiseThen(promise, fulfillmentHandler, rejectionHandler). |
283 | 0 | static_cast<JS::Promise&>(*promise->promise()).perform_then(fulfillment_handler, rejection_handler, nullptr); |
284 | | |
285 | | // 5. Set index to index + 1. |
286 | 0 | ++index; |
287 | 0 | } |
288 | 0 | } |
289 | | |
290 | | JS::NonnullGCPtr<JS::Promise> create_rejected_promise_from_exception(JS::Realm& realm, Exception exception) |
291 | 0 | { |
292 | 0 | auto throw_completion = Bindings::dom_exception_to_throw_completion(realm.vm(), move(exception)); |
293 | 0 | auto promise_capability = WebIDL::create_rejected_promise(realm, *throw_completion.value()); |
294 | 0 | return JS::NonnullGCPtr { verify_cast<JS::Promise>(*promise_capability->promise().ptr()) }; |
295 | 0 | } |
296 | | |
297 | | } |