File
Blob: src/workerd/jsg/promise.h
| 1 | // Copyright (c) 2017-2022 Cloudflare, Inc. |
| 2 | // Licensed under the Apache 2.0 license found in the LICENSE file or at: |
| 3 | // https://opensource.org/licenses/Apache-2.0 |
| 4 | |
| 5 | #pragma once |
| 6 | |
| 7 | #include "jsg.h" |
| 8 | #include "util.h" |
| 9 | #include "wrappable.h" |
| 10 | |
| 11 | #include <v8-function.h> |
| 12 | #include <v8-promise.h> |
| 13 | |
| 14 | #include <kj/async.h> |
| 15 | #include <kj/table.h> |
| 16 | |
| 17 | namespace workerd::jsg { |
| 18 | |
| 19 | // ======================================================================================= |
| 20 | // Utilities for wrapping arbitrary C++ in an opaque way. See wrapOpaque(). |
| 21 | // |
| 22 | // At present this is used privately in the Promise implementation, but we could consider making |
| 23 | // wrapOpaque() more public if it is useful. |
| 24 | |
| 25 | template <typename T, bool = isGcVisitable<T>()> |
| 26 | struct OpaqueWrappable; |
| 27 | |
| 28 | struct OpaqueWrappableBase: public Wrappable { |
| 29 | kj::StringPtr jsgGetMemoryName() const override final { |
| 30 | return "OpaqueWrappable"_kjc; |
| 31 | } |
| 32 | void jsgGetMemoryInfo(MemoryTracker& tracker) const override final { |
| 33 | Wrappable::jsgGetMemoryInfo(tracker); |
| 34 | } |
| 35 | }; |
| 36 | |
| 37 | template <typename T> |
| 38 | struct OpaqueWrappable<T, false>: public OpaqueWrappableBase { |
| 39 | // Used to implement wrapOpaque(). |
| 40 | |
| 41 | OpaqueWrappable(T&& value): value(kj::mv(value)) {} |
| 42 | |
| 43 | T value; |
| 44 | bool movedAway = false; |
| 45 | |
| 46 | size_t jsgGetMemorySelfSize() const override final { |
| 47 | return sizeof(OpaqueWrappable); |
| 48 | } |
| 49 | }; |
| 50 | |
| 51 | template <typename T> |
| 52 | struct OpaqueWrappable<T, true>: public OpaqueWrappable<T, false> { |
| 53 | // When T is GC-visitable, make sure to implement visitation. |
| 54 | |
| 55 | using OpaqueWrappable<T, false>::OpaqueWrappable; |
| 56 | |
| 57 | void jsgVisitForGc(GcVisitor& visitor) override { |
| 58 | if (!this->movedAway) { |
| 59 | visitor.visit(this->value); |
| 60 | } |
| 61 | } |
| 62 | }; |
| 63 | |
| 64 | // Create a JavaScript value that wraps `t` in an opaque way. JS code will see this as an empty |
| 65 | // object, as if created by `{}`, but C++ code can unwrap the handle with `unwrapOpaque()`. |
| 66 | // |
| 67 | // If `T` is a type that can be passed to GcVisitor::visit(), then it will be visited whenever |
| 68 | // the opaque handle is found to be reachable. |
| 69 | // |
| 70 | // Generally, the opaque handle should not actually be passed to the application at all. This |
| 71 | // is useful in cases where the producer and consumer are both C++ code, but V8 requires that |
| 72 | // a handle be used for some reason. For example, this is used to pass C++ values through V8 |
| 73 | // Promises. |
| 74 | // |
| 75 | // Opaque-wrapping of `V8Ref<T>` is explicitly disallowed to avoid waste. Just use the handle |
| 76 | // directly in this case. If you really want to wrap a V8Ref opaquely, wrap it in a struct of |
| 77 | // your own first. (Don't forget to implement `visitForGc()`.) |
| 78 | template <typename T> |
| 79 | v8::Local<v8::Value> wrapOpaque(v8::Local<v8::Context> context, T&& t) { |
| 80 | static_assert(!kj::isReference<T>()); |
| 81 | static_assert(!isV8Ref<T>(), "no need to opaque-wrap regular JavaScript values"); |
| 82 | static_assert(!isV8Local<T>(), "can't opaque-wrap non-persistent handles"); |
| 83 | |
| 84 | auto wrapped = kj::refcounted<OpaqueWrappable<T>>(kj::mv(t)); |
| 85 | return wrapped->attachOpaqueWrapper(context, isGcVisitable<T>()); |
| 86 | } |
| 87 | |
| 88 | // Unwraps a handle created using `wrapOpaque()`. This consumes (moves away) the underlying |
| 89 | // value, so can only be called once. Throws if the handle is the wrong type or has already been |
| 90 | // consumed previously. |
| 91 | template <typename T> |
| 92 | T unwrapOpaque(v8::Isolate* isolate, v8::Local<v8::Value> handle) { |
| 93 | static_assert(!kj::isReference<T>()); |
| 94 | static_assert(!isV8Ref<T>(), "no need to opaque-wrap regular JavaScript values"); |
| 95 | static_assert(!isV8Local<T>(), "can't opaque-wrap non-persistent handles"); |
| 96 | |
| 97 | Wrappable& wrappable = KJ_ASSERT_NONNULL(Wrappable::tryUnwrapOpaque(isolate, handle)); |
| 98 | OpaqueWrappable<T>* holder = dynamic_cast<OpaqueWrappable<T>*>(&wrappable); |
| 99 | KJ_ASSERT(holder != nullptr); |
| 100 | KJ_ASSERT(!holder->movedAway); |
| 101 | holder->movedAway = true; |
| 102 | return kj::mv(holder->value); |
| 103 | } |
| 104 | |
| 105 | // Unwraps a handle created using `wrapOpaque()`, without consuming the value. Throws if the |
| 106 | // handle is the wrong type or has already been consumed previously. |
| 107 | template <typename T> |
| 108 | T& unwrapOpaqueRef(v8::Isolate* isolate, v8::Local<v8::Value> handle) { |
| 109 | static_assert(!kj::isReference<T>()); |
| 110 | static_assert(!isV8Ref<T>(), "no need to opaque-wrap regular JavaScript values"); |
| 111 | static_assert(!isV8Local<T>(), "can't opaque-wrap non-persistent handles"); |
| 112 | |
| 113 | Wrappable& wrappable = KJ_ASSERT_NONNULL(Wrappable::tryUnwrapOpaque(isolate, handle)); |
| 114 | OpaqueWrappable<T>* holder = dynamic_cast<OpaqueWrappable<T>*>(&wrappable); |
| 115 | KJ_ASSERT(holder != nullptr); |
| 116 | KJ_ASSERT(!holder->movedAway); |
| 117 | return holder->value; |
| 118 | } |
| 119 | |
| 120 | // Destroys the value contained by an opaque handle, without returning it. This is equivalent |
| 121 | // to calling unwrapOpaque<T>() and dropping the result, except that if the handle is the wrong |
| 122 | // type, this function silently does nothing rather than throw. |
| 123 | template <typename T> |
| 124 | void dropOpaque(v8::Isolate* isolate, v8::Local<v8::Value> handle) { |
| 125 | static_assert(!kj::isReference<T>()); |
| 126 | static_assert(!isV8Ref<T>()); |
| 127 | |
| 128 | KJ_IF_SOME(wrappable, Wrappable::tryUnwrapOpaque(isolate, handle)) { |
| 129 | OpaqueWrappable<T>* holder = dynamic_cast<OpaqueWrappable<T>*>(&wrappable); |
| 130 | if (holder != nullptr) { |
| 131 | holder->movedAway = true; |
| 132 | auto drop KJ_UNUSED = kj::mv(holder->value); |
| 133 | } |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | // ======================================================================================= |
| 138 | // Promise implementation |
| 139 | |
| 140 | // This type (opaque-wrapped) is the type of the "data" for a continuation callback. We have both |
| 141 | // the success and error callbacks share the same "data" object so that both underlying C++ |
| 142 | // callbacks are proactively destroyed after one of the runs. Otherwise, we'd only destroy the |
| 143 | // function that was called, while the other one would have to wait for GC, which may mean |
| 144 | // keeping around C++ resources longer than necessary. |
| 145 | template <typename ThenFunc, typename CatchFunc> |
| 146 | struct ThenCatchPair { |
| 147 | ThenFunc thenFunc; |
| 148 | CatchFunc catchFunc; |
| 149 | }; |
| 150 | |
| 151 | // FunctionCallback implementing a C++ .then() continuation on a JS promise. |
| 152 | // |
| 153 | // We expect the input is already an opaque-wrapped value, args.Data() is an opaque-wrapped C++ |
| 154 | // function to execute, and we want to produce an opaque-wrapped output or Promise. |
| 155 | template <typename FuncPairType, bool isCatch, typename Input, typename Output> |
| 156 | void promiseContinuation(const v8::FunctionCallbackInfo<v8::Value>& args) { |
| 157 | liftKj(args, [&]() { |
| 158 | auto isolate = args.GetIsolate(); |
| 159 | #ifdef KJ_DEBUG |
| 160 | // In debug mode only, we verify that the function hasn't captured any KJ heap objects without |
| 161 | // a IoOwn. We don't bother with this check in release mode because it's pretty deterministic, |
| 162 | // so it's likely to be caught in debug, and we'd like to avoid the extra overhead in releases. |
| 163 | DISALLOW_KJ_IO_DESTRUCTORS_SCOPE; |
| 164 | #endif |
| 165 | auto funcPair = unwrapOpaque<FuncPairType>(isolate, args.Data()); |
| 166 | #ifdef KJ_DEBUG |
| 167 | kj::AllowAsyncDestructorsScope allowAsyncDestructors; |
| 168 | #endif |
| 169 | auto callFunc = [&]() -> Output { |
| 170 | auto& js = Lock::from(isolate); |
| 171 | if constexpr (isCatch) { |
| 172 | // Exception from V8 is not expected to be opaque-wrapped. It's just a Value. |
| 173 | return funcPair.catchFunc(js, Value(isolate, args[0])); |
| 174 | } else if constexpr (isVoid<Input>()) { |
| 175 | return funcPair.thenFunc(js); |
| 176 | } else if constexpr (isV8Ref<Input>()) { |
| 177 | return funcPair.thenFunc(js, Input(isolate, args[0])); |
| 178 | } else { |
| 179 | return funcPair.thenFunc(js, unwrapOpaque<Input>(isolate, args[0])); |
| 180 | } |
| 181 | }; |
| 182 | if constexpr (isVoid<Output>()) { |
| 183 | callFunc(); |
| 184 | } else if constexpr (isPromise<Output>()) { |
| 185 | // Continuation returns Promise. We don't want to opaque-wrap that, we want to return it |
| 186 | // raw, so that the V8 Promise machinery will chain it. |
| 187 | |
| 188 | // We cast the return value to v8::Local<v8::Value> so that it doesn't trigger liftKj()'s |
| 189 | // special handling of promises, where it tries to catch exceptions and merge them into the |
| 190 | // promise. We don't need to do this, because this is being called as a .then() which already |
| 191 | // catches exceptions and does the right thing. |
| 192 | return v8::Local<v8::Value>(callFunc().consumeHandle(Lock::from(isolate))); |
| 193 | } else if constexpr (isV8Ref<Output>()) { |
| 194 | return callFunc().getHandle(isolate); |
| 195 | } else { |
| 196 | return wrapOpaque(isolate->GetCurrentContext(), callFunc()); |
| 197 | } |
| 198 | }); |
| 199 | } |
| 200 | |
| 201 | // Promise continuation that propagates the value or exception unmodified, but makes sure to |
| 202 | // proactively destroy the ThenCatchPair. |
| 203 | template <typename FuncPairType, bool isCatch> |
| 204 | void identityPromiseContinuation(const v8::FunctionCallbackInfo<v8::Value>& args) { |
| 205 | auto isolate = args.GetIsolate(); |
| 206 | dropOpaque<FuncPairType>(isolate, args.Data()); |
| 207 | if constexpr (isCatch) { |
| 208 | isolate->ThrowException(args[0]); |
| 209 | } else { |
| 210 | args.GetReturnValue().Set(args[0]); |
| 211 | } |
| 212 | } |
| 213 | |
| 214 | template <typename TypeWrapper> |
| 215 | class PromiseWrapper; |
| 216 | |
| 217 | template <typename T> |
| 218 | class Promise { |
| 219 | public: |
| 220 | static_assert(!kj::canConvert<T*, v8::Data*>(), |
| 221 | "jsg::Promise<T> expects T to be an instantiable C++ type, not a JS heap type; use " |
| 222 | "jsg::Promise<jsg::V8Ref<T>> to represent a promise for a JavaScript heap object."); |
| 223 | |
| 224 | Promise(v8::Isolate* isolate, v8::Local<v8::Promise> v8Promise) |
| 225 | : v8Promise(V8Ref<v8::Promise>(isolate, v8Promise)) {} |
| 226 | |
| 227 | Promise(decltype(nullptr)): v8Promise(kj::none) {} |
| 228 | // For use when you're declaring a local variable that will be initialized later. |
| 229 | |
| 230 | void markAsHandled(Lock& js) { |
| 231 | auto promise = getInner(js); |
| 232 | promise->MarkAsHandled(); |
| 233 | markedAsHandled = true; |
| 234 | } |
| 235 | |
| 236 | // Attach a continuation function and error handler to be called when this promise |
| 237 | // is fulfilled. It is important to remember that then(...) can synchronously throw |
| 238 | // a JavaScript exception (and jsg::JsExceptionThrown) in certain cases. |
| 239 | template <typename Func, typename ErrorFunc> |
| 240 | PromiseForResult<Func, T, true> then(Lock& js, Func&& func, ErrorFunc&& errorFunc) { |
| 241 | using Output = ReturnType<Func, T, true>; |
| 242 | static_assert(kj::isSameType<Output, ReturnType<ErrorFunc, Value, true>>(), |
| 243 | "functions passed to .then() must return exactly the same type"); |
| 244 | |
| 245 | using FuncPair = ThenCatchPair<Func, ErrorFunc>; |
| 246 | return thenImpl<Output>(js, FuncPair{kj::fwd<Func>(func), kj::fwd<ErrorFunc>(errorFunc)}, |
| 247 | &promiseContinuation<FuncPair, false, T, Output>, |
| 248 | &promiseContinuation<FuncPair, true, Value, Output>); |
| 249 | } |
| 250 | |
| 251 | // Attach a continuation function to be called when this promise is fulfilled. |
| 252 | // It is important to remember that then(...) can synchronously throw |
| 253 | // a JavaScript exception (and jsg::JsExceptionThrown) in certain cases. |
| 254 | template <typename Func> |
| 255 | PromiseForResult<Func, T, true> then(Lock& js, Func&& func) { |
| 256 | using Output = ReturnType<Func, T, true>; |
| 257 | |
| 258 | // HACK: The error function is never called, so it need not actually be a functor. |
| 259 | using FuncPair = ThenCatchPair<Func, bool>; |
| 260 | return thenImpl<Output>(js, FuncPair{kj::fwd<Func>(func), false}, |
| 261 | &promiseContinuation<FuncPair, false, T, Output>, |
| 262 | &identityPromiseContinuation<FuncPair, true>); |
| 263 | } |
| 264 | |
| 265 | template <typename ErrorFunc> |
| 266 | Promise<T> catch_(Lock& js, ErrorFunc&& errorFunc) { |
| 267 | static_assert(kj::isSameType<T, ReturnType<ErrorFunc, Value, true>>(), |
| 268 | "function passed to .catch_() must return exactly the promise's type"); |
| 269 | |
| 270 | // HACK: The non-error function is never called, so it need not actually be a functor. |
| 271 | using FuncPair = ThenCatchPair<bool, ErrorFunc>; |
| 272 | return thenImpl<T>(js, FuncPair{false, kj::fwd<ErrorFunc>(errorFunc)}, |
| 273 | &identityPromiseContinuation<FuncPair, false>, |
| 274 | &promiseContinuation<FuncPair, true, Value, T>); |
| 275 | } |
| 276 | |
| 277 | // whenResolved returns a new Promise<void> that resolves when this promise resolves, |
| 278 | // stopping the propagation of the resolved value. Unlike then(), calling whenResolved() |
| 279 | // does not consume the promise, and whenResolved() can be called multiple times, |
| 280 | // with each call creating a new branch off the original promise. Another key difference |
| 281 | // with whenResolved() is that the markAsHandled status will propagate to the new Promise<void> |
| 282 | // returned by whenResolved(). |
| 283 | Promise<void> whenResolved(Lock& js) { |
| 284 | auto promise = Promise<void>(js.v8Isolate, getInner(js)); |
| 285 | if (markedAsHandled) { |
| 286 | promise.markAsHandled(js); |
| 287 | } |
| 288 | return kj::mv(promise); |
| 289 | } |
| 290 | |
| 291 | v8::Local<v8::Promise> consumeHandle(Lock& js) { |
| 292 | auto result = getInner(js); |
| 293 | v8Promise = kj::none; |
| 294 | return result; |
| 295 | } |
| 296 | |
| 297 | // If the promise is resolved, return the result, consuming the Promise. If it is pending |
| 298 | // or rejected, returns null. This can be used as an optimization or in tests, but you must |
| 299 | // never rely on it for correctness. |
| 300 | kj::Maybe<T> tryConsumeResolved(Lock& js) { |
| 301 | return js.withinHandleScope([&]() -> kj::Maybe<T> { |
| 302 | auto handle = |
| 303 | KJ_REQUIRE_NONNULL(v8Promise, "jsg::Promise can only be used once").getHandle(js); |
| 304 | switch (handle->State()) { |
| 305 | case v8::Promise::kPending: |
| 306 | case v8::Promise::kRejected: |
| 307 | return kj::none; |
| 308 | case v8::Promise::kFulfilled: |
| 309 | v8Promise = kj::none; |
| 310 | return unwrapOpaque<T>(js.v8Isolate, handle->Result()); |
| 311 | } |
| 312 | }); |
| 313 | } |
| 314 | |
| 315 | class Resolver { |
| 316 | public: |
| 317 | Resolver(v8::Isolate* isolate, v8::Local<v8::Promise::Resolver> v8Resolver) |
| 318 | : v8Resolver(isolate, kj::mv(v8Resolver)) {} |
| 319 | |
| 320 | template <typename U = T, typename = kj::EnableIf<!isVoid<U>()>> |
| 321 | void resolve(Lock& js, kj::NoInfer<U>&& value) { |
| 322 | js.withinHandleScope([&] { |
| 323 | auto context = js.v8Context(); |
| 324 | v8::Local<v8::Value> handle; |
| 325 | if constexpr (isV8Ref<U>()) { |
| 326 | handle = value.getHandle(js); |
| 327 | } else { |
| 328 | handle = wrapOpaque(context, kj::mv(value)); |
| 329 | } |
| 330 | check(v8Resolver.getHandle(js)->Resolve(context, handle)); |
| 331 | }); |
| 332 | } |
| 333 | |
| 334 | template <typename U = T, typename = kj::EnableIf<isVoid<U>()>> |
| 335 | void resolve(Lock& js) { |
| 336 | js.withinHandleScope( |
| 337 | [&] { check(v8Resolver.getHandle(js)->Resolve(js.v8Context(), js.v8Undefined())); }); |
| 338 | } |
| 339 | |
| 340 | void resolve(Lock& js, Promise&& promise) { |
| 341 | // Resolve to another Promise. |
| 342 | check(v8Resolver.getHandle(js)->Resolve(js.v8Context(), promise.consumeHandle(js))); |
| 343 | } |
| 344 | |
| 345 | void reject(Lock& js, v8::Local<v8::Value> exception) { |
| 346 | js.withinHandleScope( |
| 347 | [&] { check(v8Resolver.getHandle(js)->Reject(js.v8Context(), exception)); }); |
| 348 | } |
| 349 | |
| 350 | void reject(Lock& js, kj::Exception exception, ExceptionToJsOptions options = {}) { |
| 351 | reject(js, exceptionToJs(js.v8Isolate, kj::mv(exception), options)); |
| 352 | } |
| 353 | |
| 354 | Resolver addRef(Lock& js) { |
| 355 | return {js.v8Isolate, v8Resolver.getHandle(js)}; |
| 356 | } |
| 357 | void visitForGc(GcVisitor& visitor) { |
| 358 | visitor.visit(v8Resolver); |
| 359 | } |
| 360 | |
| 361 | JSG_MEMORY_INFO(Resolver) { |
| 362 | tracker.trackField("resolver", v8Resolver); |
| 363 | } |
| 364 | |
| 365 | private: |
| 366 | V8Ref<v8::Promise::Resolver> v8Resolver; |
| 367 | friend class MemoryTracker; |
| 368 | }; |
| 369 | |
| 370 | void visitForGc(GcVisitor& visitor) { |
| 371 | visitor.visit(v8Promise); |
| 372 | } |
| 373 | |
| 374 | JSG_MEMORY_INFO(Promise) { |
| 375 | KJ_IF_SOME(promise, v8Promise) { |
| 376 | tracker.trackField("promise", promise); |
| 377 | } |
| 378 | } |
| 379 | |
| 380 | // Ths is for testing/diagnostics purposes only. |
| 381 | enum class State { |
| 382 | PENDING = v8::Promise::kPending, |
| 383 | FULFILLED = v8::Promise::kFulfilled, |
| 384 | REJECTED = v8::Promise::kRejected, |
| 385 | CONSUMED = 3 // Not a real state; indicates the Promise has been consumed. |
| 386 | }; |
| 387 | State getState(Lock& js) { |
| 388 | KJ_IF_SOME(promise, v8Promise) { |
| 389 | return static_cast<State>(promise.getHandle(js)->State()); |
| 390 | } else { |
| 391 | return State::CONSUMED; |
| 392 | } |
| 393 | } |
| 394 | |
| 395 | private: |
| 396 | kj::Maybe<V8Ref<v8::Promise>> v8Promise; |
| 397 | bool markedAsHandled = false; |
| 398 | |
| 399 | v8::Local<v8::Promise> getInner(Lock& js) { |
| 400 | return KJ_REQUIRE_NONNULL(v8Promise, "jsg::Promise can only be used once").getHandle(js); |
| 401 | } |
| 402 | |
| 403 | template <typename U = T, typename = kj::EnableIf<!isVoid<U>()>()> |
| 404 | Promise(Lock& js, kj::NoInfer<U>&& value) { |
| 405 | js.withinHandleScope([&] { |
| 406 | auto context = js.v8Context(); |
| 407 | auto resolver = check(v8::Promise::Resolver::New(context)); |
| 408 | v8::Local<v8::Value> handle; |
| 409 | if constexpr (isV8Ref<U>()) { |
| 410 | handle = value.getHandle(js); |
| 411 | } else { |
| 412 | handle = wrapOpaque(context, kj::mv(value)); |
| 413 | }; |
| 414 | check(resolver->Resolve(context, handle)); |
| 415 | v8Promise.emplace(js.v8Isolate, resolver->GetPromise()); |
| 416 | }); |
| 417 | } |
| 418 | |
| 419 | template <typename U = T, typename = kj::EnableIf<isVoid<U>()>()> |
| 420 | explicit Promise(Lock& js) { |
| 421 | js.withinHandleScope([&] { |
| 422 | auto context = js.v8Context(); |
| 423 | auto resolver = check(v8::Promise::Resolver::New(context)); |
| 424 | check(resolver->Resolve(context, js.v8Undefined())); |
| 425 | v8Promise.emplace(js.v8Isolate, resolver->GetPromise()); |
| 426 | }); |
| 427 | } |
| 428 | |
| 429 | template <typename Result, typename FuncPair> |
| 430 | MaintainPromise<Result> thenImpl(Lock& js, |
| 431 | FuncPair&& funcPair, |
| 432 | v8::FunctionCallback thenCallback, |
| 433 | v8::FunctionCallback errCallback) { |
| 434 | return js.withinHandleScope([&] { |
| 435 | auto context = js.v8Context(); |
| 436 | |
| 437 | auto funcPairHandle = wrapOpaque(context, kj::mv(funcPair)); |
| 438 | |
| 439 | auto then = check(v8::Function::New( |
| 440 | context, thenCallback, funcPairHandle, 1, v8::ConstructorBehavior::kThrow)); |
| 441 | |
| 442 | auto errThen = check(v8::Function::New( |
| 443 | context, errCallback, funcPairHandle, 1, v8::ConstructorBehavior::kThrow)); |
| 444 | |
| 445 | return MaintainPromise<Result>( |
| 446 | js.v8Isolate, check(consumeHandle(js)->Then(context, then, errThen))); |
| 447 | }); |
| 448 | } |
| 449 | |
| 450 | friend class Lock; |
| 451 | template <typename TypeWrapper> |
| 452 | friend class PromiseWrapper; |
| 453 | friend class MemoryTracker; |
| 454 | }; |
| 455 | |
| 456 | template <typename T> |
| 457 | class Promise<Promise<T>> { |
| 458 | static_assert(sizeof(T*) == 0, "Promise<Promise<T>> is invalid; use Promise<T> instead"); |
| 459 | }; |
| 460 | |
| 461 | template <typename T> |
| 462 | class Promise<kj::Promise<T>> { |
| 463 | static_assert(sizeof(T*) == 0, "jsg::Promise<kj::Promise<T>> is illegal; you need a IoOwn!"); |
| 464 | }; |
| 465 | |
| 466 | template <typename T> |
| 467 | struct PromiseResolverPair { |
| 468 | Promise<T> promise; |
| 469 | Promise<T>::Resolver resolver; |
| 470 | |
| 471 | JSG_MEMORY_INFO(PromiseResolverPair) { |
| 472 | tracker.trackField("promise", promise); |
| 473 | tracker.trackField("resolver", resolver); |
| 474 | } |
| 475 | }; |
| 476 | |
| 477 | template <typename T> |
| 478 | PromiseResolverPair<T> Lock::newPromiseAndResolver() { |
| 479 | return withinHandleScope([&]() -> PromiseResolverPair<T> { |
| 480 | auto resolver = check(v8::Promise::Resolver::New(v8Context())); |
| 481 | auto promise = resolver->GetPromise(); |
| 482 | return {{v8Isolate, promise}, {v8Isolate, resolver}}; |
| 483 | }); |
| 484 | } |
| 485 | |
| 486 | template <typename T> |
| 487 | inline Promise<T> Lock::resolvedPromise(T&& value) { |
| 488 | return Promise<T>(*this, kj::fwd<T>(value)); |
| 489 | } |
| 490 | inline Promise<void> Lock::resolvedPromise() { |
| 491 | return Promise<void>(*this); |
| 492 | } |
| 493 | |
| 494 | template <typename T> |
| 495 | Promise<T> Lock::rejectedPromise(v8::Local<v8::Value> exception) { |
| 496 | auto [promise, resolver] = newPromiseAndResolver<T>(); |
| 497 | resolver.reject(*this, exception); |
| 498 | return kj::mv(promise); |
| 499 | } |
| 500 | |
| 501 | template <typename T> |
| 502 | Promise<T> Lock::rejectedPromise(jsg::Value exception) { |
| 503 | return withinHandleScope([&] { return rejectedPromise<T>(exception.getHandle(*this)); }); |
| 504 | } |
| 505 | |
| 506 | template <typename T> |
| 507 | Promise<T> Lock::rejectedPromise(kj::Exception&& exception, ExceptionToJsOptions options) { |
| 508 | return withinHandleScope( |
| 509 | [&] { return rejectedPromise<T>(exceptionToJs(kj::mv(exception), options)); }); |
| 510 | } |
| 511 | |
| 512 | template <class Func> |
| 513 | PromiseForResult<Func, void, false> Lock::evalNow(Func&& func) { |
| 514 | using Result = RemovePromise<ReturnType<Func, void>>; |
| 515 | v8::TryCatch tryCatch(v8Isolate); |
| 516 | try { |
| 517 | if constexpr (isPromise<ReturnType<Func, void>>()) { |
| 518 | return func(); |
| 519 | } else { |
| 520 | return resolvedPromise<Result>(func()); |
| 521 | } |
| 522 | } catch (jsg::JsExceptionThrown&) { |
| 523 | if (tryCatch.HasCaught() && tryCatch.CanContinue()) { |
| 524 | return rejectedPromise<Result>(tryCatch.Exception()); |
| 525 | } else { |
| 526 | // Probably TerminateExecution() called. |
| 527 | tryCatch.ReThrow(); |
| 528 | throw; |
| 529 | } |
| 530 | } catch (kj::Exception& e) { |
| 531 | return rejectedPromise<Result>(kj::mv(e)); |
| 532 | } catch (std::exception& exception) { |
| 533 | return rejectedPromise<Result>(makeInternalError(v8Isolate, exception.what())); |
| 534 | } catch (...) { |
| 535 | return rejectedPromise<Result>(makeInternalError( |
| 536 | v8Isolate, kj::str("caught unknown exception of type: ", kj::getCaughtExceptionType()))); |
| 537 | } |
| 538 | } |
| 539 | |
| 540 | // ----------------------------------------------------------------------------- |
| 541 | |
| 542 | // Continuation function that converts a promised C++ value into a JavaScript value. |
| 543 | template <typename TypeWrapper, typename Input> |
| 544 | void thenWrap(const v8::FunctionCallbackInfo<v8::Value>& args) { |
| 545 | if constexpr (isVoid<Input>()) { |
| 546 | // No wrapping needed. Note that we still attach `thenWrap` to the promise chain only because |
| 547 | // we use `args.data` to prevent the object from being GC'ed while the promise is still |
| 548 | // executing. |
| 549 | args.GetReturnValue().SetUndefined(); |
| 550 | } else if constexpr (isV8Ref<Input>()) { |
| 551 | // Similarly, no unwrapping needed. |
| 552 | args.GetReturnValue().Set(args[0]); |
| 553 | } else { |
| 554 | liftKj(args, [&]() { |
| 555 | v8::Isolate* isolate = args.GetIsolate(); |
| 556 | auto& wrapper = TypeWrapper::from(isolate); |
| 557 | auto context = isolate->GetCurrentContext(); |
| 558 | auto& lock = Lock::from(isolate); |
| 559 | return wrapper.wrap(lock, context, kj::none, unwrapOpaque<Input>(isolate, args[0])); |
| 560 | }); |
| 561 | } |
| 562 | } |
| 563 | |
| 564 | // Continuation function that converts a promised JavaScript value into a C++ value. |
| 565 | template <typename TypeWrapper, typename Output> |
| 566 | void thenUnwrap(const v8::FunctionCallbackInfo<v8::Value>& args) { |
| 567 | liftKj(args, [&]() { |
| 568 | v8::Isolate* isolate = args.GetIsolate(); |
| 569 | auto& wrapper = TypeWrapper::from(isolate); |
| 570 | auto context = isolate->GetCurrentContext(); |
| 571 | auto& js = Lock::from(isolate); |
| 572 | return wrapOpaque(context, |
| 573 | wrapper.template unwrap<Output>( |
| 574 | js, context, args[0], TypeErrorContext::promiseResolution())); |
| 575 | }); |
| 576 | } |
| 577 | |
| 578 | // TypeWrapper mixin for Promise. |
| 579 | template <typename TypeWrapper> |
| 580 | class PromiseWrapper { |
| 581 | public: |
| 582 | // The constructor here is a bit of a hack. The config is optional and might not be a JsgConfig |
| 583 | // object (or convertible to a JsgConfig) if is provided. However, because of the way TypeWrapper |
| 584 | // inherits PromiseWrapper, we always end up passing a config option (which might be |
| 585 | // std::nullptr_t). The getConfig allows us to handle any case using reasonable defaults. |
| 586 | PromiseWrapper(const auto& config): config(getConfig(config)) {} |
| 587 | |
| 588 | template <typename T> |
| 589 | static constexpr const char* getName(Promise<T>*) { |
| 590 | return "Promise"; |
| 591 | } |
| 592 | |
| 593 | template <typename T> |
| 594 | v8::Local<v8::Promise> wrap(jsg::Lock& js, |
| 595 | v8::Local<v8::Context> context, |
| 596 | kj::Maybe<v8::Local<v8::Object>> creator, |
| 597 | Promise<T>&& promise) { |
| 598 | // Add a .then() to unwrap the value (i.e. convert C++ value to JavaScript). |
| 599 | // |
| 600 | // We use `creator` as the `data` value for this continuation so that the creator object |
| 601 | // cannot be GC'ed while the callback still exists. This gives us the KJ-style guarantee that |
| 602 | // the object whose method returned the promise will not be destroyed while the promise is |
| 603 | // still executing. |
| 604 | auto markedAsHandled = promise.markedAsHandled; |
| 605 | auto then = check(v8::Function::New(context, &thenWrap<TypeWrapper, T>, creator.orDefault({}), |
| 606 | 1, v8::ConstructorBehavior::kThrow)); |
| 607 | auto ret = check(promise.consumeHandle(js)->Then(context, then)); |
| 608 | // Although we added a .then() to the promise to translate the value to JavaScript, we would |
| 609 | // like things to behave as if the C++ code returned this Promise directly to JavaScript. In |
| 610 | // particular, if the C++ code marked the Promise handled, then the derived JavaScript promise |
| 611 | // ought to be marked as handled as well. |
| 612 | if (markedAsHandled) { |
| 613 | ret->MarkAsHandled(); |
| 614 | } |
| 615 | |
| 616 | return ret; |
| 617 | } |
| 618 | |
| 619 | template <typename T> |
| 620 | kj::Maybe<Promise<T>> tryUnwrap(Lock& js, |
| 621 | v8::Local<v8::Context> context, |
| 622 | v8::Local<v8::Value> handle, |
| 623 | Promise<T>*, |
| 624 | kj::Maybe<v8::Local<v8::Object>> parentObject) { |
| 625 | if (handle->IsPromise()) { |
| 626 | auto promise = handle.As<v8::Promise>(); |
| 627 | if constexpr (!isVoid<T>() && !isV8Ref<T>()) { |
| 628 | // Add a .then() to unwrap the promise's resolution (i.e. convert it from JS to C++). |
| 629 | // Note that we don't need to handle the rejection case here as there is no wrapping |
| 630 | // applied to exception values, so we just let it propagate through. |
| 631 | // |
| 632 | // TODO(perf): We could in theory check if promise->State() is kFulfilled and, in that |
| 633 | // case, pull out promise->Result(), unwrap it, and make a new immediate promise. |
| 634 | // Similarly in `wrap()`. Not clear if the added complexity is worth it, though. |
| 635 | auto then = check(v8::Function::New( |
| 636 | context, &thenUnwrap<TypeWrapper, T>, {}, 1, v8::ConstructorBehavior::kThrow)); |
| 637 | promise = check(promise->Then(context, then)); |
| 638 | } |
| 639 | return Promise<T>(js.v8Isolate, promise); |
| 640 | } else { |
| 641 | // Input is a resolved value (not a promise). Try to unwrap it now. |
| 642 | |
| 643 | // If the input is an object that is not a promise, there's a chance it is a custom |
| 644 | // thenable (and object with a then method intended to be used as a promise). If that |
| 645 | // is the case, then we can handle the thenable by resolving it to a promise then |
| 646 | // unwrapping that promise. |
| 647 | // Unfortunately this needs to be gated by a compatibility flag because there are |
| 648 | // existing workers that appear to rely on the old behavior -- although it's not clear |
| 649 | // if those workers actually work the way they were intended to. |
| 650 | if (config.unwrapCustomThenables && isThenable(context, handle)) { |
| 651 | auto paf = check(v8::Promise::Resolver::New(context)); |
| 652 | check(paf->Resolve(context, handle)); |
| 653 | return tryUnwrap( |
| 654 | js, context, paf->GetPromise(), static_cast<Promise<T>*>(nullptr), parentObject); |
| 655 | } |
| 656 | |
| 657 | if constexpr (isVoid<T>()) { |
| 658 | // When expecting Promise<void>, we treat absolutely any non-promise value as being |
| 659 | // an immediately-resolved promise. This is consistent with JavaScript where you'd |
| 660 | // commonly use `Promise.resolve(param).then(() => {...})` in order to coerce the param |
| 661 | // to a promise... normally you wouldn't bother checking that the param specifically |
| 662 | // resolved to `undefined`, you'd just throw away whatever it resolved to. |
| 663 | // |
| 664 | // It's possible to argue that we should actually allow only `undefined` here but |
| 665 | // changing it now could break existing users, e.g. html-rewriter.ew-test is broken |
| 666 | // because it writes `() => someExpression()` for a callback that's supposed to |
| 667 | // optionally return Promise<void> -- it seems like the callback isn't actually intending |
| 668 | // to return the result of `someExpression()` but does so by accident since the braces |
| 669 | // are missing. This is probably common in user code, too. |
| 670 | return js.resolvedPromise(); |
| 671 | } else { |
| 672 | auto& wrapper = *static_cast<TypeWrapper*>(this); |
| 673 | KJ_IF_SOME(value, wrapper.tryUnwrap(js, context, handle, (T*)nullptr, parentObject)) { |
| 674 | return js.resolvedPromise(kj::mv(value)); |
| 675 | } else { |
| 676 | // Wrong type. |
| 677 | return kj::none; |
| 678 | } |
| 679 | } |
| 680 | } |
| 681 | } |
| 682 | |
| 683 | private: |
| 684 | const JsgConfig config; |
| 685 | |
| 686 | static bool isThenable(v8::Local<v8::Context> context, v8::Local<v8::Value> handle) { |
| 687 | if (handle->IsObject()) { |
| 688 | auto obj = handle.As<v8::Object>(); |
| 689 | return check(obj->Has(context, v8StrIntern(v8::Isolate::GetCurrent(), "then"))); |
| 690 | } |
| 691 | return false; |
| 692 | } |
| 693 | }; |
| 694 | |
| 695 | // ----------------------------------------------------------------------------- |
| 696 | |
| 697 | // A utility used internally by ServiceWorkerGlobalScope to perform the book keeping |
| 698 | // for unhandled promise rejection notifications. The handler maintains a table of |
| 699 | // weak references to rejected promises that have not been handled and will handle |
| 700 | // emitting events and console warnings as appropriate. |
| 701 | class UnhandledRejectionHandler { |
| 702 | public: |
| 703 | using Handler = void(jsg::Lock& js, |
| 704 | v8::PromiseRejectEvent event, |
| 705 | jsg::V8Ref<v8::Promise> promise, |
| 706 | jsg::Value value); |
| 707 | |
| 708 | explicit UnhandledRejectionHandler(kj::Function<Handler> handler): handler(kj::mv(handler)) {} |
| 709 | |
| 710 | void report(jsg::Lock& js, |
| 711 | v8::PromiseRejectEvent event, |
| 712 | jsg::V8Ref<v8::Promise> promise, |
| 713 | jsg::Value value); |
| 714 | |
| 715 | void setUseMicrotasksCompletedCallback(bool value) { |
| 716 | useMicrotasksCompletedCallback = value; |
| 717 | } |
| 718 | |
| 719 | void clear(); |
| 720 | |
| 721 | JSG_MEMORY_INFO(UnhandledRejectionHandler) { |
| 722 | // TODO (soon): Can we reasonably measure the function handler? |
| 723 | tracker.trackField("unhandledRejections", unhandledRejections); |
| 724 | tracker.trackField("warnedRejections", warnedRejections); |
| 725 | } |
| 726 | |
| 727 | private: |
| 728 | // Used as part of the book keeping for unhandled rejections. When an |
| 729 | // unhandled rejection occurs, the unhandledRejections Table will be updated. |
| 730 | // If the rejection is later handled asynchronously, then the item will be |
| 731 | // removed from the table. When the unhandled rejection table is processed |
| 732 | // later in the event loop tick, any remaining rejections will generate a |
| 733 | // warning to the inspector console (if enabled); |
| 734 | struct UnhandledRejection { |
| 735 | explicit UnhandledRejection(jsg::Lock& js, |
| 736 | jsg::V8Ref<v8::Promise> promise, |
| 737 | jsg::Value value, |
| 738 | v8::Local<v8::Message> message); |
| 739 | |
| 740 | ~UnhandledRejection(); |
| 741 | |
| 742 | UnhandledRejection(UnhandledRejection&& other) = default; |
| 743 | UnhandledRejection& operator=(UnhandledRejection&& other) = default; |
| 744 | |
| 745 | // TODO(cleanup): It would be better to use a jsg::HashableV8Ref or |
| 746 | // jsg::Identity here but we need the Globals to always be weak so |
| 747 | // that the book keeping doesn't end up being a memory leak. |
| 748 | |
| 749 | uint hash; |
| 750 | |
| 751 | // We use v8::Globals directly here because these references are going to |
| 752 | // be made weak and could be garbage collected and cleared while the items |
| 753 | // are still in the unhandledRejections or warnedRejections tables. |
| 754 | |
| 755 | v8::Global<v8::Promise> promise; |
| 756 | v8::Global<v8::Value> value; |
| 757 | v8::Global<v8::Message> message; |
| 758 | kj::Maybe<Ref<AsyncContextFrame>> asyncContextFrame; |
| 759 | |
| 760 | inline bool isAlive() { |
| 761 | return !promise.IsEmpty() && !value.IsEmpty(); |
| 762 | } |
| 763 | |
| 764 | uint hashCode() const { |
| 765 | return hash; |
| 766 | } |
| 767 | |
| 768 | JSG_MEMORY_INFO(UnhandledRejection) { |
| 769 | tracker.trackField("promise", promise); |
| 770 | tracker.trackField("value", value); |
| 771 | visitForMemoryInfo(tracker); |
| 772 | } |
| 773 | void visitForMemoryInfo(MemoryTracker& tracker) const; |
| 774 | }; |
| 775 | |
| 776 | // A v8::Promise with memoized hash code. |
| 777 | struct HashedPromise { |
| 778 | v8::Local<v8::Promise> promise; |
| 779 | uint hash; |
| 780 | |
| 781 | HashedPromise(v8::Local<v8::Promise> promise) |
| 782 | : promise(promise), |
| 783 | hash(kj::hashCode(promise->GetIdentityHash())) {} |
| 784 | |
| 785 | JSG_MEMORY_INFO(HashedPromise) { |
| 786 | tracker.trackField("promise", promise); |
| 787 | } |
| 788 | }; |
| 789 | |
| 790 | struct UnhandledRejectionCallbacks { |
| 791 | inline const UnhandledRejection& keyForRow( |
| 792 | const UnhandledRejection& row KJ_LIFETIMEBOUND) const { |
| 793 | return row; |
| 794 | } |
| 795 | inline bool matches(const UnhandledRejection& a, const UnhandledRejection& b) const { |
| 796 | return a.promise == b.promise; |
| 797 | } |
| 798 | inline bool matches(const UnhandledRejection& a, const HashedPromise& b) const { |
| 799 | return a.promise == b.promise; |
| 800 | } |
| 801 | inline uint hashCode(const UnhandledRejection& row) const { |
| 802 | return row.hashCode(); |
| 803 | } |
| 804 | inline uint hashCode(const HashedPromise& key) const { |
| 805 | return key.hash; |
| 806 | } |
| 807 | }; |
| 808 | |
| 809 | kj::Function<Handler> handler; |
| 810 | bool scheduled = false; |
| 811 | // Controlled by the unhandled_rejection_after_microtask_checkpoint compat flag. |
| 812 | bool useMicrotasksCompletedCallback = false; |
| 813 | |
| 814 | using UnhandledRejectionsTable = |
| 815 | kj::Table<UnhandledRejection, kj::HashIndex<UnhandledRejectionCallbacks>>; |
| 816 | |
| 817 | UnhandledRejectionsTable unhandledRejections; |
| 818 | UnhandledRejectionsTable warnedRejections; |
| 819 | |
| 820 | void rejectedWithNoHandler(jsg::Lock& js, jsg::V8Ref<v8::Promise> promise, jsg::Value value); |
| 821 | void handledAfterRejection(jsg::Lock& js, jsg::V8Ref<v8::Promise> promise); |
| 822 | void ensureProcessingWarnings(jsg::Lock& js); |
| 823 | void processWarnings(jsg::Lock& js); |
| 824 | |
| 825 | // Must be static: V8 requires a plain C function pointer for this callback. |
| 826 | static void onMicrotasksCompleted(v8::Isolate* isolate, void* data); |
| 827 | }; |
| 828 | |
| 829 | } // namespace workerd::jsg |