docs
Loading...
Searching...
No Matches
Task.h
Go to the documentation of this file.
1///
2/// Copyright (C) 2026 Ultralight, Inc. All rights reserved.
3/// A license is required for commercial use. https://ultralig.ht
4///
5/// Coroutines for async JavaScript bindings: js::Task, js::RunOnWorker, and js::Await.
6/// `<Ultralight/JS.h>` includes this header (and with it `<coroutine>`).
7///
8/// ULTRALIGHT_JS_TASK_EH is 1 when the including file is compiled with C++ exceptions. Then an
9/// exception that escapes a Task rejects its Promise.
10///
11#pragma once
12#include <Ultralight/CAPI/CAPI_JSAPI.h>
13#include <Ultralight/CAPI/CAPI_JSRuntime.h>
16#include <Ultralight/js/Value.h>
17#include <Ultralight/js/detail/Coroutine.h>
18
19#include <cassert>
20#include <coroutine>
21#include <type_traits>
22#include <utility>
23
24namespace ultralight {
25namespace js {
26
27///
28/// A coroutine that returns a JavaScript Promise to the page.
29///
30/// A Task is a C++20 coroutine. Returning one from a bound function gives the page a JavaScript
31/// Promise that settles during a later Renderer::Update() after the coroutine finishes.
32///
33/// The coroutine executes sequentially from top to bottom, pausing at each `co_await` without
34/// blocking the Renderer's thread.
35///
36/// Return a Task from a bound function to perform background work and return a Promise:
37///
38/// ```
39/// app["loadSave"] = [](std::string slot) -> js::Task<SaveData> {
40/// SaveData data =
41/// co_await js::RunOnWorker([slot] { return ReadSaveFromDisk(slot); });
42/// if (!data.valid)
43/// co_return js::Error::Make(js::ErrorType::Error, "save is corrupt")
44/// .WithCode("BAD_SAVE");
45/// co_return data;
46/// };
47/// ```
48///
49/// Page script awaits the returned Promise normally:
50///
51/// ```js
52/// const save = await app.loadSave("slot1");
53/// ```
54///
55/// ## JavaScript Equivalents
56///
57/// Task maps standard JavaScript asynchronous syntax directly to C++ coroutine keywords:
58///
59/// | JavaScript | C++ |
60/// |---------------------|----------------------------------|
61/// | `async function` | `[](...) -> js::Task<T>` |
62/// | `return value;` | `co_return value;` |
63/// | `throw new Error()` | `co_return js::Error::Make(...)` |
64/// | `await promise` | `co_await js::Await<T>(promise)` |
65///
66/// ## Awaiting Page Promises
67///
68/// Pass a call to a page function to Await() to invoke it and await its returned Promise:
69///
70/// ```
71/// app["confirm"] = [](js::Value ask) -> js::Task<bool> {
72/// js::Result<bool> answer = co_await js::Await<bool>(ask("Overwrite?"));
73/// co_return answer ? *answer : false;
74/// };
75/// ```
76///
77/// The returned Result holds the resolved value converted to the requested type, or an Error
78/// holding the rejection reason, a type conversion failure, or a page-gone error if the page
79/// navigates away first.
80///
81/// ## Threads and Timing
82///
83/// Coroutines follow these execution and threading rules:
84///
85/// - **The coroutine body runs on the Renderer's thread.** It starts synchronously inside the
86/// page's call up to the first `co_await`, and resumes on the Renderer's thread after each await.
87/// - **Worker threads must not touch JavaScript.** Use RunOnWorker() to run background operations
88/// on the worker pool, passing plain C++ data in and out without touching Value or Context.
89/// - **The Promise settles during a later Renderer::Update().** Completions arrive on the
90/// Renderer's thread when your frame loop updates the renderer.
91/// - **Parameters stay valid for the entire Task.** Arguments, references, and views like
92/// `std::string_view` and `std::span` remain valid across every `co_await` until the task
93/// completes.
94///
95/// ## Page Navigation and API Teardown
96///
97/// Waiting tasks handle navigation and destruction safely:
98///
99/// - **A navigating page lets waiting tasks resume.** Awaiting a Promise from that page yields a
100/// page-gone error, operations on its values fail safely, and the library discards the task's
101/// final result when it completes.
102/// - **Destroying the API cancels waiting tasks.** Once the last owning handle to the API is
103/// destroyed, waiting tasks don't resume after their next `co_await`, and their Promises reject
104/// with a TypeError whose code is `"ULJS_DETACHED"`.
105///
106/// @note A `Task<void>` can't co_return an error. Return `Task<bool>` instead or use a Resolver to
107/// report failures from a void operation.
108///
109/// @see Resolver, RunOnWorker(), Await()
110///
111template <typename T = void>
112class [[nodiscard]] Task {
113 public:
114 static_assert(!detail::IsExpectedType<T>::value,
115 "co_return the value or the js::Error directly from Task<U>; Task's promise "
116 "already carries the error channel");
117 static_assert(!std::is_same_v<T, Error>, "a Task cannot resolve with a js::Error");
118
119 using promise_type = detail::TaskPromise<T>;
120 using Handle = std::coroutine_handle<promise_type>;
121
122 ///
123 /// Move constructor (`other` becomes empty).
124 ///
125 Task(Task&& other) noexcept : handle_(std::exchange(other.handle_, {})) {}
126 Task(const Task&) = delete;
127 Task& operator=(const Task&) = delete;
128
129 ///
130 /// Move assignment (discards this Task if it never started, and `other` becomes empty).
131 ///
132 Task& operator=(Task&& other) noexcept {
133 if (this != &other) {
134 if (handle_)
135 handle_.destroy();
136 handle_ = std::exchange(other.handle_, {});
137 }
138 return *this;
139 }
140
141 ///
142 /// Destructor. A Task that never started is discarded without running its body.
143 ///
145 if (handle_)
146 handle_.destroy();
147 }
148
149 ///
150 /// Start the Task and send its result to `resolver`.
151 ///
152 /// This Task becomes empty, and the coroutine frees itself when it finishes.
153 ///
154 /// @param resolver The Resolver that settles with the result.
155 ///
156 /// @pre Must be called on the Renderer's thread.
157 ///
158 void Start(Resolver resolver) && {
159 Handle handle = std::exchange(handle_, {});
160 assert(handle && "Start on an empty Task");
161 handle.promise().resolver_.emplace(std::move(resolver));
162 handle.promise().root_ = &handle.promise();
163 handle.resume();
164 }
165
166 ///
167 /// Run this Task from another Task and wait for it (eg, `co_await std::move(task)`).
168 ///
169 /// @return Returns an awaitable that yields a Result<T>.
170 ///
171 auto operator co_await() && {
172 assert(handle_ && "co_await on an empty Task");
173 return Awaiter { handle_ };
174 }
175
176 ///
177 /// Awaiting a Task by name is a compile error: a Task runs once, so write
178 /// `co_await std::move(task)`.
179 ///
180 template <typename U = T>
181 void operator co_await() & {
182 static_assert(detail::kAlwaysFalse<U>,
183 "co_await std::move(task): awaiting a Task runs it once and consumes it");
184 }
185
186 private:
187 friend promise_type;
188 friend struct detail::TaskAccess;
189
190 struct Awaiter {
191 Handle handle;
192 bool await_ready() const noexcept { return false; }
193 template <typename P>
194 std::coroutine_handle<> await_suspend(std::coroutine_handle<P> continuation) noexcept {
195 handle.promise().continuation_ = continuation;
196 // An awaited Task joins its parent's chain, so it stops when the parent's would.
197 if (detail::TaskFrame* parent = detail::FrameOf(continuation))
198 handle.promise().root_ = parent->root_;
199 return handle; // Symmetric transfer: starts the awaited coroutine now.
200 }
201 Result<T> await_resume() { return handle.promise().TakeResult(); }
202 };
203
204 explicit Task(Handle handle) : handle_(handle) {}
205 Handle handle_;
206};
207
208/// \cond INTERNAL
209namespace detail {
210
211template <typename T>
212Task<T> TaskPromise<T>::get_return_object() {
213 auto handle = std::coroutine_handle<TaskPromise<T>>::from_promise(*this);
214 this->handle_ = handle;
215 return Task<T>(handle);
216}
217
218inline Task<void> TaskPromise<void>::get_return_object() {
219 auto handle = std::coroutine_handle<TaskPromise<void>>::from_promise(*this);
220 handle_ = handle;
221 return Task<void>(handle);
222}
223
224} // namespace detail
225/// \endcond
226
227///
228/// Run a function on a background thread, then resume the coroutine on the Renderer's thread
229/// with its return value:
230///
231/// ```
232/// SaveData data = co_await js::RunOnWorker([&] { return ReadSaveFromDisk(slot); });
233/// ```
234///
235/// Unlike js::Await() and awaiting a Task, this gives you the function's return value directly
236/// (not a Result), so report failures inside that value or, with C++ exceptions enabled, by
237/// throwing.
238///
239/// @param fn The function to run. It takes no arguments.
240///
241/// @return Returns an awaitable that yields `fn`'s return value.
242///
243/// \parblock
244/// @note The background threads are a small pool shared by the whole library (up to 4
245/// threads, fewer on machines with few cores), so don't block one for a long time.
246/// \endparblock
247///
248/// \parblock
249/// @note If the Renderer is destroyed while `fn` is still waiting for a thread, `fn` never
250/// runs and the coroutine never resumes (its locals are never destroyed). Let pending
251/// Tasks finish before you destroy the Renderer.
252/// \endparblock
253///
254/// \parblock
255/// @note With C++ exceptions enabled, an exception thrown by `fn` is rethrown in the coroutine
256/// when it resumes, so it rejects the Task's Promise.
257/// \endparblock
258///
259/// @warning Don't touch JavaScript in `fn` (js::Value and js::Context only work on the
260/// Renderer's thread). Pass plain C++ data in and out.
261///
262template <typename Fn>
263auto RunOnWorker(Fn&& fn) {
264 return detail::WorkerAwaitable<std::decay_t<Fn>>(std::forward<Fn>(fn));
265}
266
267///
268/// Wait for a JavaScript promise from a Task:
269///
270/// ```
271/// Result<std::string> text = co_await js::Await<std::string>(promise);
272/// ```
273///
274/// The Task resumes on the Renderer's thread during a Renderer::Update() after the promise
275/// settles. The Result holds the value converted to T, or an Error: the rejection reason, a
276/// TypeError if the value doesn't convert, or a page-gone Error if the page goes away first.
277///
278/// Like JavaScript's `await`, this also takes a value that isn't a Promise. A value that isn't
279/// an object resumes the Task right away with that value. Any other object (eg, a thenable)
280/// goes through the page's `Promise.resolve()` first, which can run page code.
281///
282/// @param promise The promise (or other value) to wait for.
283///
284/// @return Returns an awaitable that yields a Result<T>.
285///
286/// \parblock
287/// @note If `promise` is empty, the Task resumes right away with an empty-handle Error (or a
288/// page-gone Error if its page is gone).
289/// \endparblock
290///
291/// \parblock
292/// @note A rejection you wait for this way counts as handled, so the page doesn't report it
293/// as unhandled.
294/// \endparblock
295///
296template <typename T = Value>
297 requires Marshalable<T>
298auto Await(const Value& promise) {
299 return detail::PromiseAwaitable<T>(promise);
300}
301
302///
303/// Call a JavaScript function and wait for the Promise it returns, from a Task:
304///
305/// ```
306/// Result<bool> ok = co_await js::Await<bool>(ask("Overwrite?"));
307/// Result<double> saved = co_await js::Await<double>(game["save"]("slot1"));
308/// ```
309///
310/// This waits for the call's result like the overload above. If the call itself fails (it
311/// throws, or the function is empty or its page is gone), the Task resumes right away with
312/// that Error.
313///
314/// @param call The result of the call (a js::Result<js::Value>).
315///
316/// @return Returns an awaitable that yields a Result<T>.
317///
318template <typename T = Value, typename R>
319 requires(Marshalable<T> && std::is_same_v<std::remove_cvref_t<R>, Result<Value>>)
320auto Await(R&& call) {
321 if (!call)
322 return detail::PromiseAwaitable<T>(std::forward<R>(call).error());
323 return detail::PromiseAwaitable<T>(std::forward<R>(call).value());
324}
325
326} // namespace js
327} // namespace ultralight
A handle that settles a JavaScript Promise.
Definition Resolver.h:97
~Task()
Destructor.
Definition Task.h:144
void Start(Resolver resolver) &&
Start the Task and send its result to resolver.
Definition Task.h:158
Task & operator=(const Task &)=delete
Task & operator=(Task &&other) noexcept
Move assignment (discards this Task if it never started, and other becomes empty).
Definition Task.h:132
std::coroutine_handle< promise_type > Handle
Definition Task.h:120
detail::TaskPromise< T > promise_type
Definition Task.h:119
Task(const Task &)=delete
Task(Task &&other) noexcept
Move constructor (other becomes empty).
Definition Task.h:125
A handle to a live JavaScript value.
Definition Value.h:210
Type-checked JavaScript bridge between C++ and web pages.
Definition JSInterop.h:141
auto RunOnWorker(Fn &&fn)
Run a function on a background thread, then resume the coroutine on the Renderer's thread with its re...
Definition Task.h:263
auto Await(const Value &promise)
Wait for a JavaScript promise from a Task:
Definition Task.h:298
Expected< T, Error > Result
The result of a JavaScript operation that can fail: a T or a js::Error.
Definition Error.h:520
Root namespace for every public Ultralight type, function, and enumeration.