docs
Loading...
Searching...
No Matches
Resolver.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#pragma once
6#include <Ultralight/CAPI/CAPI_JSAPI.h>
7#include <Ultralight/CAPI/CAPI_JSClass.h>
8#include <Ultralight/detail/Exceptions.h>
11
12#include <atomic>
13#include <type_traits>
14#include <utility>
15
16namespace ultralight {
17namespace js {
18
19/// \cond INTERNAL
20namespace detail {
21struct ResolverAccess;
22} // namespace detail
23/// \endcond
24
25///
26/// A handle that settles a JavaScript Promise.
27///
28/// A Resolver lets native code settle a JavaScript Promise from any thread. Taking a Resolver as
29/// the trailing parameter of a bound function turns it into an asynchronous call that returns a
30/// Promise to the page immediately.
31///
32/// Pass the resolver to a background thread and settle it when the work finishes:
33///
34/// ```
35/// app["save"] = [](std::string path, js::Resolver done) {
36/// RunOnSaveThread([path, done = std::move(done)] {
37/// if (WriteSave(path))
38/// done.Resolve(true);
39/// else
40/// done.Reject(js::Error::Make(js::ErrorType::Error, "disk full"));
41/// });
42/// };
43/// ```
44///
45/// Page script awaits the returned Promise normally:
46///
47/// ```js
48/// const saved = await app.save("slot1.dat");
49/// ```
50///
51/// ## Settling the Promise
52///
53/// Resolvers follow these rules:
54///
55/// - **Resolve() and Reject() are safe from any thread.**
56/// - **Values convert on the Renderer's thread during a later Renderer::Update().** Passing an
57/// argument to Resolve() copies or moves the C++ value immediately.
58/// - **Only the first call to Resolve() or Reject() takes effect.**
59/// - **Destroying an unsettled Resolver rejects the Promise.** This ensures the page never waits
60/// forever.
61///
62/// ## Sharing a Resolver
63///
64/// Resolver is move-only and can't be captured directly by copyable types such as `std::function`.
65/// Wrap the resolver in `std::shared_ptr` to pass it into a copyable callback or job queue:
66///
67/// ```
68/// auto shared = std::make_shared<js::Resolver>(std::move(done));
69/// jobs.push_back([shared] { shared->Resolve(42); });
70/// ```
71///
72/// ## Promises Outside Bound Functions
73///
74/// Call js::Context::MakePromise() on the page's context to create a pending Promise directly
75/// without a bound function:
76///
77/// ```
78/// auto [promise, resolver] = ctx.MakePromise();
79/// ctx["ready"] = promise;
80///
81/// // Later, from any thread:
82/// resolver.Resolve(std::string("ok"));
83/// ```
84///
85/// \parblock
86/// @note If the page navigates away or closes before the Promise settles, calling Resolve() or
87/// Reject() is safe and the library discards the result.
88/// \endparblock
89///
90/// \parblock
91/// @note Use a Resolver when an outside system or background thread signals completion of an
92/// operation. For sequential multi-step async operations, use js::Task instead.
93/// \endparblock
94///
95/// @see js::Task, js::Context::MakePromise(), js::Error
96///
97class Resolver {
98 public:
99 ///
100 /// Create an empty Resolver (Resolve() and Reject() do nothing).
101 ///
102 Resolver() = default;
103
104 // --- Interop with the C API (most embedders never touch raw handles) -------------------
105
106 ///
107 /// Take ownership of a resolver handle from the C API.
108 ///
109 /// You only need this at the C boundary (eg, for the handle a ULJSAsyncFunctionCallback
110 /// receives, or to call Task::Start()). A bound function that takes a js::Resolver gets one
111 /// ready to use.
112 ///
113 /// @param handle The handle to take ownership of.
114 ///
115 /// @return Returns a Resolver that owns `handle`.
116 ///
117 /// @note There's no FromBorrowed(): a Resolver settles its Promise once. To share one, see
118 /// the class notes.
119 ///
120 static Resolver Adopt(ULJSPromiseResolver handle) { return Resolver(handle); }
121
122 ///
123 /// Move constructor (`other` becomes empty).
124 ///
125 Resolver(Resolver&& other) noexcept
126 : resolver_(std::exchange(other.resolver_, nullptr)),
127 protected_(other.protected_.exchange(nullptr)) {}
128
129 ///
130 /// Move assignment (`other` becomes empty). If this Resolver held a Promise it hasn't settled,
131 /// that Promise is rejected now.
132 ///
133 Resolver& operator=(Resolver&& other) noexcept {
134 if (this != &other) {
135 ulDestroyJSPromiseResolver(resolver_);
136 ReleaseProtection();
137 resolver_ = std::exchange(other.resolver_, nullptr);
138 protected_.store(other.protected_.exchange(nullptr));
139 }
140 return *this;
141 }
142
143 Resolver(const Resolver&) = delete;
144 Resolver& operator=(const Resolver&) = delete;
145
146 ///
147 /// Destructor (rejects the Promise if it hasn't been settled).
148 ///
150 ulDestroyJSPromiseResolver(resolver_);
151 ReleaseProtection();
152 }
153
154 ///
155 /// Whether or not this Resolver holds a Promise (it stays true after Resolve() or Reject()).
156 ///
157 explicit operator bool() const { return resolver_ != nullptr; }
158
159 ///
160 /// Resolve the Promise with `undefined`.
161 ///
162 /// @note Safe to call from any thread.
163 ///
164 void Resolve() const {
165 ulJSPromiseResolverComplete(resolver_, &UndefinedThunk, nullptr, nullptr);
166 ReleaseProtection();
167 }
168
169 ///
170 /// Resolve the Promise with a value.
171 ///
172 /// @param value The value, copied or moved now and converted through js::TypeTraits later
173 /// on the Renderer's thread. A string literal or `char*` is copied into a
174 /// std::string.
175 ///
176 /// @note Safe to call from any thread.
177 ///
178 template <typename T>
180 void Resolve(T&& value) const {
181 static_assert(!detail::kIsClassInstancePointer<T>,
182 "a raw instance pointer cannot cross a deferred boundary (delivery "
183 "happens later, on the Renderer's thread): capture an owning holder (the "
184 "class's canonical holder) or js::Eternal<T> instead");
185 static_assert(!detail::IsCallScopedView<std::decay_t<T>>::value,
186 "a std::span cannot cross a deferred boundary (delivery happens later; "
187 "the view would dangle): copy into a std::vector<E> or RefPtr<Buffer>");
188 using Stored = detail::CapturedArg<T>;
189 auto* payload = new Stored(detail::CaptureArg(std::forward<T>(value)));
190 ulJSPromiseResolverComplete(resolver_, &ResolveThunk<Stored>, payload,
191 &DeletePayload<Stored>);
192 ReleaseProtection();
193 }
194
195 ///
196 /// Reject the Promise with an error.
197 ///
198 /// @param error The rejection reason.
199 ///
200 /// @note Safe to call from any thread.
201 ///
202 void Reject(Error error) const {
203 auto* payload = new Error(std::move(error));
204 ulJSPromiseResolverComplete(resolver_, &RejectThunk, payload, &DeletePayload<Error>);
205 ReleaseProtection();
206 }
207
208 ///
209 /// Get the C API handle without transferring ownership (this Resolver still destroys it).
210 ///
211 /// @return Returns the handle (NULL if this Resolver is empty).
212 ///
213 /// @warning Destroying the handle must be its last use. If another thread settles the Promise
214 /// through this handle, make sure that happens before this Resolver is destroyed.
215 ///
216 ULJSPromiseResolver raw() const { return resolver_; }
217
218 ///
219 /// Give up ownership of the C API handle and return it.
220 ///
221 /// @return Returns the handle (this Resolver becomes empty). You must call
222 /// ulDestroyJSPromiseResolver() when finished.
223 ///
224 /// @warning Destroying the handle must be its last use (see ulDestroyJSPromiseResolver()).
225 ///
226 ULJSPromiseResolver LeakRef() {
227 ULJSPromiseResolver handle = resolver_;
228 resolver_ = nullptr;
229 return handle;
230 }
231
232 private:
233 explicit Resolver(ULJSPromiseResolver handle) : resolver_(handle) {}
234
235 static ULJSValue UndefinedThunk(void*, ULJSContext, ULJSValue*) { return nullptr; }
236
237 // The conversion runs during a later update, inside the library: a converter that throws
238 // rejects the Promise with a generic Error instead of unwinding into it.
239 template <typename Stored>
240 static ULJSValue ResolveThunk(void* user_data, ULJSContext ctx, ULJSValue* exception) {
241 ULJSValue result = nullptr;
242 ultralight::detail::CallCatchingExceptions(
243 [&] { result = TypeTraits<Stored>::ToJS(ctx, *static_cast<Stored*>(user_data)); },
244 [&](const char* what) {
245 result = nullptr;
246 *exception = detail::NativeExceptionError(nullptr, what).ToJS(ctx);
247 });
248 return result;
249 }
250
251 static ULJSValue RejectThunk(void* user_data, ULJSContext ctx, ULJSValue* exception) {
252 ultralight::detail::CallCatchingExceptions(
253 [&] { *exception = static_cast<Error*>(user_data)->ToJS(ctx); },
254 [&](const char* what) {
255 *exception = detail::NativeExceptionError(nullptr, what).ToJS(ctx);
256 });
257 return nullptr;
258 }
259
260 template <typename Stored>
261 static void DeletePayload(void* user_data) {
262 delete static_cast<Stored*>(user_data);
263 }
264
265 friend struct detail::ResolverAccess;
266
267 // The instance an async class method was called on stays protected until this Resolver
268 // settles or is destroyed (see "Async Methods" in js::ClassBuilder). Any thread: the
269 // release itself is handed to the Renderer's thread. Atomic because a shared Resolver can
270 // settle from two threads at once, and only one may release the protection.
271 void ReleaseProtection() const { ulJSObjectUnprotectInstance(protected_.exchange(nullptr)); }
272
273 ULJSPromiseResolver resolver_ = nullptr;
274 mutable std::atomic<ULJSProtectedInstance> protected_ { nullptr };
275};
276
277} // namespace js
278} // namespace ultralight
struct C_JSContext * ULJSContext
Opaque handle to a JavaScript execution context (one page's script world in one View).
Definition View.h:25
An error from a JavaScript operation.
Definition Error.h:115
Error(Error &&other) noexcept
Move constructor (other no longer holds the exception).
Definition Error.h:175
ULJSPromiseResolver raw() const
Get the C API handle without transferring ownership (this Resolver still destroys it).
Definition Resolver.h:216
static Resolver Adopt(ULJSPromiseResolver handle)
Take ownership of a resolver handle from the C API.
Definition Resolver.h:120
void Resolve() const
Resolve the Promise with undefined.
Definition Resolver.h:164
Resolver & operator=(const Resolver &)=delete
void Resolve(T &&value) const
Resolve the Promise with a value.
Definition Resolver.h:180
Resolver(Resolver &&other) noexcept
Move constructor (other becomes empty).
Definition Resolver.h:125
ULJSPromiseResolver LeakRef()
Give up ownership of the C API handle and return it.
Definition Resolver.h:226
void Reject(Error error) const
Reject the Promise with an error.
Definition Resolver.h:202
Resolver(const Resolver &)=delete
~Resolver()
Destructor (rejects the Promise if it hasn't been settled).
Definition Resolver.h:149
Resolver & operator=(Resolver &&other) noexcept
Move assignment (other becomes empty).
Definition Resolver.h:133
Resolver()=default
Create an empty Resolver (Resolve() and Reject() do nothing).
Whether or not js::TypeTraits can convert T (ignoring const, volatile, and references).
Definition TypeTraits.h:275
js::Value ToJS(const js::Context &context, const Element &element)
Get an element's JavaScript object (the same object the page's scripts see for it).
Definition JSInterop.h:97
Type-checked JavaScript bridge between C++ and web pages.
Definition JSInterop.h:141
Root namespace for every public Ultralight type, function, and enumeration.
@ Error
Error icon.
Definition Dialogs.h:57