diff --git a/Source/JavaScriptCore/runtime/JSMicrotask.cpp b/Source/JavaScriptCore/runtime/JSMicrotask.cpp index 7bf56f7caa3a..d803ef145745 100644 --- a/Source/JavaScriptCore/runtime/JSMicrotask.cpp +++ b/Source/JavaScriptCore/runtime/JSMicrotask.cpp @@ -899,11 +899,22 @@ static void promiseFinallyReactionJob(JSGlobalObject* globalObject, VM& vm, JSPr context->setHandlerOrContext(vm, valueOrReason); context->setPerCellBit(status == JSPromise::Status::Fulfilled); +#if USE(BUN_JSC_ADDITIONS) + // PromiseFinallyAwaitJob may run as a later microtask (scheduled below via + // performPromiseThenWithInternalMicrotask or + // createResolvingFunctionsWithInternalMicrotask), after this call's async + // context has been unwound. Capture it with the reaction so that phase 2 + // can restore it, like PromiseFinallyReactionJob does for phase 1. + JSValue scheduledContext = AsyncContextSwapScope::wrapWithCurrent(vm, globalObject, context); +#else + JSValue scheduledContext = context; +#endif + if (result.inherits()) { auto* promise = uncheckedDowncast(result); if (promise->realm() == globalObject && promise->isThenFastAndNonObservable()) { scope.release(); - promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::PromiseFinallyAwaitJob, resultPromise, context); + promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::PromiseFinallyAwaitJob, resultPromise, scheduledContext); return; } } @@ -945,7 +956,7 @@ static void promiseFinallyReactionJob(JSGlobalObject* globalObject, VM& vm, JSPr return; } - auto [resolve, reject] = JSPromise::createResolvingFunctionsWithInternalMicrotask(vm, globalObject, InternalMicrotask::PromiseFinallyAwaitJob, context); + auto [resolve, reject] = JSPromise::createResolvingFunctionsWithInternalMicrotask(vm, globalObject, InternalMicrotask::PromiseFinallyAwaitJob, scheduledContext); scope.release(); promiseResolveThenableJob(globalObject, resolutionObject, then, resolve, reject, microtaskCallCache); } @@ -1803,15 +1814,24 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas auto* promise = uncheckedDowncast(arguments[0]); auto* promiseToResolve = uncheckedDowncast(arguments[1]); - if (!promiseSpeciesWatchpointIsValid(vm, promise)) [[unlikely]] - RELEASE_AND_RETURN(scope, promiseResolveThenableJobFastSlow(globalObject, promise, promiseToResolve)); - #if USE(BUN_JSC_ADDITIONS) + // Install the async context captured by resolvePromise() for both paths below: the slow + // path registers reactions through performPromiseThen, which captures the current context. AsyncContextSwapScope asyncContextScope(vm, globalObject, arguments[2]); #endif + if (!promiseSpeciesWatchpointIsValid(vm, promise)) [[unlikely]] + RELEASE_AND_RETURN(scope, promiseResolveThenableJobFastSlow(globalObject, promise, promiseToResolve)); + scope.release(); +#if USE(BUN_JSC_ADDITIONS) + // promiseToResolve adopts promise's settlement in PromiseResolveWithoutHandlerJob. Capture + // the context (now installed) with the reaction so that it settles in the same context the + // slow path and PromiseResolveThenableJob would settle it in. + promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::PromiseResolveWithoutHandlerJob, promiseToResolve, AsyncContextSwapScope::wrapWithCurrent(vm, globalObject, jsUndefined())); +#else promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::PromiseResolveWithoutHandlerJob, promiseToResolve, jsUndefined()); +#endif return; } @@ -1861,6 +1881,15 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas } case InternalMicrotask::PromiseResolveWithoutHandlerJob: { +#if USE(BUN_JSC_ADDITIONS) + // arguments[2] is the context the reaction was registered with: an InternalFieldTuple + // [userContext, asyncContext] from performPromiseThen, performPromiseThenWithContext or + // PromiseResolveThenableJobFast, or undefined. Keep the async context installed while the + // derived promise settles, so that a rejection reaches the rejection tracker in the context + // then() was called in, the same as PromiseReactionJob does when there is a handler. + JSValue contextArg = arguments[2]; + AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::unwrapContextTuple(contextArg)); +#endif RELEASE_AND_RETURN(scope, promiseResolveWithoutHandlerJob(globalObject, vm, arguments[0], arguments[1], static_cast(payload))); } @@ -2110,8 +2139,13 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas // arguments[0] = unused (we get resultPromise from context) // arguments[1] = settled value from onFinally's result // arguments[2] = context (JSSlimPromiseReaction: promise=resultPromise, handlerOrContext=originalValue, perCellBit=wasFulfilled) + // OR InternalFieldTuple: [context, asyncContext] when Bun async context is present // payload = status of onFinally's result - auto* context = uncheckedDowncast(arguments[2]); + JSValue contextArg = arguments[2]; +#if USE(BUN_JSC_ADDITIONS) + AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::unwrapContextTuple(contextArg)); +#endif + auto* context = uncheckedDowncast(contextArg); auto* resultPromise = uncheckedDowncast(context->promise()); scope.release(); promiseFinallyAwaitJob(resultPromise->realm(), vm, diff --git a/Source/JavaScriptCore/runtime/JSPromise.cpp b/Source/JavaScriptCore/runtime/JSPromise.cpp index ff34f32f3d6a..28d51280836b 100644 --- a/Source/JavaScriptCore/runtime/JSPromise.cpp +++ b/Source/JavaScriptCore/runtime/JSPromise.cpp @@ -401,7 +401,13 @@ void JSPromise::performPromiseThen(VM& vm, JSGlobalObject* globalObject, JSValue globalObject->queueMicrotask(vm, InternalMicrotask::PromiseReactionJob, static_cast(Status::Rejected), promiseOrCapability, onRejected, settled); #endif else +#if USE(BUN_JSC_ADDITIONS) + // The derived promise settles in PromiseResolveWithoutHandlerJob; carry the async context + // so that it settles (and is reported to the rejection tracker) in the context then() was called in. + globalObject->queueMicrotask(vm, InternalMicrotask::PromiseResolveWithoutHandlerJob, static_cast(Status::Rejected), promiseOrCapability, settled, context); +#else globalObject->queueMicrotask(vm, InternalMicrotask::PromiseResolveWithoutHandlerJob, static_cast(Status::Rejected), promiseOrCapability, settled, jsUndefined()); +#endif markAsHandled(); break; } @@ -414,7 +420,11 @@ void JSPromise::performPromiseThen(VM& vm, JSGlobalObject* globalObject, JSValue globalObject->queueMicrotask(vm, InternalMicrotask::PromiseReactionJob, static_cast(Status::Fulfilled), promiseOrCapability, onFulfilled, settled); #endif else +#if USE(BUN_JSC_ADDITIONS) + globalObject->queueMicrotask(vm, InternalMicrotask::PromiseResolveWithoutHandlerJob, static_cast(Status::Fulfilled), promiseOrCapability, settled, context); +#else globalObject->queueMicrotask(vm, InternalMicrotask::PromiseResolveWithoutHandlerJob, static_cast(Status::Fulfilled), promiseOrCapability, settled, jsUndefined()); +#endif break; } } @@ -453,7 +463,7 @@ void JSPromise::performPromiseThenWithContext(VM& vm, JSGlobalObject* globalObje if (rejectedCallable) globalObject->queueMicrotask(vm, InternalMicrotask::PromiseReactionJob, static_cast(Status::Rejected), promiseOrCapability, onRejected, settled, context); else - globalObject->queueMicrotask(vm, InternalMicrotask::PromiseResolveWithoutHandlerJob, static_cast(Status::Rejected), promiseOrCapability, settled, jsUndefined()); + globalObject->queueMicrotask(vm, InternalMicrotask::PromiseResolveWithoutHandlerJob, static_cast(Status::Rejected), promiseOrCapability, settled, context); markAsHandled(); break; } @@ -462,7 +472,7 @@ void JSPromise::performPromiseThenWithContext(VM& vm, JSGlobalObject* globalObje if (fulfilledCallable) globalObject->queueMicrotask(vm, InternalMicrotask::PromiseReactionJob, static_cast(Status::Fulfilled), promiseOrCapability, onFulfilled, settled, context); else - globalObject->queueMicrotask(vm, InternalMicrotask::PromiseResolveWithoutHandlerJob, static_cast(Status::Fulfilled), promiseOrCapability, settled, jsUndefined()); + globalObject->queueMicrotask(vm, InternalMicrotask::PromiseResolveWithoutHandlerJob, static_cast(Status::Fulfilled), promiseOrCapability, settled, context); break; } } @@ -912,9 +922,12 @@ void JSPromise::triggerPromiseReactions(VM& vm, JSGlobalObject* globalObject, St // performPromiseThen normalizes non-callable sides to jsUndefined() when storing // an async context in a full reaction; cheap tag check instead of isCallable(). if (handler.isUndefined()) { + // No handler for this settlement: the derived promise just adopts it. Pass the + // reaction's context along so PromiseResolveWithoutHandlerJob settles it in the + // async context then() was called in. task = InternalMicrotask::PromiseResolveWithoutHandlerJob; handler = argument; - arg = jsUndefined(); + arg = fullReaction->context(); break; } JSValue context = fullReaction->context();