From c9ab11f64d0352f0297a101de484606f012da1c7 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 29 Oct 2026 02:57:02 +0000 Subject: [PATCH 1/7] [JSC] A job of the module loader runs under the async context of the import() that asked for it A job of the module loader ran under the async context that the job before it left. A module that import() loads saw no AsyncLocalStorage store. So did each hook of the host for a static dependency (resolve, fetch), the body of a CommonJS module that the loader evaluates, and the then() of a namespace. The loader now keeps the async context of whoever asks for a module with the request: in ModuleLoadingContext and ModuleLoaderPayload, where it takes a word that was padding, in the reaction of ModuleRegistryFetchSettled and AsyncModuleExecutionDone, and as the context of ImportModuleNamespace, which had none. The jobs that can reach code of the host or of the program run under it (ModuleLoaderAsyncContextScope). No cell and no job is added, and every job but ModuleRegistryFetchSettled carries what it carried. Where that context is no store, the job runs as it did: under what is in the slot. An entry point is evaluated by such a job, and what it enters with enterWith() is what the handlers of the process run under today. The jobs that a synchronous load drains under the context of its caller stay part of the require() call. What a hook of the host enters inside a load ends with the hook, so every request of a load keeps the context of the load. A loader that has its own context (Bun.ModuleGraph) runs the body of a CommonJS module under it, as it does the body of an ES module. AOT: the jobs of an import() in compiled code are queued with the async context of the call, which is handed on from job to job. An import() that is kept outside the queue keeps it. The steps that the runtime takes for the loader take the context that the loader would have: of the import() that is handed over, of the time before a file ran, and a CommonJS file that the runtime runs for the loader leaves the context as the job of the loader would. --- JSTests/BUN-TEST-DIFFERENCES.md | 2 +- .../stress/bun-async-context-propagation.js | 45 +++++++++++++ .../enters-dependency.js | 1 + .../bun-async-context-propagation/enters.js | 4 ++ .../graph-dependency.js | 1 + .../graph-nested.js | 1 + .../bun-async-context-propagation/graph.js | 4 ++ .../other-graph-dependency.js | 1 + .../other-graph.js | 3 + .../plain-dependency.js | 1 + .../bun-async-context-propagation/plain.js | 3 + .../bun-async-context-propagation/thenable.js | 4 ++ .../tla-importer.js | 3 + .../bun-async-context-propagation/tla.js | 3 + .../aot/runtime/AOTModuleEvaluation.cpp | 26 ++++++-- .../aot/runtime/AOTModuleMicrotasks.h | 5 +- .../JavaScriptCore/aot/runtime/AOTModules.cpp | 65 ++++++++++++------- .../JavaScriptCore/aot/runtime/AOTModules.h | 8 ++- .../runtime/AsyncContextSwapScope.h | 29 +++++++++ .../runtime/CyclicModuleRecord.cpp | 8 +++ Source/JavaScriptCore/runtime/JSMicrotask.cpp | 46 ++++++++++--- .../JavaScriptCore/runtime/JSModuleLoader.cpp | 41 +++++++++--- .../runtime/ModuleLoaderPayload.cpp | 16 ++++- .../runtime/ModuleLoaderPayload.h | 8 ++- .../runtime/ModuleLoadingContext.cpp | 45 +++++++++++-- .../runtime/ModuleLoadingContext.h | 14 ++-- .../runtime/ModuleRegistryEntry.cpp | 10 ++- .../runtime/ModuleRegistryEntry.h | 2 + .../runtime/SyntheticModuleRecord.cpp | 15 ++++- 29 files changed, 345 insertions(+), 69 deletions(-) create mode 100644 JSTests/stress/resources/bun-async-context-propagation/enters-dependency.js create mode 100644 JSTests/stress/resources/bun-async-context-propagation/enters.js create mode 100644 JSTests/stress/resources/bun-async-context-propagation/graph-dependency.js create mode 100644 JSTests/stress/resources/bun-async-context-propagation/graph-nested.js create mode 100644 JSTests/stress/resources/bun-async-context-propagation/graph.js create mode 100644 JSTests/stress/resources/bun-async-context-propagation/other-graph-dependency.js create mode 100644 JSTests/stress/resources/bun-async-context-propagation/other-graph.js create mode 100644 JSTests/stress/resources/bun-async-context-propagation/plain-dependency.js create mode 100644 JSTests/stress/resources/bun-async-context-propagation/plain.js create mode 100644 JSTests/stress/resources/bun-async-context-propagation/thenable.js create mode 100644 JSTests/stress/resources/bun-async-context-propagation/tla-importer.js create mode 100644 JSTests/stress/resources/bun-async-context-propagation/tla.js diff --git a/JSTests/BUN-TEST-DIFFERENCES.md b/JSTests/BUN-TEST-DIFFERENCES.md index ab4cc3f3767d9..2e0ec4ccbf91c 100644 --- a/JSTests/BUN-TEST-DIFFERENCES.md +++ b/JSTests/BUN-TEST-DIFFERENCES.md @@ -184,7 +184,7 @@ Not upstream's: a sync neither brings nor changes them. Most are named `stress/b | file | what it holds the engine to | |---|---| -| `stress/bun-async-context-propagation.js` | the embedder's async-context slot is captured when a job is scheduled and current while it runs | +| `stress/bun-async-context-propagation.js` | the embedder's async-context slot is captured when a job is scheduled and current while it runs, the jobs of the module loader included (its modules are in `stress/resources/bun-async-context-propagation/`) | | `stress/bun-aot-builtins-table.js` | the ahead-of-time compiler's table of built-ins (`Source/JavaScriptCore/aot/builtins/*.builtins.ts`) agrees with the engine. **After a sync that removes, renames, moves or reimplements a built-in that the table knows this fails, and each line it prints names the entry to fix.** | | `stress/sort-scratch-double-pivot.js` | double arithmetic in the comparator of an inlined sort stores no double into an Int32 array | | `stress/array-sort-inline-int32-double-comparator.js` | an earlier, smaller test for the sort fix | diff --git a/JSTests/stress/bun-async-context-propagation.js b/JSTests/stress/bun-async-context-propagation.js index 8b19b417c889b..ce1574a60269d 100644 --- a/JSTests/stress/bun-async-context-propagation.js +++ b/JSTests/stress/bun-async-context-propagation.js @@ -174,3 +174,48 @@ let log = []; shouldBe(seen, undefined, "residue from a sibling microtask"); shouldBe(get(), undefined, "residue after a microtask that captured nothing"); } + +// The module loader loads, links and evaluates an import() in jobs of its own. Where a context was current at the +// import(), they run under it, and what a module puts in the slot ends with the job that ran the module. +{ + log = []; + const entered = ["entered"]; + globalThis.entered = entered; + const settled = []; + const settle = name => [() => settled.push([name, get()]), error => settled.push([name, get(), error])]; + const resources = "./resources/bun-async-context-propagation/"; + inContext(A, () => { + import(resources + "graph.js").then(...settle("graph")); + import(resources + "tla-importer.js").then(...settle("tla importer")); + import(resources + "thenable.js").then(...settle("thenable")); + import(resources + "enters.js").then(...settle("enters")); + }); + inContext(B, () => { + import(resources + "other-graph.js").then(...settle("other graph")); + }); + import(resources + "plain.js").then(...settle("plain")); + drainMicrotasks(); + shouldBe(get(), undefined, "residue after the jobs of the module loader"); + const expected = { + // Module bodies, a static dependency first, and an import() that a body starts. + "graph dependency": A, "graph": A, "graph nested": A, + "other graph dependency": B, "other graph": B, + // A body behind its own await, and the module that waits for it. + "tla before await": A, "tla after await": A, "tla importer": A, + // The `then` of a module namespace. + "namespace then": A, + // A module that enters a context keeps it to the end of its job, and no longer. + "enters dependency": A, "enters": entered, + // An import() under no context. + "plain dependency": undefined, "plain": undefined, + }; + for (const [name, ctx] of log) + shouldBe(ctx, expected[name], name); + shouldBe(log.length, Object.keys(expected).length, "all module bodies ran"); + const expectedSettled = { "graph": A, "tla importer": A, "thenable": A, "enters": A, "other graph": B, "plain": undefined }; + for (const [name, ctx, error] of settled) { + shouldBe(error, undefined, "import() of " + name + " rejected with " + error + ":"); + shouldBe(ctx, expectedSettled[name], "import() of " + name); + } + shouldBe(settled.length, Object.keys(expectedSettled).length, "all imports settled"); +} diff --git a/JSTests/stress/resources/bun-async-context-propagation/enters-dependency.js b/JSTests/stress/resources/bun-async-context-propagation/enters-dependency.js new file mode 100644 index 0000000000000..481c643c08c08 --- /dev/null +++ b/JSTests/stress/resources/bun-async-context-propagation/enters-dependency.js @@ -0,0 +1 @@ +log.push(["enters dependency", $vm.asyncContext()]); diff --git a/JSTests/stress/resources/bun-async-context-propagation/enters.js b/JSTests/stress/resources/bun-async-context-propagation/enters.js new file mode 100644 index 0000000000000..a9fa404301c68 --- /dev/null +++ b/JSTests/stress/resources/bun-async-context-propagation/enters.js @@ -0,0 +1,4 @@ +import "./enters-dependency.js"; + +$vm.setAsyncContext(entered); +log.push(["enters", $vm.asyncContext()]); diff --git a/JSTests/stress/resources/bun-async-context-propagation/graph-dependency.js b/JSTests/stress/resources/bun-async-context-propagation/graph-dependency.js new file mode 100644 index 0000000000000..4031e94540610 --- /dev/null +++ b/JSTests/stress/resources/bun-async-context-propagation/graph-dependency.js @@ -0,0 +1 @@ +log.push(["graph dependency", $vm.asyncContext()]); diff --git a/JSTests/stress/resources/bun-async-context-propagation/graph-nested.js b/JSTests/stress/resources/bun-async-context-propagation/graph-nested.js new file mode 100644 index 0000000000000..b729126554cd4 --- /dev/null +++ b/JSTests/stress/resources/bun-async-context-propagation/graph-nested.js @@ -0,0 +1 @@ +log.push(["graph nested", $vm.asyncContext()]); diff --git a/JSTests/stress/resources/bun-async-context-propagation/graph.js b/JSTests/stress/resources/bun-async-context-propagation/graph.js new file mode 100644 index 0000000000000..172c011bf26be --- /dev/null +++ b/JSTests/stress/resources/bun-async-context-propagation/graph.js @@ -0,0 +1,4 @@ +import "./graph-dependency.js"; + +log.push(["graph", $vm.asyncContext()]); +import("./graph-nested.js"); diff --git a/JSTests/stress/resources/bun-async-context-propagation/other-graph-dependency.js b/JSTests/stress/resources/bun-async-context-propagation/other-graph-dependency.js new file mode 100644 index 0000000000000..af7b904271ddf --- /dev/null +++ b/JSTests/stress/resources/bun-async-context-propagation/other-graph-dependency.js @@ -0,0 +1 @@ +log.push(["other graph dependency", $vm.asyncContext()]); diff --git a/JSTests/stress/resources/bun-async-context-propagation/other-graph.js b/JSTests/stress/resources/bun-async-context-propagation/other-graph.js new file mode 100644 index 0000000000000..338c306e62777 --- /dev/null +++ b/JSTests/stress/resources/bun-async-context-propagation/other-graph.js @@ -0,0 +1,3 @@ +import "./other-graph-dependency.js"; + +log.push(["other graph", $vm.asyncContext()]); diff --git a/JSTests/stress/resources/bun-async-context-propagation/plain-dependency.js b/JSTests/stress/resources/bun-async-context-propagation/plain-dependency.js new file mode 100644 index 0000000000000..6de0252c76516 --- /dev/null +++ b/JSTests/stress/resources/bun-async-context-propagation/plain-dependency.js @@ -0,0 +1 @@ +log.push(["plain dependency", $vm.asyncContext()]); diff --git a/JSTests/stress/resources/bun-async-context-propagation/plain.js b/JSTests/stress/resources/bun-async-context-propagation/plain.js new file mode 100644 index 0000000000000..094883161c9ff --- /dev/null +++ b/JSTests/stress/resources/bun-async-context-propagation/plain.js @@ -0,0 +1,3 @@ +import "./plain-dependency.js"; + +log.push(["plain", $vm.asyncContext()]); diff --git a/JSTests/stress/resources/bun-async-context-propagation/thenable.js b/JSTests/stress/resources/bun-async-context-propagation/thenable.js new file mode 100644 index 0000000000000..affba5e768cf6 --- /dev/null +++ b/JSTests/stress/resources/bun-async-context-propagation/thenable.js @@ -0,0 +1,4 @@ +export function then(resolve) { + log.push(["namespace then", $vm.asyncContext()]); + resolve("thenable"); +} diff --git a/JSTests/stress/resources/bun-async-context-propagation/tla-importer.js b/JSTests/stress/resources/bun-async-context-propagation/tla-importer.js new file mode 100644 index 0000000000000..6be26d5a715a2 --- /dev/null +++ b/JSTests/stress/resources/bun-async-context-propagation/tla-importer.js @@ -0,0 +1,3 @@ +import "./tla.js"; + +log.push(["tla importer", $vm.asyncContext()]); diff --git a/JSTests/stress/resources/bun-async-context-propagation/tla.js b/JSTests/stress/resources/bun-async-context-propagation/tla.js new file mode 100644 index 0000000000000..ffe97e4390a71 --- /dev/null +++ b/JSTests/stress/resources/bun-async-context-propagation/tla.js @@ -0,0 +1,3 @@ +log.push(["tla before await", $vm.asyncContext()]); +await null; +log.push(["tla after await", $vm.asyncContext()]); diff --git a/Source/JavaScriptCore/aot/runtime/AOTModuleEvaluation.cpp b/Source/JavaScriptCore/aot/runtime/AOTModuleEvaluation.cpp index f9f5622c8cd5d..ecff5066f366b 100644 --- a/Source/JavaScriptCore/aot/runtime/AOTModuleEvaluation.cpp +++ b/Source/JavaScriptCore/aot/runtime/AOTModuleEvaluation.cpp @@ -20,6 +20,7 @@ #include "AOTState.h" #include "AOTStronglyConnectedComponents.h" #include "AOTStubEntrypoints.h" +#include "AsyncContextSwapScope.h" #include "ExceptionHelpers.h" #include "GlobalObjectMethodTable.h" #include "JSCInlines.h" @@ -894,13 +895,18 @@ bool Modules::evaluateImportedCommonJSFiles(ModuleID root, JobsOfImportedFiles j bool didRunFile = false; MicrotaskQueue& queue = vm.defaultMicrotaskQueue(); FilesBeingRun filesBeingRun { root, std::nullopt, m_innermostFilesBeingRun, jobs == JobsOfImportedFiles::WaitForLoader, load }; + filesBeingRun.asyncContext = AsyncContextSwapScope::current(vm, m_instance.globalObject); SetForScope innermostFilesBeingRun(m_innermostFilesBeingRun, &filesBeingRun); SetForScope modulesWhoseFilesRunAheadOfLoader(vm.aotState().modulesWhoseFilesRunAheadOfLoader, filesBeingRun.isAheadOfJobsOfLoader ? this : vm.aotState().modulesWhoseFilesRunAheadOfLoader); // The start of the root calls the files one after the other, and leaves where one of them has queued a job or where the root would begin. if (jobs == JobsOfImportedFiles::WaitForLoader && data(root).evaluatesItself() && data(root).isEntryPoint() && queue.isEmpty() && !m_rootWhoseFilesRun) { ensureRecord(root)->retain(); if (storedStatus(root) == StoredModuleStatus::InRecord) { - callBodyUnderOwnMark(root, false, true); + { + // As for a file below. The files run in one call here, so one of them leaves the next what it entered. + ModuleLoaderAsyncContextScope asyncContextOfFiles(vm, m_instance.globalObject, filesBeingRun.asyncContext); + callBodyUnderOwnMark(root, false, true); + } didRunFile = true; if (filesBeingRun.fileHandedToLoader) [[unlikely]] { ModuleID file = *filesBeingRun.fileHandedToLoader; @@ -977,11 +983,17 @@ bool Modules::evaluateImportedCommonJSFiles(ModuleID root, JobsOfImportedFiles j size_t numberOfJobs = numberOfJobsThatWouldWait(jobs); filesBeingRun.fileOrFace = imported; filesBeingRun.numberOfJobsInFrontOfFile = numberOfJobs; - // As the start of the root would call it. - if (storedStatus(imported) < StoredModuleStatus::InRecord && data(imported).evaluatesItself()) - evaluateByCallingBody(imported); - else - evaluateModuleInRecord(imported); + { + // The loader of `bun run` runs the file in a job of its own, under the async context of whoever asked for the file: what the file enters ends with that job where the job has a store. + std::optional asyncContextOfFile; + if (jobs == JobsOfImportedFiles::WaitForLoader) + asyncContextOfFile.emplace(vm, m_instance.globalObject, filesBeingRun.asyncContext); + // As the start of the root would call it. + if (storedStatus(imported) < StoredModuleStatus::InRecord && data(imported).evaluatesItself()) + evaluateByCallingBody(imported); + else + evaluateModuleInRecord(imported); + } Exception* exception = scope.exception(); if (filesBeingRun.fileHandedToLoader) [[unlikely]] { JSValue errorOfFile; @@ -1110,6 +1122,8 @@ void Modules::handGraphToLoader(FilesBeingRun& files) SetForScope fileBeingHandedToLoader(m_fileBeingHandedToLoader, fileOf(*running)); // The steps were due before any require() of the file began. SetForScope noSynchronousLoad(vm.m_synchronousModuleQueueSetAside, vm.m_synchronousModuleQueue); + // And before the file entered an async context: they take what was current then, for themselves and for the requests that they make. + AsyncContextSwapScope asyncContextBeforeFiles(vm, m_instance.globalObject, files.asyncContext); if (files.load) { ModuleRecord* record = recordIfExists(files.root); files.load->forgetVisited(record); diff --git a/Source/JavaScriptCore/aot/runtime/AOTModuleMicrotasks.h b/Source/JavaScriptCore/aot/runtime/AOTModuleMicrotasks.h index ebd56b6d44ddf..abc93cbe506e1 100644 --- a/Source/JavaScriptCore/aot/runtime/AOTModuleMicrotasks.h +++ b/Source/JavaScriptCore/aot/runtime/AOTModuleMicrotasks.h @@ -16,9 +16,10 @@ class JSGlobalObject; namespace AOT { -void runCompiledModuleImport(JSGlobalObject*, JSValue importPromise, JSValue module, JSValue referrerAsyncOrder); +// asyncContext: what the job was queued with, which the job that it queues next is queued with too. +void runCompiledModuleImport(JSGlobalObject*, JSValue importPromise, JSValue module, JSValue referrerAsyncOrder, JSValue asyncContext); void runCompiledModuleImportSettled(JSGlobalObject*, JSValue importPromise, JSValue resolution, JSValue module, JSPromise::Status); -void runCompiledModuleImportStep(JSValue importPromise, JSValue module, JSValue error, uint8_t stepsLeft); +void runCompiledModuleImportStep(JSValue importPromise, JSValue module, JSValue error, uint8_t stepsLeft, JSValue asyncContext); // Behind the last step that the loader takes for a graph. void didSettleLoad(JSPromise* settled); diff --git a/Source/JavaScriptCore/aot/runtime/AOTModules.cpp b/Source/JavaScriptCore/aot/runtime/AOTModules.cpp index 0fa9b1bd10967..ab10a7f3e3674 100644 --- a/Source/JavaScriptCore/aot/runtime/AOTModules.cpp +++ b/Source/JavaScriptCore/aot/runtime/AOTModules.cpp @@ -29,6 +29,7 @@ #include "AOTThreshold.h" #include "AOTVMState.h" #include "AbstractSlotVisitorInlines.h" +#include "AsyncContextSwapScope.h" #include "DeferGCInlines.h" #include "DeferTermination.h" #include "DisallowVMEntry.h" @@ -713,20 +714,29 @@ void Modules::throwFailure(ModuleFailureID id, bool isForRequire) throwException(realm, scope, createModuleMissingAtBuildError(realm, specifier, importer, isForRequire ? "MODULE_NOT_FOUND"_s : "ERR_MODULE_NOT_FOUND"_s)); } -static void queueImportJob(JSGlobalObject* realm, JSPromise* importPromise, ModuleID module, std::optional referrerAsyncOrder) +// asyncContext: the async context of the call of import(), which every job of that import() is queued with and runs under where it is a store (ModuleLoaderAsyncContextScope). +// It is handed on from job to job, not read again: a module that the import() runs may have entered another. +static void queueImportJob(JSGlobalObject* realm, JSPromise* importPromise, ModuleID module, std::optional referrerAsyncOrder, JSValue asyncContext) { constexpr uint8_t unusedPayload = 0; - realm->queueMicrotask(realm->vm(), InternalMicrotask::CompiledModuleImport, unusedPayload, importPromise, jsNumber(module.index()), referrerAsyncOrder ? jsNumber(static_cast(*referrerAsyncOrder)) : jsUndefined()); + realm->queueMicrotask(realm->vm(), InternalMicrotask::CompiledModuleImport, unusedPayload, importPromise, jsNumber(module.index()), referrerAsyncOrder ? jsNumber(static_cast(*referrerAsyncOrder)) : jsUndefined(), asyncContext); +} + +Strong Modules::currentAsyncContext() const +{ + VM& vm = *m_instance.vm; + JSValue asyncContext = AsyncContextSwapScope::current(vm, m_instance.globalObject); + return asyncContext.isUndefined() ? Strong() : Strong(vm, asyncContext); } // A synchronous module load runs the jobs it diverts in its own realm, so the realm of a job is the one its import promise was created in. -void runCompiledModuleImport(JSGlobalObject*, JSValue importPromise, JSValue module, JSValue referrerAsyncOrder) +void runCompiledModuleImport(JSGlobalObject*, JSValue importPromise, JSValue module, JSValue referrerAsyncOrder, JSValue asyncContext) { auto* promise = uncheckedDowncast(importPromise); Modules* modules = modulesOf(promise->realm()); if (modules->continueCountedImport(promise, module, referrerAsyncOrder)) return; - modules->continueImport(promise, ModuleID(static_cast(module.asInt32())), referrerAsyncOrder.isUndefined() ? std::nullopt : std::optional(static_cast(referrerAsyncOrder.asNumber()))); + modules->continueImport(promise, ModuleID(static_cast(module.asInt32())), referrerAsyncOrder.isUndefined() ? std::nullopt : std::optional(static_cast(referrerAsyncOrder.asNumber())), asyncContext); } // The jobs, one behind the other, in which the loader of `bun run` hands on the namespace object of a module that it has evaluated. @@ -747,17 +757,17 @@ static constexpr uint32_t jobsFromRequestToGenerator = 2; static constexpr size_t numberOfJobsBehindEvaluation = std::size(jobsOfImportOfEvaluatedModule) - 2; // The error is empty where the evaluation has not failed. -static void queueStepOfImport(JSPromise* importPromise, JSValue module, uint8_t stepsLeft, JSValue error = { }) +static void queueStepOfImport(JSPromise* importPromise, JSValue module, uint8_t stepsLeft, JSValue asyncContext, JSValue error = { }) { JSGlobalObject* realm = importPromise->realm(); - realm->queueMicrotask(realm->vm(), InternalMicrotask::CompiledModuleImportStep, stepsLeft, importPromise, module, error); + realm->queueMicrotask(realm->vm(), InternalMicrotask::CompiledModuleImportStep, stepsLeft, importPromise, module, error, asyncContext); } -void runCompiledModuleImportStep(JSValue importPromise, JSValue module, JSValue error, uint8_t stepsLeft) +void runCompiledModuleImportStep(JSValue importPromise, JSValue module, JSValue error, uint8_t stepsLeft, JSValue asyncContext) { auto* promise = uncheckedDowncast(importPromise); if (--stepsLeft) - queueStepOfImport(promise, module, stepsLeft, error); + queueStepOfImport(promise, module, stepsLeft, asyncContext, error); else modulesOf(promise->realm())->finishImportOfEvaluatedModule(promise, ModuleID(static_cast(module.asInt32())), error); } @@ -768,8 +778,9 @@ void runCompiledModuleImportSettled(JSGlobalObject*, JSValue importPromise, JSVa RELEASE_ASSERT(status != JSPromise::Status::Pending); Modules* modules = modulesOf(promise->realm()); ModuleID imported(static_cast(module.asInt32())); + // This job has a cell, and with it no place for the async context of the import(): the steps behind it are queued without one. if (modules->isImportedByLoader(imported)) - queueStepOfImport(promise, module, numberOfJobsBehindEvaluation - 1, status == JSPromise::Status::Rejected ? resolution : JSValue()); + queueStepOfImport(promise, module, numberOfJobsBehindEvaluation - 1, JSValue(), status == JSPromise::Status::Rejected ? resolution : JSValue()); else if (status == JSPromise::Status::Rejected) promise->reject(promise->vm(), resolution); else @@ -870,7 +881,7 @@ JSPromise* Modules::startImport(ModuleID module, std::optional referrer if (isCalledByProgram && (!isBuiltinModule || isBuiltinModuleThatLoaderHas) && loaderOnlyHandsOverNamespace(module)) { JSPromise* promise = JSPromise::create(vm, realm->promiseStructure()); if (promise->isThenFastAndNonObservable()) [[likely]] { - queueStepOfImport(promise, jsNumber(module.index()), std::size(jobsOfImportOfEvaluatedModule)); + queueStepOfImport(promise, jsNumber(module.index()), std::size(jobsOfImportOfEvaluatedModule), AsyncContextSwapScope::current(vm, realm)); return promise; } } @@ -886,13 +897,13 @@ JSPromise* Modules::startImport(ModuleID module, std::optional referrer JSPromise* promise = JSPromise::create(vm, realm->promiseStructure()); if (promise->isThenFastAndNonObservable()) [[likely]] { uint32_t serial = m_serialOfNextCountedImport++; - m_countedImports.append(CountedImport { Strong(vm, promise), module, referrerAsyncOrder, serial }); + m_countedImports.append(CountedImport { Strong(vm, promise), currentAsyncContext(), module, referrerAsyncOrder, serial }); ++m_numberOfCountedImports; if (!m_orderOfCountedJobs.isEmpty()) { m_orderOfCountedJobs.append(serial); m_modulesOfCountedImports.set(module.index()); } - queueImportJob(realm, promise, module, referrerAsyncOrder); + queueImportJob(realm, promise, module, referrerAsyncOrder, AsyncContextSwapScope::current(vm, realm)); return promise; } } @@ -900,7 +911,7 @@ JSPromise* Modules::startImport(ModuleID module, std::optional referrer RETURN_IF_EXCEPTION(scope, rejectedPromiseWithPendingException(realm, scope)); if (asyncLoadMustWaitFor(module) && !loader()->registryEntry(moduleKey(module))) [[unlikely]] { JSPromise* promise = JSPromise::create(vm, realm->promiseStructure()); - m_deferredImports.append(DeferredImport { fileOf(module), Strong(vm, promise) }); + m_deferredImports.append(DeferredImport { fileOf(module), Strong(vm, promise), currentAsyncContext() }); return promise; } // `bun run` has loaded a module with the graph that first came to it, and its loader knows the module from then on. @@ -929,7 +940,7 @@ JSPromise* Modules::startImport(ModuleID module, std::optional referrer } JSPromise* importPromise = JSPromise::create(*m_instance.vm, realm->promiseStructure()); - queueImportJob(realm, importPromise, module, referrerAsyncOrder); + queueImportJob(realm, importPromise, module, referrerAsyncOrder, AsyncContextSwapScope::current(*m_instance.vm, realm)); return importPromise; } @@ -945,7 +956,7 @@ void Modules::resumeDeferredImports(ModuleID file, uint8_t jobsUntilLoaded) } // A failed load skips ModuleLoadingContext::Step::Requested and DynamicImportEvaluateSettled. uint8_t skippedJobs = error ? 2 : 0; - queueStepOfImport(import.promise.get(), jsNumber(file.index()), jobsUntilLoaded + std::size(jobsOfImportOfEvaluatedModule) - 1 - skippedJobs, error); + queueStepOfImport(import.promise.get(), jsNumber(file.index()), jobsUntilLoaded + std::size(jobsOfImportOfEvaluatedModule) - 1 - skippedJobs, import.asyncContext.get(), error); return true; }); } @@ -1098,6 +1109,8 @@ void Modules::handCountedImportsToLoader() // The steps run no code of the program, so nothing joins or leaves the list. for (uint32_t serial : std::exchange(m_orderOfCountedJobs, { })) { CountedImport& import = find(serial); + // What the loader keeps for the requests of an import() is the async context of its call, whoever hands the import() over. + AsyncContextSwapScope asyncContextOfImport(vm, m_instance.globalObject, import.asyncContext.get()); if (!import.jobsOfLoader) { import.jobsOfLoader = MicrotaskQueue::create(vm); JSPromise* loaded = askLoaderForImport(import.module, import.referrerAsyncOrder); @@ -1152,7 +1165,7 @@ bool Modules::continueCountedImport(JSPromise* importPromise, JSValue module, JS if (!m_orderOfCountedJobs.isEmpty()) m_orderOfCountedJobs.append(counted.serial); constexpr uint8_t unusedPayload = 0; - m_instance.globalObject->queueMicrotask(*m_instance.vm, InternalMicrotask::CompiledModuleImport, unusedPayload, importPromise, module, referrerAsyncOrder); + m_instance.globalObject->queueMicrotask(*m_instance.vm, InternalMicrotask::CompiledModuleImport, unusedPayload, importPromise, module, referrerAsyncOrder, counted.asyncContext.get()); return true; } CountedImport import = WTF::move(counted); @@ -1290,7 +1303,7 @@ void Modules::decideHowCountedImportGoesOn(CountedImport& import) import.jobsRun = job; if (!m_orderOfCountedJobs.isEmpty()) m_orderOfCountedJobs.append(import.serial); - queueImportJob(m_instance.globalObject, importPromise, module, import.referrerAsyncOrder); + queueImportJob(m_instance.globalObject, importPromise, module, import.referrerAsyncOrder, import.asyncContext.get()); didLeaveJob.release(); m_innermostCountedImportInOwnJob = std::exchange(import.outerInOwnJob, nullptr); m_countedImports.append(WTF::move(import)); @@ -1338,11 +1351,11 @@ void Modules::decideHowCountedImportGoesOn(CountedImport& import) RETURN_IF_EXCEPTION(scope, void()); switch (evaluation->status()) { case JSPromise::Status::Fulfilled: - queueStepOfImport(importPromise, jsNumber(module.index()), numberOfJobsBehindEvaluation); + queueStepOfImport(importPromise, jsNumber(module.index()), numberOfJobsBehindEvaluation, import.asyncContext.get()); return; case JSPromise::Status::Rejected: evaluation->markAsHandled(); - queueStepOfImport(importPromise, jsNumber(module.index()), numberOfJobsBehindEvaluation, evaluation->result()); + queueStepOfImport(importPromise, jsNumber(module.index()), numberOfJobsBehindEvaluation, import.asyncContext.get(), evaluation->result()); return; case JSPromise::Status::Pending: evaluation->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::CompiledModuleImportSettled, importPromise, jsNumber(module.index())); @@ -1401,22 +1414,22 @@ void Modules::keepLinkWalk(std::unique_ptr&& walk) m_linkWalksOfLoads.append(WTF::move(walk)); } -void Modules::continueImport(JSPromise* importPromise, ModuleID module, std::optional referrerAsyncOrder) +void Modules::continueImport(JSPromise* importPromise, ModuleID module, std::optional referrerAsyncOrder, JSValue asyncContext) { VM& vm = *m_instance.vm; auto scope = DECLARE_THROW_SCOPE(vm); if (status(module) == ModuleStatus::Evaluating) { // The body of the module is on the stack below this job, so a job queued now would run, and queue itself again, before the body can go on. - vm.whenIdle([realm = m_instance.globalObject, importPromise = Strong(vm, importPromise), module, referrerAsyncOrder] { - queueImportJob(realm, importPromise.get(), module, referrerAsyncOrder); + vm.whenIdle([realm = m_instance.globalObject, importPromise = Strong(vm, importPromise), asyncContext = asyncContext && !asyncContext.isUndefined() ? Strong(vm, asyncContext) : Strong(), module, referrerAsyncOrder] { + queueImportJob(realm, importPromise.get(), module, referrerAsyncOrder, asyncContext.get()); }); return; } if (m_importThatWaitsForQueuedJobs == importPromise) { if (!vm.defaultMicrotaskQueue().isEmpty()) { - queueImportJob(m_instance.globalObject, importPromise, module, referrerAsyncOrder); + queueImportJob(m_instance.globalObject, importPromise, module, referrerAsyncOrder, asyncContext); return; } m_importThatWaitsForQueuedJobs = nullptr; @@ -1438,7 +1451,7 @@ void Modules::continueImport(JSPromise* importPromise, ModuleID module, std::opt m_importThatWaitsForQueuedJobs = importPromise; if (didRunModule || didRunFile) { keepLinkWalk(WTF::move(linkWalk)); - queueImportJob(m_instance.globalObject, importPromise, module, referrerAsyncOrder); + queueImportJob(m_instance.globalObject, importPromise, module, referrerAsyncOrder, asyncContext); return; } @@ -2499,6 +2512,8 @@ void ModuleRecord::executeAsync(JSGlobalObject* globalObject) ASSERT(status() == Status::Evaluating || status() == Status::EvaluatingAsync); ASSERT(hasTLA()); + // As CyclicModuleRecord::executeAsync(), which registers the job before it runs the body: the modules that wait for this one run under the async context of whoever evaluates it, not under what the body has entered. + JSValue asyncContext = AsyncContextSwapScope::current(vm, globalObject); JSValue result = modules().callBodyOfModuleEnteredByLoader(m_module); RETURN_IF_EXCEPTION(scope, void()); auto* promise = dynamicDowncast(result); @@ -2506,7 +2521,7 @@ void ModuleRecord::executeAsync(JSGlobalObject* globalObject) promise = JSPromise::resolvedPromise(globalObject, result); RETURN_IF_EXCEPTION(scope, void()); } - promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::AsyncModuleExecutionDone, nullptr, this); + promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::AsyncModuleExecutionDone, nullptr, this, asyncContext); } void ModuleRecord::appendDynamicStarTargets(StarExportTargets& targets) diff --git a/Source/JavaScriptCore/aot/runtime/AOTModules.h b/Source/JavaScriptCore/aot/runtime/AOTModules.h index e005896044601..0042131b7ea7c 100644 --- a/Source/JavaScriptCore/aot/runtime/AOTModules.h +++ b/Source/JavaScriptCore/aot/runtime/AOTModules.h @@ -245,7 +245,7 @@ class Modules { enum class ImportCaller : bool { CompiledCode, Host }; JSPromise* startImport(ModuleID, std::optional referrerAsyncOrder, ImportCaller = ImportCaller::CompiledCode); JSPromise* importDynamic(JSValue specifier, JSValue options, ModuleID referrer); - void continueImport(JSPromise* importPromise, ModuleID, std::optional referrerAsyncOrder); + void continueImport(JSPromise* importPromise, ModuleID, std::optional referrerAsyncOrder, JSValue asyncContext); void resolveWithNamespace(JSPromise* importPromise, ModuleID); void finishImportOfEvaluatedModule(JSPromise* importPromise, ModuleID, JSValue error); // Says whether the jobs of that import() are counted. @@ -467,6 +467,8 @@ class Modules { std::optional fileHandedToLoader { }; // By a CommonJS file: how many modules stand between the root and the nearest one that imports it. Made when something asks. Vector distanceOfFiles { }; + // The async context that was current before the files ran. The steps of the loader that were due by then run under it: handGraphToLoader(). + JSValue asyncContext { }; }; void handGraphToLoader(FilesBeingRun&); static void runJobsThatAreDue(VM&, MicrotaskQueue&); @@ -550,8 +552,11 @@ class Modules { std::span level(size_t depth) { return found.mutableSpan().subspan(levels[depth].first, (depth + 1 == levels.size() ? found.size() : levels[depth + 1].first) - levels[depth].first); } }; bool isLeftToLoad(PlanOfLoad&, PlanOfLoad::Found&); + // For an import() that is kept outside the queue of jobs: the async context of its call, which the jobs that take it up again are queued with. Empty where there is none. + Strong currentAsyncContext() const; struct CountedImport { Strong promise; + Strong asyncContext; ModuleID module; std::optional referrerAsyncOrder; uint32_t serial; @@ -639,6 +644,7 @@ class Modules { struct DeferredImport { ModuleID file; Strong promise; + Strong asyncContext; }; Vector m_deferredImports; ModuleID fileOf(ModuleID fileOrFace) const { return m_table.fileBehindFace(fileOrFace).value_or(fileOrFace); } diff --git a/Source/JavaScriptCore/runtime/AsyncContextSwapScope.h b/Source/JavaScriptCore/runtime/AsyncContextSwapScope.h index 0913c7623c45e..b3c1c369c13e9 100644 --- a/Source/JavaScriptCore/runtime/AsyncContextSwapScope.h +++ b/Source/JavaScriptCore/runtime/AsyncContextSwapScope.h @@ -30,6 +30,8 @@ #include "InternalFieldTuple.h" #include "JSCast.h" #include "JSGlobalObject.h" +#include "VM.h" +#include #include #include @@ -160,6 +162,33 @@ class AsyncContextSwapScope { JSValue m_restoreAsyncContext; }; +// For a job of the module loader, and for what the loader calls on its way. The loader keeps the async context of +// whoever asked for a module with the request (ModuleLoadingContext, ModuleLoaderPayload, ModuleRegistryEntry), or in +// the job where the job has the slot free. Where that context is a store, the job is a continuation of the import() +// that asked: it runs under the store, and what it leaves in the slot ends with it. +// +// Two cases differ from AsyncContextSwapScope: +// - A job whose request captured no store runs as such a job always did: under what is in the slot, and what it leaves +// there stays. An entry point is evaluated by such a job, and what it enters is where the program goes on from. +// - While a synchronous load drains its jobs, a job under the context that is current already is part of the +// require() call. What it enters is the caller's, as it is for a CommonJS module. +class ModuleLoaderAsyncContextScope { + WTF_MAKE_NONCOPYABLE(ModuleLoaderAsyncContextScope); + WTF_FORBID_HEAP_ALLOCATION; +public: + ALWAYS_INLINE ModuleLoaderAsyncContextScope(VM& vm, JSGlobalObject* globalObject, JSValue asyncContext) + { + if (asyncContext.isEmpty() || asyncContext.isUndefined()) [[likely]] + return; + if (vm.synchronousModuleQueue() && asyncContext == AsyncContextSwapScope::current(vm, globalObject)) [[unlikely]] + return; + m_scope.emplace(vm, globalObject, asyncContext); + } + +private: + std::optional m_scope; +}; + } // namespace JSC #endif // USE(BUN_JSC_ADDITIONS) diff --git a/Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp b/Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp index 4db40611bfd18..02be5a44b3af3 100644 --- a/Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp +++ b/Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp @@ -29,6 +29,9 @@ #if ENABLE(AOT) #include "AOTModuleRecord.h" #endif +#if USE(BUN_JSC_ADDITIONS) +#include "AsyncContextSwapScope.h" +#endif #include "BuiltinNames.h" #include "ExceptionHelpers.h" #include "Interpreter.h" @@ -735,7 +738,12 @@ void CyclicModuleRecord::executeAsync(JSGlobalObject* globalObject) // 7. Let onRejected be CreateBuiltinFunction(rejectedClosure, 0, "", « »). // Also handled in JSMicrotask.cpp. // 8. Perform PerformPromiseThen(capability.[[Promise]], onFulfilled, onRejected). +#if USE(BUN_JSC_ADDITIONS) + // The modules that wait for this one run in that job, under the async context of whoever evaluates this one. + promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::AsyncModuleExecutionDone, nullptr, this, AsyncContextSwapScope::current(vm, globalObject)); +#else promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::AsyncModuleExecutionDone, nullptr, this); +#endif // 9. Perform ! module.ExecuteModule(capability). execute(globalObject, promise); RETURN_IF_EXCEPTION(scope, void()); diff --git a/Source/JavaScriptCore/runtime/JSMicrotask.cpp b/Source/JavaScriptCore/runtime/JSMicrotask.cpp index 0734c48cd212e..32b104eefd377 100644 --- a/Source/JavaScriptCore/runtime/JSMicrotask.cpp +++ b/Source/JavaScriptCore/runtime/JSMicrotask.cpp @@ -1099,11 +1099,11 @@ static void asyncModuleExecutionResume(JSGlobalObject* globalObject, VM& vm, JSM static void moduleRegistryFetchSettled(JSGlobalObject* globalObject, VM& vm, ThrowScope& scope, std::span arguments, uint8_t payload) { - // arguments[0] = pre-created modulePromise + // arguments[0] = unused: the reaction keeps the async context there // arguments[1] = resolution (JSSourceCode* for JS/WASM/JSON/text, JSValue for anything else) or rejection (error) - // arguments[2] = ModuleRegistryEntry* + // arguments[2] = ModuleRegistryEntry*, which holds the pre-created modulePromise auto* entry = uncheckedDowncast(arguments[2]); - auto* modulePromise = uncheckedDowncast(arguments[0]); + auto* modulePromise = entry->modulePromise(); auto status = static_cast(payload); #if USE(BUN_JSC_ADDITIONS) // hostLoadImportedModule may have already replayed this step inline while @@ -1355,7 +1355,7 @@ static void moduleLoadTopSettled(JSGlobalObject* globalObject, VM& vm, ThrowScop innerLoadFlags.add(ModuleLoadFlag::UseImportMap); if (context->dynamic()) { #if USE(BUN_JSC_ADDITIONS) - combinedCell = ModuleLoaderPayload::create(vm, statePromise, context->deferred(), context->referrerAsyncOrder()); + combinedCell = ModuleLoaderPayload::create(vm, statePromise, context->deferred(), context->referrerAsyncOrder(), context->asyncContext()); #else combinedCell = ModuleLoaderPayload::create(vm, statePromise, context->deferred()); #endif @@ -1791,7 +1791,7 @@ static void importModuleNamespace(JSGlobalObject* globalObject, VM& vm, ThrowSco // requestImportModule: namespace getter // arguments[0] = resultPromise // arguments[1] = module namespace (from dynamic import pipeline) or error - // arguments[2] = unused + // arguments[2] = the async context of the import(), which runInternalMicrotask() has installed auto* resultPromise = uncheckedDowncast(arguments[0]); auto status = static_cast(payload); if (status == JSPromise::Status::Fulfilled) { @@ -2453,6 +2453,9 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas #endif case InternalMicrotask::AsyncModuleExecutionDone: { +#if USE(BUN_JSC_ADDITIONS) + ModuleLoaderAsyncContextScope asyncContextScope(vm, globalObject, argument3); +#endif auto* module = uncheckedDowncast(argument2); JSGlobalObject* realm = nullptr; #if ENABLE(AOT) @@ -2465,14 +2468,18 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas } #if ENABLE(AOT) - case InternalMicrotask::CompiledModuleImport: - RELEASE_AND_RETURN(scope, AOT::runCompiledModuleImport(globalObject, argument0, argument1, argument2)); + case InternalMicrotask::CompiledModuleImport: { + ModuleLoaderAsyncContextScope asyncContextScope(vm, globalObject, argument3); + RELEASE_AND_RETURN(scope, AOT::runCompiledModuleImport(globalObject, argument0, argument1, argument2, argument3)); + } case InternalMicrotask::CompiledModuleImportSettled: RELEASE_AND_RETURN(scope, AOT::runCompiledModuleImportSettled(globalObject, argument0, argument1, argument2, static_cast(payload))); - case InternalMicrotask::CompiledModuleImportStep: - RELEASE_AND_RETURN(scope, AOT::runCompiledModuleImportStep(argument0, argument1, argument2, payload)); + case InternalMicrotask::CompiledModuleImportStep: { + ModuleLoaderAsyncContextScope asyncContextScope(vm, globalObject, argument3); + RELEASE_AND_RETURN(scope, AOT::runCompiledModuleImportStep(argument0, argument1, argument2, payload, argument3)); + } #endif case InternalMicrotask::AsyncModuleExecutionResume: { @@ -2488,6 +2495,9 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas } case InternalMicrotask::ModuleRegistryFetchSettled: { +#if USE(BUN_JSC_ADDITIONS) + ModuleLoaderAsyncContextScope asyncContextScope(vm, globalObject, argument3); +#endif moduleRegistryFetchSettled(globalObject, vm, scope, std::array { argument0, argument1, argument2 }, payload); return; } @@ -2503,11 +2513,17 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas } case InternalMicrotask::ModuleLoadStep: { +#if USE(BUN_JSC_ADDITIONS) + ModuleLoaderAsyncContextScope asyncContextScope(vm, globalObject, uncheckedDowncast(argument2)->asyncContext()); +#endif moduleLoadStep(globalObject, vm, scope, std::array { argument0, argument1, argument2 }, payload); return; } case InternalMicrotask::ModuleLoadTopSettled: { +#if USE(BUN_JSC_ADDITIONS) + ModuleLoaderAsyncContextScope asyncContextScope(vm, globalObject, uncheckedDowncast(argument2)->asyncContext()); +#endif moduleLoadTopSettled(globalObject, vm, scope, std::array { argument0, argument1, argument2 }, payload); return; } @@ -2533,6 +2549,9 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas } case InternalMicrotask::ModuleLoadLinkEvaluateSettled: { +#if USE(BUN_JSC_ADDITIONS) + ModuleLoaderAsyncContextScope asyncContextScope(vm, globalObject, uncheckedDowncast(argument2)->asyncContext()); +#endif moduleLoadLinkEvaluateSettled(globalObject, vm, scope, std::array { argument0, argument1, argument2 }, payload); return; } @@ -2563,11 +2582,17 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas } case InternalMicrotask::DynamicImportLoadSettled: { +#if USE(BUN_JSC_ADDITIONS) + ModuleLoaderAsyncContextScope asyncContextScope(vm, globalObject, uncheckedDowncast(argument2)->asyncContext()); +#endif dynamicImportLoadSettled(globalObject, vm, scope, std::array { argument0, argument1, argument2 }, payload, /* deferred */ false); return; } case InternalMicrotask::DynamicImportDeferLoadSettled: { +#if USE(BUN_JSC_ADDITIONS) + ModuleLoaderAsyncContextScope asyncContextScope(vm, globalObject, uncheckedDowncast(argument2)->asyncContext()); +#endif dynamicImportLoadSettled(globalObject, vm, scope, std::array { argument0, argument1, argument2 }, payload, /* deferred */ true); return; } @@ -2583,6 +2608,9 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas } case InternalMicrotask::ImportModuleNamespace: { +#if USE(BUN_JSC_ADDITIONS) + ModuleLoaderAsyncContextScope asyncContextScope(vm, globalObject, argument2); +#endif importModuleNamespace(globalObject, vm, scope, std::array { argument0, argument1, argument2 }, payload); return; } diff --git a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp index a4af7f4d83024..b4709e352e8a4 100644 --- a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp +++ b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp @@ -30,6 +30,9 @@ #if ENABLE(AOT) #include "AOTModuleRecord.h" #endif +#if USE(BUN_JSC_ADDITIONS) +#include "AsyncContextSwapScope.h" +#endif #include "ProgramExecutable.h" #include "BuiltinNames.h" @@ -447,7 +450,7 @@ JSPromise* JSModuleLoader::loadModule(JSGlobalObject* globalObject, const Identi #else AbstractModuleRecord::ModuleRequest request { specifier, ScriptFetchParameters::create(type) }; #endif - auto* context = ModuleLoadingContext::create(vm, this, request, WTF::move(scriptFetcher), flags, referrerAsyncOrder); + auto* context = ModuleLoadingContext::create(globalObject, this, request, WTF::move(scriptFetcher), flags, referrerAsyncOrder); JSPromise* intermediatePromise = JSPromise::create(vm, globalObject->promiseStructure()); intermediatePromise->markAsHandled(); @@ -539,7 +542,12 @@ JSPromise* JSModuleLoader::requestImportModuleForKey(JSGlobalObject* globalObjec JSPromise* resultPromise = JSPromise::create(vm, globalObject->promiseStructure()); resultPromise->markAsHandled(); +#if USE(BUN_JSC_ADDITIONS) + // The job has no other use for its context: it is the async context of the import(), under which the job asks the namespace for then(). + promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::ImportModuleNamespace, resultPromise, AsyncContextSwapScope::current(vm, globalObject)); +#else promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::ImportModuleNamespace, resultPromise, jsUndefined()); +#endif return resultPromise; } @@ -835,7 +843,13 @@ JSPromise* JSModuleLoader::hostLoadImportedModule(JSGlobalObject* globalObject, if (!mapEntry) { // 8. Let url be the result of resolving a module specifier given referencingScript and moduleRequest.[[Specifier]], catching any exceptions. If they throw an exception, let resolutionError be the thrown exception. - resolved = resolve(globalObject, specifier, referrerKey, scriptFetcher, useImportMap); + { +#if USE(BUN_JSC_ADDITIONS) + // What a hook of the host leaves in the async context ends with the hook, so that what this request captures below is the context of the load. + ModuleLoaderAsyncContextScope asyncContextOfLoad(vm, globalObject, AsyncContextSwapScope::current(vm, globalObject)); +#endif + resolved = resolve(globalObject, specifier, referrerKey, scriptFetcher, useImportMap); + } // 9. If the previous step threw an exception, then: if (Exception* resolutionError = scope.exception()) { // A TerminationException is not a resolution failure: it can be neither cleared @@ -954,7 +968,7 @@ JSPromise* JSModuleLoader::hostLoadImportedModule(JSGlobalObject* globalObject, finishLoadingImportedModule(globalObject, referrer, moduleRequest, payload, mapEntry->record(), scriptFetcher); RETURN_IF_EXCEPTION(scope, nullptr); } else { - auto* context = ModuleLoadingContext::create(vm, this, ModuleLoadingContext::Step::Cached, referrer, moduleRequest, payload, mapEntry, scriptFetcher); + auto* context = ModuleLoadingContext::create(globalObject, this, ModuleLoadingContext::Step::Cached, referrer, moduleRequest, payload, mapEntry, scriptFetcher); JSPromise* resultPromise = deferredPromise ? deferredPromise : JSPromise::create(vm, globalObject->promiseStructure()); resultPromise->markAsHandled(); promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::ModuleLoadStep, resultPromise, context); @@ -966,7 +980,7 @@ JSPromise* JSModuleLoader::hostLoadImportedModule(JSGlobalObject* globalObject, } else { #if ENABLE(AOT) if (auto file = vm.hasCompiledCode() ? AOT::ModuleRecord::fileToWaitFor(globalObject, resolved) : std::nullopt) [[unlikely]] - return AOT::ModuleRecord::deferRequest(globalObject, *file, ModuleLoadingContext::create(vm, this, ModuleLoadingContext::Step::Cached, referrer, moduleRequest, payload, nullptr, scriptFetcher), useImportMap); + return AOT::ModuleRecord::deferRequest(globalObject, *file, ModuleLoadingContext::create(globalObject, this, ModuleLoadingContext::Step::Cached, referrer, moduleRequest, payload, nullptr, scriptFetcher), useImportMap); #endif mapEntry = ModuleRegistryEntry::create(vm, this, resolved, type, scriptFetcher); Locker locker { cellLock() }; @@ -991,8 +1005,12 @@ JSPromise* JSModuleLoader::hostLoadImportedModule(JSGlobalObject* globalObject, } #endif // Per "fetch the descendants of a module script", the referrer is the referring module's base URL. - if (!promise) + if (!promise) { +#if USE(BUN_JSC_ADDITIONS) + ModuleLoaderAsyncContextScope asyncContextOfLoad(vm, globalObject, AsyncContextSwapScope::current(vm, globalObject)); +#endif promise = fetch(globalObject, identifierToJSValue(vm, resolved), moduleReferrer(referrerKey), moduleRequest.m_attributes, scriptFetcher); + } RETURN_IF_EXCEPTION(scope, nullptr); mapEntry->setStatus(ModuleRegistryEntry::Status::Fetching); @@ -1001,7 +1019,7 @@ JSPromise* JSModuleLoader::hostLoadImportedModule(JSGlobalObject* globalObject, JSPromise* modulePromise = mapEntry->ensureModulePromise(globalObject); RETURN_IF_EXCEPTION(scope, nullptr); - auto* context = ModuleLoadingContext::create(vm, this, ModuleLoadingContext::Step::Main, referrer, moduleRequest, payload, mapEntry, scriptFetcher); + auto* context = ModuleLoadingContext::create(globalObject, this, ModuleLoadingContext::Step::Main, referrer, moduleRequest, payload, mapEntry, scriptFetcher); JSPromise* loadPromise = deferredPromise ? deferredPromise : JSPromise::create(vm, globalObject->promiseStructure()); loadPromise->markAsHandled(); @@ -1031,7 +1049,7 @@ JSPromise* JSModuleLoader::loadModule(JSGlobalObject* globalObject, const Module JSPromise* promise = hostLoadImportedModule(globalObject, referrer, moduleRequest, payload, scriptFetcher, flags.contains(ModuleLoadFlag::UseImportMap)); RETURN_IF_EXCEPTION(scope, nullptr); - auto* context = ModuleLoadingContext::create(vm, this, moduleRequest, WTF::move(scriptFetcher), flags); + auto* context = ModuleLoadingContext::create(globalObject, this, moduleRequest, WTF::move(scriptFetcher), flags); JSPromise* resultPromise = JSPromise::create(vm, globalObject->promiseStructure()); resultPromise->markAsHandled(); @@ -1754,7 +1772,14 @@ JSPromise* JSModuleLoader::makeModule(JSGlobalObject* globalObject, const Identi MarkedArgumentBuffer args; Vector exportNames; - JSObject* lazyExportsSource = syntheticSourceProvider->generate(globalObject, moduleKey, exportNames, args); + JSObject* lazyExportsSource; + { + // As JSModuleRecord::evaluate() does for the body of an ES module. + std::optional loaderAsyncContext; + if (JSValue asyncContext = this->asyncContext()) + loaderAsyncContext.emplace(vm, globalObject, asyncContext); + lazyExportsSource = syntheticSourceProvider->generate(globalObject, moduleKey, exportNames, args); + } RETURN_IF_EXCEPTION(scope, promise->rejectWithCaughtException(vm, scope)); auto* moduleRecord = SyntheticModuleRecord::tryCreateWithExportNamesAndValues(globalObject, this, moduleKey, exportNames, args, lazyExportsSource); diff --git a/Source/JavaScriptCore/runtime/ModuleLoaderPayload.cpp b/Source/JavaScriptCore/runtime/ModuleLoaderPayload.cpp index f171c676eafa2..9bc783874c86a 100644 --- a/Source/JavaScriptCore/runtime/ModuleLoaderPayload.cpp +++ b/Source/JavaScriptCore/runtime/ModuleLoaderPayload.cpp @@ -33,16 +33,23 @@ namespace JSC { const ClassInfo ModuleLoaderPayload::s_info = { "ModuleLoaderPayload"_s, nullptr, nullptr, nullptr, CREATE_METHOD_TABLE(ModuleLoaderPayload) }; -ModuleLoaderPayload::ModuleLoaderPayload(VM& vm, Structure* structure, JSPromise* promise, bool deferred, int64_t referrerAsyncOrder) +#if USE(BUN_JSC_ADDITIONS) +// m_asyncContext is in what was padding of the cell: a payload takes the memory that it took without it. +static_assert(sizeof(ModuleLoaderPayload) <= 48); +#endif + +ModuleLoaderPayload::ModuleLoaderPayload(VM& vm, Structure* structure, JSPromise* promise, bool deferred, int64_t referrerAsyncOrder, JSValue asyncContext) : Base(vm, structure) , m_promise(promise, WriteBarrierEarlyInit) #if USE(BUN_JSC_ADDITIONS) + , m_asyncContext(asyncContext, WriteBarrierEarlyInit) , m_referrerAsyncOrder(referrerAsyncOrder) #endif , m_deferred(deferred) { #if !USE(BUN_JSC_ADDITIONS) UNUSED_PARAM(referrerAsyncOrder); + UNUSED_PARAM(asyncContext); #endif } @@ -60,13 +67,16 @@ void ModuleLoaderPayload::visitChildrenImpl(JSCell* cell, Visitor& visitor) Base::visitChildren(thisObject, visitor); visitor.append(thisObject->m_promise); visitor.append(thisObject->m_fulfillment); +#if USE(BUN_JSC_ADDITIONS) + visitor.append(thisObject->m_asyncContext); +#endif } DEFINE_VISIT_CHILDREN(ModuleLoaderPayload); -ModuleLoaderPayload* ModuleLoaderPayload::create(VM& vm, JSPromise* promise, bool deferred, int64_t referrerAsyncOrder) +ModuleLoaderPayload* ModuleLoaderPayload::create(VM& vm, JSPromise* promise, bool deferred, int64_t referrerAsyncOrder, JSValue asyncContext) { - ModuleLoaderPayload* instance = new (NotNull, allocateCell(vm)) ModuleLoaderPayload(vm, vm.moduleLoaderPayloadStructure.get(), promise, deferred, referrerAsyncOrder); + ModuleLoaderPayload* instance = new (NotNull, allocateCell(vm)) ModuleLoaderPayload(vm, vm.moduleLoaderPayloadStructure.get(), promise, deferred, referrerAsyncOrder, asyncContext); instance->finishCreation(vm); return instance; } diff --git a/Source/JavaScriptCore/runtime/ModuleLoaderPayload.h b/Source/JavaScriptCore/runtime/ModuleLoaderPayload.h index 5e91d69c2440d..f7aaf1a2983a1 100644 --- a/Source/JavaScriptCore/runtime/ModuleLoaderPayload.h +++ b/Source/JavaScriptCore/runtime/ModuleLoaderPayload.h @@ -49,7 +49,7 @@ class ModuleLoaderPayload final : public JSCell { } inline static Structure* createStructure(VM&, JSGlobalObject*, JSValue); - static ModuleLoaderPayload* create(VM&, JSPromise*, bool deferred = false, int64_t referrerAsyncOrder = -1); + static ModuleLoaderPayload* create(VM&, JSPromise*, bool deferred = false, int64_t referrerAsyncOrder = -1, JSValue asyncContext = { }); JSPromise* promise() const { return m_promise.get(); } @@ -59,6 +59,9 @@ class ModuleLoaderPayload final : public JSCell { bool deferred() const { return m_deferred; } #if USE(BUN_JSC_ADDITIONS) int64_t referrerAsyncOrder() const { return m_referrerAsyncOrder; } + // The async context of the import(), which DynamicImportLoadSettled and DynamicImportDeferLoadSettled run under + // where it is a store. + JSValue asyncContext() const { return m_asyncContext.get(); } #endif bool decrementRemaining() @@ -68,13 +71,14 @@ class ModuleLoaderPayload final : public JSCell { } private: - ModuleLoaderPayload(VM&, Structure*, JSPromise*, bool deferred, int64_t referrerAsyncOrder); + ModuleLoaderPayload(VM&, Structure*, JSPromise*, bool deferred, int64_t referrerAsyncOrder, JSValue asyncContext); void finishCreation(VM&); WriteBarrier m_promise; WriteBarrier m_fulfillment; #if USE(BUN_JSC_ADDITIONS) + WriteBarrier m_asyncContext; int64_t m_referrerAsyncOrder { -1 }; #endif uint8_t m_remainingFulfillments { 2 }; diff --git a/Source/JavaScriptCore/runtime/ModuleLoadingContext.cpp b/Source/JavaScriptCore/runtime/ModuleLoadingContext.cpp index 7415905a43c75..ae80c6305da66 100644 --- a/Source/JavaScriptCore/runtime/ModuleLoadingContext.cpp +++ b/Source/JavaScriptCore/runtime/ModuleLoadingContext.cpp @@ -26,6 +26,9 @@ #include "config.h" #include "ModuleLoadingContext.h" +#if USE(BUN_JSC_ADDITIONS) +#include "AsyncContextSwapScope.h" +#endif #include "CyclicModuleRecord.h" #include "JSCInlines.h" #include "JSModuleLoader.h" @@ -36,7 +39,12 @@ namespace JSC { const ClassInfo ModuleLoadingContext::s_info = { "ModuleLoadingContext"_s, nullptr, nullptr, nullptr, CREATE_METHOD_TABLE(ModuleLoadingContext) }; -ModuleLoadingContext::ModuleLoadingContext(VM& vm, Structure* structure, JSModuleLoader* loader, Step step, const JSModuleLoader::ModuleReferrer& referrer, AbstractModuleRecord::ModuleRequest&& moduleRequest, JSCell* payload, ModuleRegistryEntry* entry, RefPtr scriptFetcher) +#if USE(BUN_JSC_ADDITIONS) +// m_asyncContext is in what was padding of the cell: a context takes the memory that it took without it. +static_assert(sizeof(ModuleLoadingContext) <= 112); +#endif + +ModuleLoadingContext::ModuleLoadingContext(VM& vm, Structure* structure, JSModuleLoader* loader, Step step, const JSModuleLoader::ModuleReferrer& referrer, AbstractModuleRecord::ModuleRequest&& moduleRequest, JSCell* payload, ModuleRegistryEntry* entry, RefPtr scriptFetcher, JSValue asyncContext) : Base(vm, structure) , m_step(step) , m_moduleRequest(WTF::move(moduleRequest)) @@ -45,7 +53,25 @@ ModuleLoadingContext::ModuleLoadingContext(VM& vm, Structure* structure, JSModul , m_entry(entry, WriteBarrierEarlyInit) , m_referrer(referrer.toJSValue(), WriteBarrierEarlyInit) , m_loader(loader, WriteBarrierEarlyInit) +#if USE(BUN_JSC_ADDITIONS) + , m_asyncContext(asyncContext, WriteBarrierEarlyInit) +#endif +{ +#if !USE(BUN_JSC_ADDITIONS) + UNUSED_PARAM(asyncContext); +#endif +} + +// The async context that a request keeps: what is current where it is made. +static JSValue asyncContextOfRequest(VM& vm, JSGlobalObject* globalObject) { +#if USE(BUN_JSC_ADDITIONS) + return AsyncContextSwapScope::current(vm, globalObject); +#else + UNUSED_PARAM(vm); + UNUSED_PARAM(globalObject); + return { }; +#endif } void ModuleLoadingContext::destroy(JSCell* cell) @@ -54,34 +80,38 @@ void ModuleLoadingContext::destroy(JSCell* cell) thisObject->~ModuleLoadingContext(); } -ModuleLoadingContext* ModuleLoadingContext::create(VM& vm, JSModuleLoader* loader, Step step, const JSModuleLoader::ModuleReferrer& referrer, const AbstractModuleRecord::ModuleRequest& moduleRequest, JSCell* payload, ModuleRegistryEntry* entry, RefPtr scriptFetcher) +ModuleLoadingContext* ModuleLoadingContext::create(JSGlobalObject* globalObject, JSModuleLoader* loader, Step step, const JSModuleLoader::ModuleReferrer& referrer, const AbstractModuleRecord::ModuleRequest& moduleRequest, JSCell* payload, ModuleRegistryEntry* entry, RefPtr scriptFetcher) { + VM& vm = globalObject->vm(); ASSERT(isModuleLoaderHostDefinedPayload(payload)); AbstractModuleRecord::ModuleRequest requestCopy { moduleRequest }; - auto* context = new (NotNull, allocateCell(vm)) ModuleLoadingContext(vm, vm.moduleLoadingContextStructure.get(), loader, step, referrer, WTF::move(requestCopy), payload, entry, WTF::move(scriptFetcher)); + auto* context = new (NotNull, allocateCell(vm)) ModuleLoadingContext(vm, vm.moduleLoadingContextStructure.get(), loader, step, referrer, WTF::move(requestCopy), payload, entry, WTF::move(scriptFetcher), asyncContextOfRequest(vm, globalObject)); context->finishCreation(vm); return context; } -ModuleLoadingContext::ModuleLoadingContext(VM& vm, Structure* structure, JSModuleLoader* loader, AbstractModuleRecord::ModuleRequest&& moduleRequest, RefPtr scriptFetcher, OptionSet flags, int64_t referrerAsyncOrder) +ModuleLoadingContext::ModuleLoadingContext(VM& vm, Structure* structure, JSModuleLoader* loader, AbstractModuleRecord::ModuleRequest&& moduleRequest, RefPtr scriptFetcher, OptionSet flags, int64_t referrerAsyncOrder, JSValue asyncContext) : Base(vm, structure) , m_moduleRequest(WTF::move(moduleRequest)) , m_scriptFetcher(WTF::move(scriptFetcher)) , m_loader(loader, WriteBarrierEarlyInit) #if USE(BUN_JSC_ADDITIONS) + , m_asyncContext(asyncContext, WriteBarrierEarlyInit) , m_referrerAsyncOrder(referrerAsyncOrder) #endif , m_flags(flags) { #if !USE(BUN_JSC_ADDITIONS) UNUSED_PARAM(referrerAsyncOrder); + UNUSED_PARAM(asyncContext); #endif } -ModuleLoadingContext* ModuleLoadingContext::create(VM& vm, JSModuleLoader* loader, const AbstractModuleRecord::ModuleRequest& moduleRequest, RefPtr scriptFetcher, OptionSet flags, int64_t referrerAsyncOrder) +ModuleLoadingContext* ModuleLoadingContext::create(JSGlobalObject* globalObject, JSModuleLoader* loader, const AbstractModuleRecord::ModuleRequest& moduleRequest, RefPtr scriptFetcher, OptionSet flags, int64_t referrerAsyncOrder) { + VM& vm = globalObject->vm(); AbstractModuleRecord::ModuleRequest requestCopy { moduleRequest }; - auto* context = new (NotNull, allocateCell(vm)) ModuleLoadingContext(vm, vm.moduleLoadingContextStructure.get(), loader, WTF::move(requestCopy), WTF::move(scriptFetcher), flags, referrerAsyncOrder); + auto* context = new (NotNull, allocateCell(vm)) ModuleLoadingContext(vm, vm.moduleLoadingContextStructure.get(), loader, WTF::move(requestCopy), WTF::move(scriptFetcher), flags, referrerAsyncOrder, asyncContextOfRequest(vm, globalObject)); context->finishCreation(vm); return context; } @@ -107,6 +137,9 @@ void ModuleLoadingContext::visitChildrenImpl(JSCell* cell, Visitor& visitor) visitor.append(thisObject->m_entry); visitor.append(thisObject->m_referrer); visitor.append(thisObject->m_module); +#if USE(BUN_JSC_ADDITIONS) + visitor.append(thisObject->m_asyncContext); +#endif } DEFINE_VISIT_CHILDREN(ModuleLoadingContext); diff --git a/Source/JavaScriptCore/runtime/ModuleLoadingContext.h b/Source/JavaScriptCore/runtime/ModuleLoadingContext.h index 24c5a1ef94a7e..2b3323d72bd61 100644 --- a/Source/JavaScriptCore/runtime/ModuleLoadingContext.h +++ b/Source/JavaScriptCore/runtime/ModuleLoadingContext.h @@ -60,8 +60,8 @@ class ModuleLoadingContext final : public JSCell { Cached, // cached loadPromise settled -> finishLoading or evaluationError }; - static ModuleLoadingContext* create(VM&, JSModuleLoader*, Step, const JSModuleLoader::ModuleReferrer&, const AbstractModuleRecord::ModuleRequest&, JSCell* payload, ModuleRegistryEntry*, RefPtr); - static ModuleLoadingContext* create(VM&, JSModuleLoader*, const AbstractModuleRecord::ModuleRequest&, RefPtr, OptionSet, int64_t referrerAsyncOrder = -1); + static ModuleLoadingContext* create(JSGlobalObject*, JSModuleLoader*, Step, const JSModuleLoader::ModuleReferrer&, const AbstractModuleRecord::ModuleRequest&, JSCell* payload, ModuleRegistryEntry*, RefPtr); + static ModuleLoadingContext* create(JSGlobalObject*, JSModuleLoader*, const AbstractModuleRecord::ModuleRequest&, RefPtr, OptionSet, int64_t referrerAsyncOrder = -1); Step step() const { return m_step; } void setStep(Step s) { m_step = s; } @@ -74,6 +74,11 @@ class ModuleLoadingContext final : public JSCell { AbstractModuleRecord* module() const { return m_module.get(); } void module(VM& vm, AbstractModuleRecord* mod) { m_module.set(vm, this, mod); } JSModuleLoader* loader() const { return m_loader.get(); } +#if USE(BUN_JSC_ADDITIONS) + // The async context of whoever asked for the module: create() takes what is current in its realm. The jobs that this + // context goes to (ModuleLoadStep, ModuleLoadTopSettled, ModuleLoadLinkEvaluateSettled) run under it where it is a store. + JSValue asyncContext() const { return m_asyncContext.get(); } +#endif bool evaluate() const { return m_flags.contains(ModuleLoadFlag::Evaluate); } bool dynamic() const { return m_flags.contains(ModuleLoadFlag::Dynamic); } @@ -84,8 +89,8 @@ class ModuleLoadingContext final : public JSCell { #endif private: - ModuleLoadingContext(VM&, Structure*, JSModuleLoader*, Step, const JSModuleLoader::ModuleReferrer&, AbstractModuleRecord::ModuleRequest&&, JSCell* payload, ModuleRegistryEntry*, RefPtr); - ModuleLoadingContext(VM&, Structure*, JSModuleLoader*, AbstractModuleRecord::ModuleRequest&&, RefPtr, OptionSet, int64_t referrerAsyncOrder); + ModuleLoadingContext(VM&, Structure*, JSModuleLoader*, Step, const JSModuleLoader::ModuleReferrer&, AbstractModuleRecord::ModuleRequest&&, JSCell* payload, ModuleRegistryEntry*, RefPtr, JSValue asyncContext); + ModuleLoadingContext(VM&, Structure*, JSModuleLoader*, AbstractModuleRecord::ModuleRequest&&, RefPtr, OptionSet, int64_t referrerAsyncOrder, JSValue asyncContext); Step m_step { Step::Main }; AbstractModuleRecord::ModuleRequest m_moduleRequest; @@ -96,6 +101,7 @@ class ModuleLoadingContext final : public JSCell { WriteBarrier m_module; WriteBarrier m_loader; #if USE(BUN_JSC_ADDITIONS) + WriteBarrier m_asyncContext; int64_t m_referrerAsyncOrder { -1 }; #endif OptionSet m_flags; diff --git a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp index 1949159617173..71da8e214d186 100644 --- a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp +++ b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp @@ -26,6 +26,9 @@ #include "config.h" #include "ModuleRegistryEntry.h" +#if USE(BUN_JSC_ADDITIONS) +#include "AsyncContextSwapScope.h" +#endif #include "JSCInlines.h" #include "JSModuleLoader.h" #include "JSModuleRecord.h" @@ -141,7 +144,12 @@ JSPromise* ModuleRegistryEntry::ensureModulePromise(JSGlobalObject* globalObject #endif JSPromise* fetchPromise = ensureFetchPromise(globalObject); - fetchPromise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::ModuleRegistryFetchSettled, modulePromise, this); +#if USE(BUN_JSC_ADDITIONS) + // The job makes the record, which runs the body of a CommonJS module: it runs under the async context of whoever first asked for the module (ModuleLoaderAsyncContextScope). + fetchPromise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::ModuleRegistryFetchSettled, nullptr, this, AsyncContextSwapScope::current(vm, globalObject)); +#else + fetchPromise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::ModuleRegistryFetchSettled, nullptr, this); +#endif return modulePromise; } diff --git a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.h b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.h index 9b72affd5fbd3..5f1187ffd85dd 100644 --- a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.h +++ b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.h @@ -75,6 +75,8 @@ class ModuleRegistryEntry final : public JSCell { AbstractModuleRecord* record() const; JSPromise* ensureFetchPromise(JSGlobalObject*); JSPromise* ensureModulePromise(JSGlobalObject*); + // What ModuleRegistryFetchSettled settles. The job reads it here: its reaction keeps the async context where it would keep a cell. + JSPromise* modulePromise() const { return m_modulePromise.get(); } JSPromise* loadPromise() const; JSValue error(JSGlobalObject*) const; JSValue fetchError() const; diff --git a/Source/JavaScriptCore/runtime/SyntheticModuleRecord.cpp b/Source/JavaScriptCore/runtime/SyntheticModuleRecord.cpp index 1205bf2ff6e82..40ba4ca17720e 100644 --- a/Source/JavaScriptCore/runtime/SyntheticModuleRecord.cpp +++ b/Source/JavaScriptCore/runtime/SyntheticModuleRecord.cpp @@ -27,9 +27,15 @@ #include "SyntheticModuleRecord.h" #include "ArgList.h" +#if USE(BUN_JSC_ADDITIONS) +#include "AsyncContextSwapScope.h" +#endif #include "BuiltinNames.h" #include "JSCInlines.h" #include "JSModuleEnvironment.h" +#if USE(BUN_JSC_ADDITIONS) +#include "JSModuleLoader.h" +#endif #include "JSModuleNamespaceObject.h" #include "JSONObject.h" @@ -118,7 +124,14 @@ void SyntheticModuleRecord::runDeferredGenerator(JSGlobalObject* globalObject) Ref generator = *m_deferredGenerator; MarkedArgumentBuffer exportValues; Vector exportNames; - JSObject* lazyExportsSource = generator->generate(globalObject, moduleKey(), exportNames, exportValues); + JSObject* lazyExportsSource; + { + // As JSModuleRecord::evaluate() does for the body of an ES module. + std::optional loaderAsyncContext; + if (JSValue asyncContext = moduleLoader()->asyncContext()) + loaderAsyncContext.emplace(vm, globalObject, asyncContext); + lazyExportsSource = generator->generate(globalObject, moduleKey(), exportNames, exportValues); + } if (Exception* exception = scope.exception()) [[unlikely]] { failed(exception); return; From af332558e66f4aa92a959a83abc8b24770baec07 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:22:48 +0000 Subject: [PATCH 2/7] [JSC] A load keeps the async context of its caller, and a synchronous load keeps none The first commit had every request take the async context that was current where the request was made. That is the context of the load only inside a job that runs under it. Review found where it is not: - A synchronous load (require() of an ES module) makes its requests inside the call. Once a CommonJS file of the graph had entered a store, the jobs behind it put the store of the caller back: the modules behind the file did not see what it entered. They do in Node.js, and did before the first commit. - A synchronous load that takes the fetch of a module over from an import() ran the body of a CommonJS module under the store of that import(), inside the require() of another caller. - aot: a request that waited for a CommonJS file was made again under the context of whoever came to resume it. Now the async context belongs to the load. It is taken once, where the load starts (import(), or the host's entry points), and kept in the cells of the load: ModuleLoadingContext, ModuleLoaderPayload and, new, ModuleGraphLoadingState. Every request that the load makes takes it from its payload (JSModuleLoader::asyncContextOfLoad()), whatever is current there. The hooks of the host run under it. A synchronous load (ModuleLoadFlag::Synchronous) keeps none: all of it runs inside the call, in the context of its caller, as it did. The registry entry holds the context of the load that has the record made, until ModuleRegistryFetchSettled takes it. A synchronous load that takes the fetch over clears it. The reaction carries the module promise again, so no job changes what it carries. aot: CompiledModuleImportSettled gets the async context of the import() paired with the module, and hands it on. A CommonJS file that the runtime runs for the loader runs under the context of the load that asked for it. ModuleLoaderAsyncContextScope is an AsyncContextSwapScope that enters only for a store. It has no state of its own, and a job with no store pays for no call. JSTests: two import() calls under different contexts share a dependency with a top-level await. The module that waits for it runs in the job of whoever began to evaluate the dependency, as in Node.js. --- .../stress/bun-async-context-propagation.js | 34 +++++++++++ .../waits-first.js | 3 + .../waits-second.js | 3 + .../bun-async-context-propagation/waits.js | 3 + .../aot/runtime/AOTModuleEvaluation.cpp | 12 ++-- .../aot/runtime/AOTModuleMicrotasks.h | 2 +- .../JavaScriptCore/aot/runtime/AOTModules.cpp | 17 ++++-- .../JavaScriptCore/aot/runtime/AOTModules.h | 5 +- .../runtime/AsyncContextSwapScope.h | 45 +++++++------- Source/JavaScriptCore/runtime/Completion.cpp | 8 +++ Source/JavaScriptCore/runtime/JSMicrotask.cpp | 29 +++++++-- .../JavaScriptCore/runtime/JSModuleLoader.cpp | 60 ++++++++++++++----- .../JavaScriptCore/runtime/JSModuleLoader.h | 12 +++- .../runtime/ModuleGraphLoadingState.cpp | 15 ++++- .../runtime/ModuleGraphLoadingState.h | 12 +++- .../runtime/ModuleLoadingContext.cpp | 30 +++------- .../runtime/ModuleLoadingContext.h | 11 ++-- .../runtime/ModuleRegistryEntry.cpp | 24 +++++--- .../runtime/ModuleRegistryEntry.h | 23 +++++-- 19 files changed, 247 insertions(+), 101 deletions(-) create mode 100644 JSTests/stress/resources/bun-async-context-propagation/waits-first.js create mode 100644 JSTests/stress/resources/bun-async-context-propagation/waits-second.js create mode 100644 JSTests/stress/resources/bun-async-context-propagation/waits.js diff --git a/JSTests/stress/bun-async-context-propagation.js b/JSTests/stress/bun-async-context-propagation.js index ce1574a60269d..b076500092756 100644 --- a/JSTests/stress/bun-async-context-propagation.js +++ b/JSTests/stress/bun-async-context-propagation.js @@ -219,3 +219,37 @@ let log = []; } shouldBe(settled.length, Object.keys(expectedSettled).length, "all imports settled"); } + +// A module that waits for a dependency with a top-level await runs when the body of the dependency ends, in the job of +// whoever began to evaluate the dependency. So a module that B imports runs under A where A's import() began the +// dependency, as in Node.js. Each import() settles under its own context. +{ + log = []; + let open; + globalThis.gate = new Promise(resolve => { open = resolve; }); + const settled = []; + const settle = name => [() => settled.push([name, get()]), error => settled.push([name, get(), error])]; + const resources = "./resources/bun-async-context-propagation/"; + inContext(A, () => { + import(resources + "waits-first.js").then(...settle("waits first")); + }); + drainMicrotasks(); + inContext(B, () => { + import(resources + "waits-second.js").then(...settle("waits second")); + }); + drainMicrotasks(); + shouldBe(log.length, 1, "the dependency waits"); + open(); + drainMicrotasks(); + shouldBe(get(), undefined, "residue after the modules that waited"); + const expected = { "waits before its await": A, "waits behind its await": A, "waits first": A, "waits second": A }; + for (const [name, ctx] of log) + shouldBe(ctx, expected[name], name); + shouldBe(log.length, Object.keys(expected).length, "all modules that waited ran"); + const expectedSettled = { "waits first": A, "waits second": B }; + for (const [name, ctx, error] of settled) { + shouldBe(error, undefined, "import() of " + name + " rejected with " + error + ":"); + shouldBe(ctx, expectedSettled[name], "import() of " + name); + } + shouldBe(settled.length, Object.keys(expectedSettled).length, "both imports settled"); +} diff --git a/JSTests/stress/resources/bun-async-context-propagation/waits-first.js b/JSTests/stress/resources/bun-async-context-propagation/waits-first.js new file mode 100644 index 0000000000000..010a1f0c1d6ec --- /dev/null +++ b/JSTests/stress/resources/bun-async-context-propagation/waits-first.js @@ -0,0 +1,3 @@ +import "./waits.js"; + +log.push(["waits first", $vm.asyncContext()]); diff --git a/JSTests/stress/resources/bun-async-context-propagation/waits-second.js b/JSTests/stress/resources/bun-async-context-propagation/waits-second.js new file mode 100644 index 0000000000000..72d52ec340e36 --- /dev/null +++ b/JSTests/stress/resources/bun-async-context-propagation/waits-second.js @@ -0,0 +1,3 @@ +import "./waits.js"; + +log.push(["waits second", $vm.asyncContext()]); diff --git a/JSTests/stress/resources/bun-async-context-propagation/waits.js b/JSTests/stress/resources/bun-async-context-propagation/waits.js new file mode 100644 index 0000000000000..79ddca2870e21 --- /dev/null +++ b/JSTests/stress/resources/bun-async-context-propagation/waits.js @@ -0,0 +1,3 @@ +log.push(["waits before its await", $vm.asyncContext()]); +await gate; +log.push(["waits behind its await", $vm.asyncContext()]); diff --git a/Source/JavaScriptCore/aot/runtime/AOTModuleEvaluation.cpp b/Source/JavaScriptCore/aot/runtime/AOTModuleEvaluation.cpp index ecff5066f366b..ea446754fbe9f 100644 --- a/Source/JavaScriptCore/aot/runtime/AOTModuleEvaluation.cpp +++ b/Source/JavaScriptCore/aot/runtime/AOTModuleEvaluation.cpp @@ -857,7 +857,7 @@ void Modules::resumeDeferredLoadsOfFinishedFiles() } // `bun run` runs an imported CommonJS file while it loads the graph of the importer. No ES module of that graph has begun by then, so a require() in the file that leads back to one evaluates it there and then. -bool Modules::evaluateImportedCommonJSFiles(ModuleID root, JobsOfImportedFiles jobs, ErrorOfImportedFile error, ModuleGraphLoadingState* load) +bool Modules::evaluateImportedCommonJSFiles(ModuleID root, JobsOfImportedFiles jobs, ErrorOfImportedFile error, ModuleGraphLoadingState* load, JSValue asyncContextOfImport) { VM& vm = *m_instance.vm; auto scope = DECLARE_THROW_SCOPE(vm); @@ -896,6 +896,8 @@ bool Modules::evaluateImportedCommonJSFiles(ModuleID root, JobsOfImportedFiles j MicrotaskQueue& queue = vm.defaultMicrotaskQueue(); FilesBeingRun filesBeingRun { root, std::nullopt, m_innermostFilesBeingRun, jobs == JobsOfImportedFiles::WaitForLoader, load }; filesBeingRun.asyncContext = AsyncContextSwapScope::current(vm, m_instance.globalObject); + // What the jobs of the loader would run under: the async context of the load, not what is current here. + JSValue asyncContextOfLoad = load ? load->asyncContext() : asyncContextOfImport; SetForScope innermostFilesBeingRun(m_innermostFilesBeingRun, &filesBeingRun); SetForScope modulesWhoseFilesRunAheadOfLoader(vm.aotState().modulesWhoseFilesRunAheadOfLoader, filesBeingRun.isAheadOfJobsOfLoader ? this : vm.aotState().modulesWhoseFilesRunAheadOfLoader); // The start of the root calls the files one after the other, and leaves where one of them has queued a job or where the root would begin. @@ -904,7 +906,7 @@ bool Modules::evaluateImportedCommonJSFiles(ModuleID root, JobsOfImportedFiles j if (storedStatus(root) == StoredModuleStatus::InRecord) { { // As for a file below. The files run in one call here, so one of them leaves the next what it entered. - ModuleLoaderAsyncContextScope asyncContextOfFiles(vm, m_instance.globalObject, filesBeingRun.asyncContext); + ModuleLoaderAsyncContextScope asyncContextOfFiles(vm, m_instance.globalObject, asyncContextOfLoad); callBodyUnderOwnMark(root, false, true); } didRunFile = true; @@ -984,10 +986,10 @@ bool Modules::evaluateImportedCommonJSFiles(ModuleID root, JobsOfImportedFiles j filesBeingRun.fileOrFace = imported; filesBeingRun.numberOfJobsInFrontOfFile = numberOfJobs; { - // The loader of `bun run` runs the file in a job of its own, under the async context of whoever asked for the file: what the file enters ends with that job where the job has a store. + // The loader of `bun run` runs the file in a job of its own, under the async context of the load that asked for the file: what the file enters ends with that job where the load has a store. std::optional asyncContextOfFile; if (jobs == JobsOfImportedFiles::WaitForLoader) - asyncContextOfFile.emplace(vm, m_instance.globalObject, filesBeingRun.asyncContext); + asyncContextOfFile.emplace(vm, m_instance.globalObject, asyncContextOfLoad); // As the start of the root would call it. if (storedStatus(imported) < StoredModuleStatus::InRecord && data(imported).evaluatesItself()) evaluateByCallingBody(imported); @@ -1122,7 +1124,7 @@ void Modules::handGraphToLoader(FilesBeingRun& files) SetForScope fileBeingHandedToLoader(m_fileBeingHandedToLoader, fileOf(*running)); // The steps were due before any require() of the file began. SetForScope noSynchronousLoad(vm.m_synchronousModuleQueueSetAside, vm.m_synchronousModuleQueue); - // And before the file entered an async context: they take what was current then, for themselves and for the requests that they make. + // And before the file entered an async context: a step of a load that keeps none runs under what was current then. AsyncContextSwapScope asyncContextBeforeFiles(vm, m_instance.globalObject, files.asyncContext); if (files.load) { ModuleRecord* record = recordIfExists(files.root); diff --git a/Source/JavaScriptCore/aot/runtime/AOTModuleMicrotasks.h b/Source/JavaScriptCore/aot/runtime/AOTModuleMicrotasks.h index abc93cbe506e1..8d98f3939e2d5 100644 --- a/Source/JavaScriptCore/aot/runtime/AOTModuleMicrotasks.h +++ b/Source/JavaScriptCore/aot/runtime/AOTModuleMicrotasks.h @@ -18,7 +18,7 @@ namespace AOT { // asyncContext: what the job was queued with, which the job that it queues next is queued with too. void runCompiledModuleImport(JSGlobalObject*, JSValue importPromise, JSValue module, JSValue referrerAsyncOrder, JSValue asyncContext); -void runCompiledModuleImportSettled(JSGlobalObject*, JSValue importPromise, JSValue resolution, JSValue module, JSPromise::Status); +void runCompiledModuleImportSettled(JSGlobalObject*, JSValue importPromise, JSValue resolution, JSValue module, JSPromise::Status, JSValue asyncContext); void runCompiledModuleImportStep(JSValue importPromise, JSValue module, JSValue error, uint8_t stepsLeft, JSValue asyncContext); // Behind the last step that the loader takes for a graph. void didSettleLoad(JSPromise* settled); diff --git a/Source/JavaScriptCore/aot/runtime/AOTModules.cpp b/Source/JavaScriptCore/aot/runtime/AOTModules.cpp index ab10a7f3e3674..403f9bf6fcfe5 100644 --- a/Source/JavaScriptCore/aot/runtime/AOTModules.cpp +++ b/Source/JavaScriptCore/aot/runtime/AOTModules.cpp @@ -763,6 +763,12 @@ static void queueStepOfImport(JSPromise* importPromise, JSValue module, uint8_t realm->queueMicrotask(realm->vm(), InternalMicrotask::CompiledModuleImportStep, stepsLeft, importPromise, module, error, asyncContext); } +// CompiledModuleImportSettled has a cell, so its reaction has no place for the async context of the import(). Where there is one, the module goes to the job paired with it: runInternalMicrotask() takes the pair apart. +static JSValue contextOfImportSettled(JSGlobalObject* realm, ModuleID module, JSValue asyncContext) +{ + return AsyncContextSwapScope::wrap(realm->vm(), realm, jsNumber(module.index()), asyncContext); +} + void runCompiledModuleImportStep(JSValue importPromise, JSValue module, JSValue error, uint8_t stepsLeft, JSValue asyncContext) { auto* promise = uncheckedDowncast(importPromise); @@ -772,15 +778,14 @@ void runCompiledModuleImportStep(JSValue importPromise, JSValue module, JSValue modulesOf(promise->realm())->finishImportOfEvaluatedModule(promise, ModuleID(static_cast(module.asInt32())), error); } -void runCompiledModuleImportSettled(JSGlobalObject*, JSValue importPromise, JSValue resolution, JSValue module, JSPromise::Status status) +void runCompiledModuleImportSettled(JSGlobalObject*, JSValue importPromise, JSValue resolution, JSValue module, JSPromise::Status status, JSValue asyncContext) { auto* promise = uncheckedDowncast(importPromise); RELEASE_ASSERT(status != JSPromise::Status::Pending); Modules* modules = modulesOf(promise->realm()); ModuleID imported(static_cast(module.asInt32())); - // This job has a cell, and with it no place for the async context of the import(): the steps behind it are queued without one. if (modules->isImportedByLoader(imported)) - queueStepOfImport(promise, module, numberOfJobsBehindEvaluation - 1, JSValue(), status == JSPromise::Status::Rejected ? resolution : JSValue()); + queueStepOfImport(promise, module, numberOfJobsBehindEvaluation - 1, asyncContext, status == JSPromise::Status::Rejected ? resolution : JSValue()); else if (status == JSPromise::Status::Rejected) promise->reject(promise->vm(), resolution); else @@ -1248,7 +1253,7 @@ void Modules::decideHowCountedImportGoesOn(CountedImport& import) import.jobsRun = jobInWhichLoaderAsksRoot - 1; if (!m_orderOfCountedJobs.isEmpty()) m_orderOfCountedJobs.appendUsingFunctor(import.jobsRun, [&](size_t) { return import.serial; }); - evaluateImportedCommonJSFiles(module, JobsOfImportedFiles::WaitForLoader, ErrorOfImportedFile::IsLeftToLoader); + evaluateImportedCommonJSFiles(module, JobsOfImportedFiles::WaitForLoader, ErrorOfImportedFile::IsLeftToLoader, nullptr, import.asyncContext.get()); RETURN_IF_EXCEPTION(scope, void()); if (import.jobsOfLoader || m_rootWhoseFilesRanAhead == module || storedStatus(module) != StoredModuleStatus::Unlinked) return goOnWithLoader(); @@ -1358,7 +1363,7 @@ void Modules::decideHowCountedImportGoesOn(CountedImport& import) queueStepOfImport(importPromise, jsNumber(module.index()), numberOfJobsBehindEvaluation, import.asyncContext.get(), evaluation->result()); return; case JSPromise::Status::Pending: - evaluation->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::CompiledModuleImportSettled, importPromise, jsNumber(module.index())); + evaluation->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::CompiledModuleImportSettled, importPromise, contextOfImportSettled(m_instance.globalObject, module, import.asyncContext.get())); return; } RELEASE_ASSERT_NOT_REACHED(); @@ -1459,7 +1464,7 @@ void Modules::continueImport(JSPromise* importPromise, ModuleID module, std::opt RETURN_IF_EXCEPTION(scope, void()); switch (evaluation->status()) { case JSPromise::Status::Pending: - evaluation->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::CompiledModuleImportSettled, importPromise, jsNumber(module.index())); + evaluation->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::CompiledModuleImportSettled, importPromise, contextOfImportSettled(m_instance.globalObject, module, asyncContext)); return; case JSPromise::Status::Fulfilled: RELEASE_AND_RETURN(scope, resolveWithNamespace(importPromise, module)); diff --git a/Source/JavaScriptCore/aot/runtime/AOTModules.h b/Source/JavaScriptCore/aot/runtime/AOTModules.h index 0042131b7ea7c..7d76e669b7ec8 100644 --- a/Source/JavaScriptCore/aot/runtime/AOTModules.h +++ b/Source/JavaScriptCore/aot/runtime/AOTModules.h @@ -169,7 +169,8 @@ class Modules { // Says whether the start that asks goes on to run the module. bool beginModuleInRecord(ModuleID); // Says whether a file ran. - bool evaluateImportedCommonJSFiles(ModuleID root, JobsOfImportedFiles = JobsOfImportedFiles::StayQueued, ErrorOfImportedFile = ErrorOfImportedFile::IsThrown, ModuleGraphLoadingState* = nullptr); + // asyncContextOfImport: of the import() that the files run for, where no load of the loader is given. A file that the loader would run in a job of its own runs under the async context of its load: ModuleLoaderAsyncContextScope. + bool evaluateImportedCommonJSFiles(ModuleID root, JobsOfImportedFiles = JobsOfImportedFiles::StayQueued, ErrorOfImportedFile = ErrorOfImportedFile::IsThrown, ModuleGraphLoadingState* = nullptr, JSValue asyncContextOfImport = { }); void queueJobsThatWaitForLoader(); void handGraphsToLoader(); void didLoadGraph(ModuleID root); @@ -467,7 +468,7 @@ class Modules { std::optional fileHandedToLoader { }; // By a CommonJS file: how many modules stand between the root and the nearest one that imports it. Made when something asks. Vector distanceOfFiles { }; - // The async context that was current before the files ran. The steps of the loader that were due by then run under it: handGraphToLoader(). + // The async context that was current before the files ran. A step of the loader that was due by then, of a load that keeps no async context, runs under it: handGraphToLoader(). JSValue asyncContext { }; }; void handGraphToLoader(FilesBeingRun&); diff --git a/Source/JavaScriptCore/runtime/AsyncContextSwapScope.h b/Source/JavaScriptCore/runtime/AsyncContextSwapScope.h index b3c1c369c13e9..a83cb4252c4bc 100644 --- a/Source/JavaScriptCore/runtime/AsyncContextSwapScope.h +++ b/Source/JavaScriptCore/runtime/AsyncContextSwapScope.h @@ -30,8 +30,6 @@ #include "InternalFieldTuple.h" #include "JSCast.h" #include "JSGlobalObject.h" -#include "VM.h" -#include #include #include @@ -141,7 +139,14 @@ class AsyncContextSwapScope { return wrap(vm, globalObject, userContext, current(vm, globalObject)); } -private: +protected: + // For a scope that decides by itself whether it enters: ModuleLoaderAsyncContextScope. + enum DoNotEnterTag { DoNotEnter }; + ALWAYS_INLINE AsyncContextSwapScope(VM& vm, DoNotEnterTag) + : m_vm(vm) + { + } + // The previous value is put back on exit even when nothing had to be // installed, so whatever the job itself leaves in the slot (enterWith()) // ends with the job. @@ -157,36 +162,34 @@ class AsyncContextSwapScope { m_asyncContextData->putInternalField(m_vm, 0, asyncContext); } +private: VM& m_vm; InternalFieldTuple* m_asyncContextData { nullptr }; JSValue m_restoreAsyncContext; }; -// For a job of the module loader, and for what the loader calls on its way. The loader keeps the async context of -// whoever asked for a module with the request (ModuleLoadingContext, ModuleLoaderPayload, ModuleRegistryEntry), or in -// the job where the job has the slot free. Where that context is a store, the job is a continuation of the import() -// that asked: it runs under the store, and what it leaves in the slot ends with it. +// For a job of the module loader, and for what the loader calls on its way. // -// Two cases differ from AsyncContextSwapScope: -// - A job whose request captured no store runs as such a job always did: under what is in the slot, and what it leaves -// there stays. An entry point is evaluated by such a job, and what it enters is where the program goes on from. -// - While a synchronous load drains its jobs, a job under the context that is current already is part of the -// require() call. What it enters is the caller's, as it is for a CommonJS module. -class ModuleLoaderAsyncContextScope { - WTF_MAKE_NONCOPYABLE(ModuleLoaderAsyncContextScope); - WTF_FORBID_HEAP_ALLOCATION; +// A load keeps the async context of whoever started it: what was current where import() was called. It is in the cells of +// the load (ModuleLoadingContext, ModuleLoaderPayload, ModuleGraphLoadingState, and ModuleRegistryEntry until the record +// is made), and every request that the load makes takes it from there: JSModuleLoader::asyncContextOfLoad(). Where that +// context is a store, a job of the load is a continuation of the import(): it runs under the store, and what it leaves +// in the slot ends with it. +// +// A load that captured no store has no scope. Its jobs run as such jobs always did: under what is in the slot, and what +// they leave there stays. That is: +// - an import() outside any store, and the load of an entry point, whose module enters what the program goes on in; +// - a synchronous load (JSModuleLoader::loadModuleSync()), which captures nothing: every step of it runs inside the call +// of require(), so what its modules enter is the caller's, as it is for a CommonJS module. +class ModuleLoaderAsyncContextScope final : public AsyncContextSwapScope { public: ALWAYS_INLINE ModuleLoaderAsyncContextScope(VM& vm, JSGlobalObject* globalObject, JSValue asyncContext) + : AsyncContextSwapScope(vm, DoNotEnter) { if (asyncContext.isEmpty() || asyncContext.isUndefined()) [[likely]] return; - if (vm.synchronousModuleQueue() && asyncContext == AsyncContextSwapScope::current(vm, globalObject)) [[unlikely]] - return; - m_scope.emplace(vm, globalObject, asyncContext); + enter(globalObject, asyncContext); } - -private: - std::optional m_scope; }; } // namespace JSC diff --git a/Source/JavaScriptCore/runtime/Completion.cpp b/Source/JavaScriptCore/runtime/Completion.cpp index a45c156f7b1f9..8a771bfd5b6ab 100644 --- a/Source/JavaScriptCore/runtime/Completion.cpp +++ b/Source/JavaScriptCore/runtime/Completion.cpp @@ -23,6 +23,9 @@ #include "config.h" #include "Completion.h" +#if USE(BUN_JSC_ADDITIONS) +#include "AsyncContextSwapScope.h" +#endif #include "BuiltinNames.h" #include "BytecodeCacheError.h" #include "CodeCache.h" @@ -250,7 +253,12 @@ JSPromise* loadAndEvaluateModule(JSGlobalObject* globalObject, SourceCode&& sour JSPromise* statePromise = JSPromise::create(vm, globalObject->promiseStructure()); RETURN_IF_EXCEPTION(scope, rejectPromise(scope, globalObject)); +#if USE(BUN_JSC_ADDITIONS) + // The load keeps the async context of its caller: JSModuleLoader::asyncContextOfLoad(). + auto graphLoadingState = ModuleGraphLoadingState::create(vm, statePromise, scriptFetcher, AsyncContextSwapScope::current(vm, globalObject)); +#else auto graphLoadingState = ModuleGraphLoadingState::create(vm, statePromise, scriptFetcher); +#endif RETURN_IF_EXCEPTION(scope, rejectPromise(scope, globalObject)); AbstractModuleRecord::ModuleRequest request { WTF::move(key), ScriptFetchParameters::create(type) }; diff --git a/Source/JavaScriptCore/runtime/JSMicrotask.cpp b/Source/JavaScriptCore/runtime/JSMicrotask.cpp index 32b104eefd377..a13aa753a5527 100644 --- a/Source/JavaScriptCore/runtime/JSMicrotask.cpp +++ b/Source/JavaScriptCore/runtime/JSMicrotask.cpp @@ -1099,11 +1099,11 @@ static void asyncModuleExecutionResume(JSGlobalObject* globalObject, VM& vm, JSM static void moduleRegistryFetchSettled(JSGlobalObject* globalObject, VM& vm, ThrowScope& scope, std::span arguments, uint8_t payload) { - // arguments[0] = unused: the reaction keeps the async context there + // arguments[0] = pre-created modulePromise // arguments[1] = resolution (JSSourceCode* for JS/WASM/JSON/text, JSValue for anything else) or rejection (error) - // arguments[2] = ModuleRegistryEntry*, which holds the pre-created modulePromise + // arguments[2] = ModuleRegistryEntry* auto* entry = uncheckedDowncast(arguments[2]); - auto* modulePromise = entry->modulePromise(); + auto* modulePromise = uncheckedDowncast(arguments[0]); auto status = static_cast(payload); #if USE(BUN_JSC_ADDITIONS) // hostLoadImportedModule may have already replayed this step inline while @@ -1216,7 +1216,11 @@ static void moduleLoadStep(JSGlobalObject* globalObject, VM& vm, ThrowScope& sco if (auto* compiled = context->payload()->inherits() ? nullptr : dynamicDowncast(module)) compiled->willBeLoadedByImport(); #endif +#if USE(BUN_JSC_ADDITIONS) + JSPromise* requestedPromise = context->loader()->loadRequestedModules(globalObject, module, context->scriptFetcher(), context->asyncContext()); +#else JSPromise* requestedPromise = context->loader()->loadRequestedModules(globalObject, module, context->scriptFetcher()); +#endif if (scope.exception()) { loadPromise->rejectWithCaughtException(vm, scope); return; @@ -1333,7 +1337,11 @@ static void moduleLoadTopSettled(JSGlobalObject* globalObject, VM& vm, ThrowScop if (!moduleData.inherits()) { #endif #endif +#if USE(BUN_JSC_ADDITIONS) + context->loader()->provideFetch(globalObject, specifier, type, moduleData, context->asyncContext()); +#else context->loader()->provideFetch(globalObject, specifier, type, moduleData); +#endif if (scope.exception()) { intermediatePromise->rejectWithCaughtException(vm, scope); return; @@ -1361,7 +1369,11 @@ static void moduleLoadTopSettled(JSGlobalObject* globalObject, VM& vm, ThrowScop #endif loadPromise = context->loader()->loadModule(globalObject, globalObject, request, combinedCell, scriptFetcher, innerLoadFlags); } else { +#if USE(BUN_JSC_ADDITIONS) + combinedCell = ModuleGraphLoadingState::create(vm, statePromise, scriptFetcher, context->asyncContext()); +#else combinedCell = ModuleGraphLoadingState::create(vm, statePromise, scriptFetcher); +#endif if (context->evaluate()) innerLoadFlags.add(ModuleLoadFlag::Evaluate); loadPromise = context->loader()->loadModule(globalObject, globalObject, request, combinedCell, scriptFetcher, innerLoadFlags); @@ -2473,8 +2485,13 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas RELEASE_AND_RETURN(scope, AOT::runCompiledModuleImport(globalObject, argument0, argument1, argument2, argument3)); } - case InternalMicrotask::CompiledModuleImportSettled: - RELEASE_AND_RETURN(scope, AOT::runCompiledModuleImportSettled(globalObject, argument0, argument1, argument2, static_cast(payload))); + case InternalMicrotask::CompiledModuleImportSettled: { + // argument2 is the module, paired with the async context of the import() where there is one. + JSValue module = argument2; + JSValue asyncContext = AsyncContextSwapScope::unwrapContextTuple(module); + ModuleLoaderAsyncContextScope asyncContextScope(vm, globalObject, asyncContext); + RELEASE_AND_RETURN(scope, AOT::runCompiledModuleImportSettled(globalObject, argument0, argument1, module, static_cast(payload), asyncContext)); + } case InternalMicrotask::CompiledModuleImportStep: { ModuleLoaderAsyncContextScope asyncContextScope(vm, globalObject, argument3); @@ -2496,7 +2513,7 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas case InternalMicrotask::ModuleRegistryFetchSettled: { #if USE(BUN_JSC_ADDITIONS) - ModuleLoaderAsyncContextScope asyncContextScope(vm, globalObject, argument3); + ModuleLoaderAsyncContextScope asyncContextScope(vm, globalObject, uncheckedDowncast(argument2)->takeAsyncContext()); #endif moduleRegistryFetchSettled(globalObject, vm, scope, std::array { argument0, argument1, argument2 }, payload); return; diff --git a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp index b4709e352e8a4..9e5b64cefdf14 100644 --- a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp +++ b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp @@ -364,11 +364,11 @@ void JSModuleLoader::provideFetch(JSGlobalObject* globalObject, const Identifier entry->provideFetch(globalObject, WTF::move(sourceCode)); // can throw } -void JSModuleLoader::provideFetch(JSGlobalObject* globalObject, const Identifier& key, ScriptFetchParameters::Type type, JSValue moduleData) +void JSModuleLoader::provideFetch(JSGlobalObject* globalObject, const Identifier& key, ScriptFetchParameters::Type type, JSValue moduleData, JSValue asyncContext) { ModuleRegistryEntry* entry = ensureRegistered(globalObject, key, type); if (entry->status() == ModuleRegistryEntry::Status::New) - entry->provideFetch(globalObject, moduleData); // can throw + entry->provideFetch(globalObject, moduleData, asyncContext); // can throw } #if USE(BUN_JSC_ADDITIONS) @@ -428,6 +428,7 @@ JSPromise* JSModuleLoader::loadModule(JSGlobalObject* globalObject, const Identi #if USE(BUN_JSC_ADDITIONS) // require(esm) of a module whose fetch an import already started. if (vm.synchronousModuleQueue() && entry->status() == ModuleRegistryEntry::Status::Fetching && promise->status() == JSPromise::Status::Pending) { + entry->clearAsyncContext(); fetchSynchronously(globalObject, promise, specifier, referrer, parameters.copyRef(), scriptFetcher.copyRef()); RETURN_IF_EXCEPTION(scope, nullptr); } @@ -450,7 +451,13 @@ JSPromise* JSModuleLoader::loadModule(JSGlobalObject* globalObject, const Identi #else AbstractModuleRecord::ModuleRequest request { specifier, ScriptFetchParameters::create(type) }; #endif - auto* context = ModuleLoadingContext::create(globalObject, this, request, WTF::move(scriptFetcher), flags, referrerAsyncOrder); +#if USE(BUN_JSC_ADDITIONS) + // The load keeps the async context of its caller. A synchronous load needs none: it ends inside the call. + JSValue asyncContext = flags.contains(ModuleLoadFlag::Synchronous) ? JSValue() : AsyncContextSwapScope::current(vm, globalObject); + auto* context = ModuleLoadingContext::create(vm, this, request, WTF::move(scriptFetcher), flags, referrerAsyncOrder, asyncContext); +#else + auto* context = ModuleLoadingContext::create(vm, this, request, WTF::move(scriptFetcher), flags, referrerAsyncOrder); +#endif JSPromise* intermediatePromise = JSPromise::create(vm, globalObject->promiseStructure()); intermediatePromise->markAsHandled(); @@ -744,6 +751,16 @@ AbstractModuleRecord* JSModuleLoader::getImportedModule(AbstractModuleRecord* re return iter->value.m_module.get(); } +#if USE(BUN_JSC_ADDITIONS) +JSValue JSModuleLoader::asyncContextOfLoad(JSCell* payload) +{ + ASSERT(isModuleLoaderHostDefinedPayload(payload)); + if (auto* state = dynamicDowncast(payload)) + return state->asyncContext(); + return uncheckedDowncast(payload)->asyncContext(); +} +#endif + JSPromise* JSModuleLoader::hostLoadImportedModule(JSGlobalObject* globalObject, const ModuleReferrer& referrer, const ModuleRequest& moduleRequest, JSCell* payload, RefPtr scriptFetcher, bool useImportMap, JSPromise* deferredPromise) { // HostLoadImportedModule(referrer, moduleRequest, loadState, payload) @@ -845,8 +862,8 @@ JSPromise* JSModuleLoader::hostLoadImportedModule(JSGlobalObject* globalObject, // 8. Let url be the result of resolving a module specifier given referencingScript and moduleRequest.[[Specifier]], catching any exceptions. If they throw an exception, let resolutionError be the thrown exception. { #if USE(BUN_JSC_ADDITIONS) - // What a hook of the host leaves in the async context ends with the hook, so that what this request captures below is the context of the load. - ModuleLoaderAsyncContextScope asyncContextOfLoad(vm, globalObject, AsyncContextSwapScope::current(vm, globalObject)); + // The hooks of the host run under the async context of the load, whoever makes the request, and what a hook leaves in the slot ends with the hook. + ModuleLoaderAsyncContextScope asyncContextOfHook(vm, globalObject, asyncContextOfLoad(payload)); #endif resolved = resolve(globalObject, specifier, referrerKey, scriptFetcher, useImportMap); } @@ -907,6 +924,7 @@ JSPromise* JSModuleLoader::hostLoadImportedModule(JSGlobalObject* globalObject, JSPromise* modulePromise = mapEntry->ensureModulePromise(globalObject); if (modulePromise->status() == JSPromise::Status::Pending) { if (fetchPromise->status() == JSPromise::Status::Pending) { + mapEntry->clearAsyncContext(); fetchSynchronously(globalObject, fetchPromise, resolved, moduleReferrer(referrerKey), RefPtr { moduleRequest.m_attributes }, scriptFetcher.copyRef()); RETURN_IF_EXCEPTION(scope, nullptr); } else if (fetchPromise->status() == JSPromise::Status::Fulfilled) { @@ -968,7 +986,7 @@ JSPromise* JSModuleLoader::hostLoadImportedModule(JSGlobalObject* globalObject, finishLoadingImportedModule(globalObject, referrer, moduleRequest, payload, mapEntry->record(), scriptFetcher); RETURN_IF_EXCEPTION(scope, nullptr); } else { - auto* context = ModuleLoadingContext::create(globalObject, this, ModuleLoadingContext::Step::Cached, referrer, moduleRequest, payload, mapEntry, scriptFetcher); + auto* context = ModuleLoadingContext::create(vm, this, ModuleLoadingContext::Step::Cached, referrer, moduleRequest, payload, mapEntry, scriptFetcher); JSPromise* resultPromise = deferredPromise ? deferredPromise : JSPromise::create(vm, globalObject->promiseStructure()); resultPromise->markAsHandled(); promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::ModuleLoadStep, resultPromise, context); @@ -980,7 +998,7 @@ JSPromise* JSModuleLoader::hostLoadImportedModule(JSGlobalObject* globalObject, } else { #if ENABLE(AOT) if (auto file = vm.hasCompiledCode() ? AOT::ModuleRecord::fileToWaitFor(globalObject, resolved) : std::nullopt) [[unlikely]] - return AOT::ModuleRecord::deferRequest(globalObject, *file, ModuleLoadingContext::create(globalObject, this, ModuleLoadingContext::Step::Cached, referrer, moduleRequest, payload, nullptr, scriptFetcher), useImportMap); + return AOT::ModuleRecord::deferRequest(globalObject, *file, ModuleLoadingContext::create(vm, this, ModuleLoadingContext::Step::Cached, referrer, moduleRequest, payload, nullptr, scriptFetcher), useImportMap); #endif mapEntry = ModuleRegistryEntry::create(vm, this, resolved, type, scriptFetcher); Locker locker { cellLock() }; @@ -1007,7 +1025,7 @@ JSPromise* JSModuleLoader::hostLoadImportedModule(JSGlobalObject* globalObject, // Per "fetch the descendants of a module script", the referrer is the referring module's base URL. if (!promise) { #if USE(BUN_JSC_ADDITIONS) - ModuleLoaderAsyncContextScope asyncContextOfLoad(vm, globalObject, AsyncContextSwapScope::current(vm, globalObject)); + ModuleLoaderAsyncContextScope asyncContextOfHook(vm, globalObject, asyncContextOfLoad(payload)); #endif promise = fetch(globalObject, identifierToJSValue(vm, resolved), moduleReferrer(referrerKey), moduleRequest.m_attributes, scriptFetcher); } @@ -1016,10 +1034,14 @@ JSPromise* JSModuleLoader::hostLoadImportedModule(JSGlobalObject* globalObject, mapEntry->setStatus(ModuleRegistryEntry::Status::Fetching); mapEntry->ensureFetchPromise(globalObject)->pipeFrom(vm, promise); } +#if USE(BUN_JSC_ADDITIONS) + JSPromise* modulePromise = mapEntry->ensureModulePromise(globalObject, asyncContextOfLoad(payload)); +#else JSPromise* modulePromise = mapEntry->ensureModulePromise(globalObject); +#endif RETURN_IF_EXCEPTION(scope, nullptr); - auto* context = ModuleLoadingContext::create(globalObject, this, ModuleLoadingContext::Step::Main, referrer, moduleRequest, payload, mapEntry, scriptFetcher); + auto* context = ModuleLoadingContext::create(vm, this, ModuleLoadingContext::Step::Main, referrer, moduleRequest, payload, mapEntry, scriptFetcher); JSPromise* loadPromise = deferredPromise ? deferredPromise : JSPromise::create(vm, globalObject->promiseStructure()); loadPromise->markAsHandled(); @@ -1049,7 +1071,11 @@ JSPromise* JSModuleLoader::loadModule(JSGlobalObject* globalObject, const Module JSPromise* promise = hostLoadImportedModule(globalObject, referrer, moduleRequest, payload, scriptFetcher, flags.contains(ModuleLoadFlag::UseImportMap)); RETURN_IF_EXCEPTION(scope, nullptr); - auto* context = ModuleLoadingContext::create(globalObject, this, moduleRequest, WTF::move(scriptFetcher), flags); +#if USE(BUN_JSC_ADDITIONS) + auto* context = ModuleLoadingContext::create(vm, this, moduleRequest, WTF::move(scriptFetcher), flags, -1, asyncContextOfLoad(payload)); +#else + auto* context = ModuleLoadingContext::create(vm, this, moduleRequest, WTF::move(scriptFetcher), flags); +#endif JSPromise* resultPromise = JSPromise::create(vm, globalObject->promiseStructure()); resultPromise->markAsHandled(); @@ -1364,7 +1390,11 @@ void JSModuleLoader::continueDynamicImport(JSGlobalObject* globalObject, ModuleL // 2. Let module be moduleCompletion.[[Value]]. auto* module = std::get(completion); // 3. Let loadPromise be module.LoadRequestedModules(). +#if USE(BUN_JSC_ADDITIONS) + JSPromise* loadPromise = loadRequestedModules(globalObject, module, WTF::move(scriptFetcher), payload->asyncContext()); +#else JSPromise* loadPromise = loadRequestedModules(globalObject, module, WTF::move(scriptFetcher)); +#endif RETURN_IF_EXCEPTION(scope, void()); // 4-8. Link and evaluate using microtask dispatch instead of closures. loadPromise->performPromiseThenWithInternalMicrotask(vm, payload->deferred() ? InternalMicrotask::DynamicImportDeferLoadSettled : InternalMicrotask::DynamicImportLoadSettled, module, payload); @@ -1372,7 +1402,7 @@ void JSModuleLoader::continueDynamicImport(JSGlobalObject* globalObject, ModuleL scope.release(); } -JSPromise* JSModuleLoader::loadRequestedModules(JSGlobalObject* globalObject, AbstractModuleRecord* module, RefPtr scriptFetcher) +JSPromise* JSModuleLoader::loadRequestedModules(JSGlobalObject* globalObject, AbstractModuleRecord* module, RefPtr scriptFetcher, JSValue asyncContext) { // LoadRequestedModules([hostDefined]) // https://tc39.es/ecma262/#sec-LoadRequestedModules @@ -1385,7 +1415,7 @@ JSPromise* JSModuleLoader::loadRequestedModules(JSGlobalObject* globalObject, Ab JSPromise* pc = JSPromise::create(vm, globalObject->promiseStructure()); // This will eventually be resolved with an AbstractModuleRecord*. pc->markAsHandled(); // 3. Let state be the GraphLoadingState Record { [[IsLoading]]: true, [[PendingModulesCount]]: 1, [[Visited]]: « », [[PromiseCapability]]: pc, [[HostDefined]]: hostDefined }. - auto state = ModuleGraphLoadingState::create(vm, pc, WTF::move(scriptFetcher)); + auto state = ModuleGraphLoadingState::create(vm, pc, WTF::move(scriptFetcher), asyncContext); RETURN_IF_EXCEPTION(scope, nullptr); // 4. Perform InnerModuleLoading(state, module). innerModuleLoading(globalObject, state, module); @@ -1661,7 +1691,7 @@ JSPromise* JSModuleLoader::loadModuleSync(JSGlobalObject* globalObject, const Id } return true; }; - JSPromise* result = loadModule(globalObject, moduleName, parameters.copyRef(), scriptFetcher.copyRef(), { ModuleLoadFlag::Evaluate }); + JSPromise* result = loadModule(globalObject, moduleName, parameters.copyRef(), scriptFetcher.copyRef(), { ModuleLoadFlag::Evaluate, ModuleLoadFlag::Synchronous }); RETURN_IF_EXCEPTION(scope, result); drainSynchronousModuleQueue(globalObject); RETURN_IF_EXCEPTION(scope, result); @@ -1669,9 +1699,9 @@ JSPromise* JSModuleLoader::loadModuleSync(JSGlobalObject* globalObject, const Id return result; // The root is loaded again, and takes its own steps from the module promise. SetForScope rootThatTakesOwnLoadSteps(m_rootThatTakesOwnLoadSteps, moduleName.impl()); - result = loadModule(globalObject, moduleName, WTF::move(parameters), WTF::move(scriptFetcher), { ModuleLoadFlag::Evaluate }); + result = loadModule(globalObject, moduleName, WTF::move(parameters), WTF::move(scriptFetcher), { ModuleLoadFlag::Evaluate, ModuleLoadFlag::Synchronous }); #else - JSPromise* result = loadModule(globalObject, moduleName, WTF::move(parameters), WTF::move(scriptFetcher), { ModuleLoadFlag::Evaluate }); + JSPromise* result = loadModule(globalObject, moduleName, WTF::move(parameters), WTF::move(scriptFetcher), { ModuleLoadFlag::Evaluate, ModuleLoadFlag::Synchronous }); #endif RETURN_IF_EXCEPTION(scope, result); diff --git a/Source/JavaScriptCore/runtime/JSModuleLoader.h b/Source/JavaScriptCore/runtime/JSModuleLoader.h index b4a7ee6fc8c27..8573c726107c6 100644 --- a/Source/JavaScriptCore/runtime/JSModuleLoader.h +++ b/Source/JavaScriptCore/runtime/JSModuleLoader.h @@ -50,6 +50,8 @@ enum class ModuleLoadFlag : uint8_t { Dynamic = 1 << 1, UseImportMap = 1 << 2, Deferred = 1 << 3, + // JSModuleLoader::loadModuleSync(): every step of the load runs inside the call, in the async context of its caller. + Synchronous = 1 << 4, }; class JSModuleLoader final : public JSCell { @@ -93,6 +95,11 @@ class JSModuleLoader final : public JSCell { // (the default): whatever is current when a module is executed. JSValue asyncContext() const { return m_asyncContext.get(); } void setAsyncContext(VM& vm, JSValue asyncContext) { m_asyncContext.set(vm, this, asyncContext); } + + // The async context of the load that a payload stands for: what was current where import() was called. A load + // keeps it in its cells (ModuleLoadingContext, ModuleLoaderPayload, ModuleGraphLoadingState), and every request + // that the load makes takes it from there, not from what is current where the request is made. + static JSValue asyncContextOfLoad(JSCell* payload); #endif DECLARE_INFO; @@ -101,7 +108,8 @@ class JSModuleLoader final : public JSCell { // APIs to control the module loader. void provideFetch(JSGlobalObject*, const Identifier& key, ScriptFetchParameters::Type, SourceCode&&); - void provideFetch(JSGlobalObject*, const Identifier& key, ScriptFetchParameters::Type, JSValue); + // asyncContext: of the load that the module is provided for. See ModuleRegistryEntry::ensureModulePromise(). + void provideFetch(JSGlobalObject*, const Identifier& key, ScriptFetchParameters::Type, JSValue, JSValue asyncContext = { }); JSPromise* loadModule(JSGlobalObject*, const Identifier& moduleName, RefPtr, RefPtr, OptionSet, int64_t referrerAsyncOrder = -1, const String& referrer = { }, AbstractModuleRecord* recordInPlaceOfFetch = nullptr); JSPromise* linkAndEvaluateModule(JSGlobalObject*, const Identifier& moduleKey, RefPtr, RefPtr); JSPromise* requestImportModule(JSGlobalObject*, const Identifier& moduleName, const Identifier& referrer, RefPtr, RefPtr, bool deferred = false, int64_t referrerAsyncOrder = -1); @@ -174,7 +182,7 @@ class JSModuleLoader final : public JSCell { JSPromise* loadModule(JSGlobalObject*, const ModuleReferrer&, const ModuleRequest&, JSCell* payload, RefPtr, OptionSet); void continueModuleLoading(JSGlobalObject*, ModuleGraphLoadingState*, ModuleCompletion result); void continueDynamicImport(JSGlobalObject*, ModuleLoaderPayload*, ModuleCompletion, RefPtr); - JSPromise* loadRequestedModules(JSGlobalObject*, AbstractModuleRecord*, RefPtr); + JSPromise* loadRequestedModules(JSGlobalObject*, AbstractModuleRecord*, RefPtr, JSValue asyncContext = { }); // If moduleData is a JSSourceCode, then it's a JavaScript/WebAssembly/JSON/text source code, // and JSC is responsible for turning it into a usable module. Otherwise, the host has already diff --git a/Source/JavaScriptCore/runtime/ModuleGraphLoadingState.cpp b/Source/JavaScriptCore/runtime/ModuleGraphLoadingState.cpp index f53fcdc33891a..58e2c76f0e1f0 100644 --- a/Source/JavaScriptCore/runtime/ModuleGraphLoadingState.cpp +++ b/Source/JavaScriptCore/runtime/ModuleGraphLoadingState.cpp @@ -34,11 +34,17 @@ namespace JSC { const ClassInfo ModuleGraphLoadingState::s_info = { "ModuleGraphLoadingState"_s, nullptr, nullptr, nullptr, CREATE_METHOD_TABLE(ModuleGraphLoadingState) }; -ModuleGraphLoadingState::ModuleGraphLoadingState(VM& vm, Structure* structure, JSPromise* promise, RefPtr scriptFetcher) +ModuleGraphLoadingState::ModuleGraphLoadingState(VM& vm, Structure* structure, JSPromise* promise, RefPtr scriptFetcher, JSValue asyncContext) : Base(vm, structure) , m_promise(promise, WriteBarrierEarlyInit) +#if USE(BUN_JSC_ADDITIONS) + , m_asyncContext(asyncContext, WriteBarrierEarlyInit) +#endif , m_scriptFetcher(WTF::move(scriptFetcher)) { +#if !USE(BUN_JSC_ADDITIONS) + UNUSED_PARAM(asyncContext); +#endif } void ModuleGraphLoadingState::destroy(JSCell* cell) @@ -61,15 +67,18 @@ void ModuleGraphLoadingState::visitChildrenImpl(JSCell* cell, Visitor& visitor) Base::visitChildren(thisObject, visitor); visitor.append(thisObject->m_promise); visitor.append(thisObject->m_fulfillment); +#if USE(BUN_JSC_ADDITIONS) + visitor.append(thisObject->m_asyncContext); +#endif Locker locker { thisObject->cellLock() }; visitor.append(thisObject->m_visited.begin(), thisObject->m_visited.end()); } DEFINE_VISIT_CHILDREN(ModuleGraphLoadingState); -ModuleGraphLoadingState* ModuleGraphLoadingState::create(VM& vm, JSPromise* promise, RefPtr scriptFetcher) +ModuleGraphLoadingState* ModuleGraphLoadingState::create(VM& vm, JSPromise* promise, RefPtr scriptFetcher, JSValue asyncContext) { - ModuleGraphLoadingState* instance = new (NotNull, allocateCell(vm)) ModuleGraphLoadingState(vm, vm.moduleGraphLoadingStateStructure.get(), promise, WTF::move(scriptFetcher)); + ModuleGraphLoadingState* instance = new (NotNull, allocateCell(vm)) ModuleGraphLoadingState(vm, vm.moduleGraphLoadingStateStructure.get(), promise, WTF::move(scriptFetcher), asyncContext); instance->finishCreation(vm); return instance; } diff --git a/Source/JavaScriptCore/runtime/ModuleGraphLoadingState.h b/Source/JavaScriptCore/runtime/ModuleGraphLoadingState.h index f6eb9024170b2..1b209096adaac 100644 --- a/Source/JavaScriptCore/runtime/ModuleGraphLoadingState.h +++ b/Source/JavaScriptCore/runtime/ModuleGraphLoadingState.h @@ -55,12 +55,17 @@ class ModuleGraphLoadingState final : public JSCell { } inline static Structure* createStructure(VM&, JSGlobalObject*, JSValue); - static ModuleGraphLoadingState* create(VM&, JSPromise*, RefPtr); + static ModuleGraphLoadingState* create(VM&, JSPromise*, RefPtr, JSValue asyncContext = { }); JSPromise* promise() const { return m_promise.get(); } unsigned pendingModulesCount() const { return m_pendingModulesCount; } bool isLoading() const { return m_isLoading; } ScriptFetcher* scriptFetcher() const { return m_scriptFetcher.get(); } +#if USE(BUN_JSC_ADDITIONS) + // The async context of the load that this state is for. The requests that the load makes for its modules inherit it: + // JSModuleLoader::asyncContextOfLoad(). + JSValue asyncContext() const { return m_asyncContext.get(); } +#endif void setPendingModulesCount(unsigned count) { m_pendingModulesCount = count; } void setIsLoading(bool loading) { m_isLoading = loading; } @@ -103,7 +108,7 @@ class ModuleGraphLoadingState final : public JSCell { private: - ModuleGraphLoadingState(VM&, Structure*, JSPromise*, RefPtr); + ModuleGraphLoadingState(VM&, Structure*, JSPromise*, RefPtr, JSValue asyncContext); void finishCreation(VM&); @@ -118,6 +123,9 @@ class ModuleGraphLoadingState final : public JSCell { Vector m_innerLoadQueue; // Slot used only at top-level loadModule to AND-join loadPromise and statePromise into combinedPromise. WriteBarrier m_fulfillment; +#if USE(BUN_JSC_ADDITIONS) + WriteBarrier m_asyncContext; +#endif // [[PendingModulesCount]] unsigned m_pendingModulesCount { 1 }; uint8_t m_remainingFulfillments { 2 }; diff --git a/Source/JavaScriptCore/runtime/ModuleLoadingContext.cpp b/Source/JavaScriptCore/runtime/ModuleLoadingContext.cpp index ae80c6305da66..8ba8ac30b63af 100644 --- a/Source/JavaScriptCore/runtime/ModuleLoadingContext.cpp +++ b/Source/JavaScriptCore/runtime/ModuleLoadingContext.cpp @@ -26,9 +26,6 @@ #include "config.h" #include "ModuleLoadingContext.h" -#if USE(BUN_JSC_ADDITIONS) -#include "AsyncContextSwapScope.h" -#endif #include "CyclicModuleRecord.h" #include "JSCInlines.h" #include "JSModuleLoader.h" @@ -62,30 +59,22 @@ ModuleLoadingContext::ModuleLoadingContext(VM& vm, Structure* structure, JSModul #endif } -// The async context that a request keeps: what is current where it is made. -static JSValue asyncContextOfRequest(VM& vm, JSGlobalObject* globalObject) -{ -#if USE(BUN_JSC_ADDITIONS) - return AsyncContextSwapScope::current(vm, globalObject); -#else - UNUSED_PARAM(vm); - UNUSED_PARAM(globalObject); - return { }; -#endif -} - void ModuleLoadingContext::destroy(JSCell* cell) { SUPPRESS_MEMORY_UNSAFE_CAST auto* thisObject = static_cast(cell); thisObject->~ModuleLoadingContext(); } -ModuleLoadingContext* ModuleLoadingContext::create(JSGlobalObject* globalObject, JSModuleLoader* loader, Step step, const JSModuleLoader::ModuleReferrer& referrer, const AbstractModuleRecord::ModuleRequest& moduleRequest, JSCell* payload, ModuleRegistryEntry* entry, RefPtr scriptFetcher) +ModuleLoadingContext* ModuleLoadingContext::create(VM& vm, JSModuleLoader* loader, Step step, const JSModuleLoader::ModuleReferrer& referrer, const AbstractModuleRecord::ModuleRequest& moduleRequest, JSCell* payload, ModuleRegistryEntry* entry, RefPtr scriptFetcher) { - VM& vm = globalObject->vm(); ASSERT(isModuleLoaderHostDefinedPayload(payload)); AbstractModuleRecord::ModuleRequest requestCopy { moduleRequest }; - auto* context = new (NotNull, allocateCell(vm)) ModuleLoadingContext(vm, vm.moduleLoadingContextStructure.get(), loader, step, referrer, WTF::move(requestCopy), payload, entry, WTF::move(scriptFetcher), asyncContextOfRequest(vm, globalObject)); +#if USE(BUN_JSC_ADDITIONS) + JSValue asyncContext = JSModuleLoader::asyncContextOfLoad(payload); +#else + JSValue asyncContext; +#endif + auto* context = new (NotNull, allocateCell(vm)) ModuleLoadingContext(vm, vm.moduleLoadingContextStructure.get(), loader, step, referrer, WTF::move(requestCopy), payload, entry, WTF::move(scriptFetcher), asyncContext); context->finishCreation(vm); return context; } @@ -107,11 +96,10 @@ ModuleLoadingContext::ModuleLoadingContext(VM& vm, Structure* structure, JSModul #endif } -ModuleLoadingContext* ModuleLoadingContext::create(JSGlobalObject* globalObject, JSModuleLoader* loader, const AbstractModuleRecord::ModuleRequest& moduleRequest, RefPtr scriptFetcher, OptionSet flags, int64_t referrerAsyncOrder) +ModuleLoadingContext* ModuleLoadingContext::create(VM& vm, JSModuleLoader* loader, const AbstractModuleRecord::ModuleRequest& moduleRequest, RefPtr scriptFetcher, OptionSet flags, int64_t referrerAsyncOrder, JSValue asyncContext) { - VM& vm = globalObject->vm(); AbstractModuleRecord::ModuleRequest requestCopy { moduleRequest }; - auto* context = new (NotNull, allocateCell(vm)) ModuleLoadingContext(vm, vm.moduleLoadingContextStructure.get(), loader, WTF::move(requestCopy), WTF::move(scriptFetcher), flags, referrerAsyncOrder, asyncContextOfRequest(vm, globalObject)); + auto* context = new (NotNull, allocateCell(vm)) ModuleLoadingContext(vm, vm.moduleLoadingContextStructure.get(), loader, WTF::move(requestCopy), WTF::move(scriptFetcher), flags, referrerAsyncOrder, asyncContext); context->finishCreation(vm); return context; } diff --git a/Source/JavaScriptCore/runtime/ModuleLoadingContext.h b/Source/JavaScriptCore/runtime/ModuleLoadingContext.h index 2b3323d72bd61..50def0b07379a 100644 --- a/Source/JavaScriptCore/runtime/ModuleLoadingContext.h +++ b/Source/JavaScriptCore/runtime/ModuleLoadingContext.h @@ -60,8 +60,10 @@ class ModuleLoadingContext final : public JSCell { Cached, // cached loadPromise settled -> finishLoading or evaluationError }; - static ModuleLoadingContext* create(JSGlobalObject*, JSModuleLoader*, Step, const JSModuleLoader::ModuleReferrer&, const AbstractModuleRecord::ModuleRequest&, JSCell* payload, ModuleRegistryEntry*, RefPtr); - static ModuleLoadingContext* create(JSGlobalObject*, JSModuleLoader*, const AbstractModuleRecord::ModuleRequest&, RefPtr, OptionSet, int64_t referrerAsyncOrder = -1); + // For a request that a load makes for one of its modules. It takes the async context of that load, from the payload. + static ModuleLoadingContext* create(VM&, JSModuleLoader*, Step, const JSModuleLoader::ModuleReferrer&, const AbstractModuleRecord::ModuleRequest&, JSCell* payload, ModuleRegistryEntry*, RefPtr); + // For a load. asyncContext: of whoever starts the load, or of the load that this one is a step of. + static ModuleLoadingContext* create(VM&, JSModuleLoader*, const AbstractModuleRecord::ModuleRequest&, RefPtr, OptionSet, int64_t referrerAsyncOrder = -1, JSValue asyncContext = { }); Step step() const { return m_step; } void setStep(Step s) { m_step = s; } @@ -75,8 +77,9 @@ class ModuleLoadingContext final : public JSCell { void module(VM& vm, AbstractModuleRecord* mod) { m_module.set(vm, this, mod); } JSModuleLoader* loader() const { return m_loader.get(); } #if USE(BUN_JSC_ADDITIONS) - // The async context of whoever asked for the module: create() takes what is current in its realm. The jobs that this - // context goes to (ModuleLoadStep, ModuleLoadTopSettled, ModuleLoadLinkEvaluateSettled) run under it where it is a store. + // The async context of the load that this request belongs to: what was current where import() was called. The jobs + // that this context goes to (ModuleLoadStep, ModuleLoadTopSettled, ModuleLoadLinkEvaluateSettled) run under it where + // it is a store. Empty for a synchronous load, whose jobs all run inside the call of require(). JSValue asyncContext() const { return m_asyncContext.get(); } #endif diff --git a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp index 71da8e214d186..8f163b462b9ae 100644 --- a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp +++ b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp @@ -26,9 +26,6 @@ #include "config.h" #include "ModuleRegistryEntry.h" -#if USE(BUN_JSC_ADDITIONS) -#include "AsyncContextSwapScope.h" -#endif #include "JSCInlines.h" #include "JSModuleLoader.h" #include "JSModuleRecord.h" @@ -39,6 +36,11 @@ namespace JSC { const ClassInfo ModuleRegistryEntry::s_info = { "ModuleRegistryEntry"_s, nullptr, nullptr, nullptr, CREATE_METHOD_TABLE(ModuleRegistryEntry) }; +#if USE(BUN_JSC_ADDITIONS) +// m_asyncContext does not grow the cell: an entry takes 96 bytes with it and without it. +static_assert(sizeof(ModuleRegistryEntry) <= 96); +#endif + ModuleRegistryEntry::ModuleRegistryEntry(VM& vm, Structure* structure, JSModuleLoader* loader, Identifier key, ScriptFetchParameters::Type type, RefPtr scriptFetcher) : Base(vm, structure) , m_key(WTF::move(key)) @@ -72,6 +74,9 @@ void ModuleRegistryEntry::visitChildrenImpl(JSCell* cell, Visitor& visitor) visitor.append(thisObject->m_modulePromise); visitor.append(thisObject->m_loadPromise); visitor.append(thisObject->m_error); +#if USE(BUN_JSC_ADDITIONS) + visitor.append(thisObject->m_asyncContext); +#endif } DEFINE_VISIT_CHILDREN(ModuleRegistryEntry); @@ -124,7 +129,7 @@ JSPromise* ModuleRegistryEntry::ensureFetchPromise(JSGlobalObject* globalObject) return promise; } -JSPromise* ModuleRegistryEntry::ensureModulePromise(JSGlobalObject* globalObject) +JSPromise* ModuleRegistryEntry::ensureModulePromise(JSGlobalObject* globalObject, JSValue asyncContext) { if (m_modulePromise) return m_modulePromise.get(); @@ -145,11 +150,12 @@ JSPromise* ModuleRegistryEntry::ensureModulePromise(JSGlobalObject* globalObject JSPromise* fetchPromise = ensureFetchPromise(globalObject); #if USE(BUN_JSC_ADDITIONS) - // The job makes the record, which runs the body of a CommonJS module: it runs under the async context of whoever first asked for the module (ModuleLoaderAsyncContextScope). - fetchPromise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::ModuleRegistryFetchSettled, nullptr, this, AsyncContextSwapScope::current(vm, globalObject)); + if (asyncContext && !asyncContext.isUndefined()) + m_asyncContext.set(vm, this, asyncContext); #else - fetchPromise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::ModuleRegistryFetchSettled, nullptr, this); + UNUSED_PARAM(asyncContext); #endif + fetchPromise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::ModuleRegistryFetchSettled, modulePromise, this); return modulePromise; } @@ -239,14 +245,14 @@ void ModuleRegistryEntry::provideFetch(JSGlobalObject* globalObject, SourceCode& provideFetch(globalObject, JSSourceCode::create(globalObject->vm(), WTF::move(sourceCode))); } -void ModuleRegistryEntry::provideFetch(JSGlobalObject* globalObject, JSValue moduleData) +void ModuleRegistryEntry::provideFetch(JSGlobalObject* globalObject, JSValue moduleData, JSValue asyncContext) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); ASSERT(m_status == Status::New); - ensureModulePromise(globalObject); + ensureModulePromise(globalObject, asyncContext); RETURN_IF_EXCEPTION(scope, void()); scope.release(); diff --git a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.h b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.h index 5f1187ffd85dd..2689daa39ab01 100644 --- a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.h +++ b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.h @@ -74,9 +74,21 @@ class ModuleRegistryEntry final : public JSCell { ScriptFetchParameters::Type moduleType() const; AbstractModuleRecord* record() const; JSPromise* ensureFetchPromise(JSGlobalObject*); - JSPromise* ensureModulePromise(JSGlobalObject*); - // What ModuleRegistryFetchSettled settles. The job reads it here: its reaction keeps the async context where it would keep a cell. - JSPromise* modulePromise() const { return m_modulePromise.get(); } + // asyncContext: of the load that asks. The first load that asks has the record made: ModuleRegistryFetchSettled makes + // it, which runs the body of a CommonJS module, under that context where it is a store. + JSPromise* ensureModulePromise(JSGlobalObject*, JSValue asyncContext = { }); +#if USE(BUN_JSC_ADDITIONS) + // For the job that makes the record. The entry does not keep the context behind it. + JSValue takeAsyncContext() + { + JSValue asyncContext = m_asyncContext.get(); + m_asyncContext.clear(); + return asyncContext; + } + // For a synchronous load that takes the fetch over: the record is made inside the call of require(), in the + // context of its caller. + void clearAsyncContext() { m_asyncContext.clear(); } +#endif JSPromise* loadPromise() const; JSValue error(JSGlobalObject*) const; JSValue fetchError() const; @@ -90,7 +102,7 @@ class ModuleRegistryEntry final : public JSCell { void setStatus(Status); void provideFetch(JSGlobalObject*, SourceCode&&); - void provideFetch(JSGlobalObject*, JSValue); + void provideFetch(JSGlobalObject*, JSValue, JSValue asyncContext = { }); void fetchComplete(JSGlobalObject*, AbstractModuleRecord*); #if USE(BUN_JSC_ADDITIONS) // For an embedder that already built the module record: leaves the entry Fetched without allocating the @@ -123,6 +135,9 @@ class ModuleRegistryEntry final : public JSCell { // The fetch / instantiation / evaluation errors are mutually exclusive: m_status // disambiguates which kind m_error holds (FetchFailed / InstantiationFailed / EvaluationFailed). WriteBarrier m_error; +#if USE(BUN_JSC_ADDITIONS) + WriteBarrier m_asyncContext; +#endif Status m_status { Status::New }; #if USE(BUN_JSC_ADDITIONS) bool m_isLoaded { false }; From 335748b3aa207e974ebf0af04cf09b9ed5f3ab90 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:19:29 +0000 Subject: [PATCH 3/7] [JSC] The drain of a synchronous load enters no async context, and an await hands on the context of the load Review of the previous commit found five places where a job ran under a context that is not its own. 1. A synchronous load (require() of an ES module) drains its own queue inside the call. That queue also takes the jobs of other loads that come due meanwhile: of an import() in flight, and of an import() that a module calls during the drain. Each such job entered the context of its load. So a CommonJS file and an ES module that the required graph shares with an import() in flight ran under the store of that import(), inside the call of require(). Before this branch, and in Node.js, they run in the context of the caller. ModuleLoaderAsyncContextScope now enters nothing while vm.synchronousModuleQueue() is set. What the call of require() runs, it runs in the context of its caller, as before this branch. ModuleRegistryEntry::clearAsyncContext() has no use left: the job that makes the record takes the context of the entry and, in the drain, does not enter it. 2. CyclicModuleRecord::executeAsync() gave the job that runs the modules behind a top-level await what the slot held. In a job that entered no context (a load that keeps none, a drain), the slot holds what whoever runs the job runs under. An entry point whose await a require() under a store settled ran under that store afterwards, with its timers. That job continues the job that began to evaluate the awaiting module. The jobs that evaluate now have a ModuleEvaluationAsyncContextScope, and executeAsync() asks JSModuleLoader::asyncContextOfModulesThatWait(): - where the job entered the context of its load: what is current where the awaiting module begins, as a load would keep it. That is the context of the load, or what a module before it entered; - where the job did not enter it (a drain): the context of the load. So a module of an import() whose graph a drain began to evaluate, and that runs after the call, runs under the store of its import(); - where the load keeps no context, and where no job of the loader evaluates: none, as before this branch. 3. A loader with an async context of its own (Bun.ModuleGraph) had the body of a CommonJS module run under that context in makeModule(). The host already sees to that, and leaves the context of a caller inside the graph alone. With the scope in makeModule(), a CommonJS file that a required ES module imports inside a graph lost the context of the caller. The scopes in makeModule() and runDeferredGenerator() are gone, and an entry of such a loader keeps no context for the job that makes the record. 4. A load kept all of the async context of its caller. Bun.ModuleGraph has the context of a graph in the same slot as the stores of AsyncLocalStorage. So a module of the host's loader that code of a graph asked for first ran in the context of that graph, and so did the hooks of the host for the dependencies of a graph's module. What runs in the context of a graph ends with the graph: such a module with a top-level await never went on once the graph was disposed, for anyone. JSModuleLoader::asyncContextForLoad() is what a load keeps. The host can set a filter for what no load may keep (setAsyncContextOfLoadFilter()). With Bun's filter a load that begins in the context of a graph keeps nothing, so its jobs run as before this branch. aot takes the context of an import() from the same place. 5. aot: where an import() under a store loads an entry point and no other job waits, the start of the entry point ran its CommonJS files in one call, under one scope. What one file entered reached the next, which it does not under `bun run`. The files now run one by one where each has a scope of its own. What stays as before this branch: a module that the call of require() itself evaluates runs in the context of the caller, also where it belongs to an import() in flight or to an import() that a required module calls. Node.js evaluates those later, in jobs of their own. JSTests: a load under no context that a drain under A begins, where the module behind the await runs under no context. And a load under A whose first module sets a context, where the module behind the await of the next one runs under that context, as in Node.js. --- .../stress/bun-async-context-propagation.js | 42 ++++++++++++++ .../awaits-gate.js | 1 + .../later-importer.js | 3 + .../bun-async-context-propagation/later.js | 3 + .../sets-then-awaits.js | 4 ++ .../bun-async-context-propagation/sets.js | 1 + .../aot/runtime/AOTModuleEvaluation.cpp | 9 +-- .../JavaScriptCore/aot/runtime/AOTModules.cpp | 20 ++++--- .../JavaScriptCore/aot/runtime/AOTModules.h | 2 + .../runtime/AsyncContextSwapScope.h | 56 +++++++++++++++++-- Source/JavaScriptCore/runtime/Completion.cpp | 2 +- .../runtime/CyclicModuleRecord.cpp | 9 ++- Source/JavaScriptCore/runtime/JSMicrotask.cpp | 14 ++--- .../JavaScriptCore/runtime/JSModuleLoader.cpp | 34 ++++++----- .../JavaScriptCore/runtime/JSModuleLoader.h | 15 ++++- .../runtime/ModuleLoadingContext.h | 3 +- .../runtime/ModuleRegistryEntry.cpp | 3 +- .../runtime/ModuleRegistryEntry.h | 7 +-- .../runtime/SyntheticModuleRecord.cpp | 15 +---- Source/JavaScriptCore/runtime/VM.cpp | 2 + Source/JavaScriptCore/runtime/VM.h | 6 ++ 21 files changed, 186 insertions(+), 65 deletions(-) create mode 100644 JSTests/stress/resources/bun-async-context-propagation/awaits-gate.js create mode 100644 JSTests/stress/resources/bun-async-context-propagation/later-importer.js create mode 100644 JSTests/stress/resources/bun-async-context-propagation/later.js create mode 100644 JSTests/stress/resources/bun-async-context-propagation/sets-then-awaits.js create mode 100644 JSTests/stress/resources/bun-async-context-propagation/sets.js diff --git a/JSTests/stress/bun-async-context-propagation.js b/JSTests/stress/bun-async-context-propagation.js index b076500092756..ec80224cf6a3a 100644 --- a/JSTests/stress/bun-async-context-propagation.js +++ b/JSTests/stress/bun-async-context-propagation.js @@ -253,3 +253,45 @@ let log = []; } shouldBe(settled.length, Object.keys(expectedSettled).length, "both imports settled"); } + +// The job that runs the modules behind a top-level await gets the context of the load, not what the slot holds where +// the load begins to evaluate. Here an import() under no context is loaded by a drain that something calls under A: +// the awaiting module runs under A as far as its await, and behind it. The module that waits for it runs later, under +// no context. +{ + log = []; + let open; + globalThis.laterGate = new Promise(resolve => { open = resolve; }); + let settledUnder = "not settled"; + const resources = "./resources/bun-async-context-propagation/"; + import(resources + "later-importer.js").then(() => { settledUnder = get(); }); + inContext(A, () => drainMicrotasks()); + shouldBe(log.length, 1, "the dependency waits"); + open(); + drainMicrotasks(); + shouldBe(get(), undefined, "residue after the module that waited"); + const expected = { "later before its await": A, "later behind its await": A, "later importer": undefined }; + for (const [name, ctx] of log) + shouldBe(ctx, expected[name], name); + shouldBe(log.length, Object.keys(expected).length, "the module that waited ran"); + shouldBe(settledUnder, undefined, "import() of later importer"); +} + +// Where the job of a load entered the context of the load, the job behind an await gets what is current where the +// awaiting module begins, as any continuation does. Here that is what a module before it has put in the slot. +{ + log = []; + let open; + globalThis.enteredGate = new Promise(resolve => { open = resolve; }); + const resources = "./resources/bun-async-context-propagation/"; + inContext(A, () => { + import(resources + "sets-then-awaits.js"); + }); + drainMicrotasks(); + shouldBe(log.length, 0, "the module waits"); + open(); + drainMicrotasks(); + shouldBe(get(), undefined, "residue after the module that waited"); + shouldBe(log.length, 1, "the module that waited ran"); + shouldBe(log[0][1], globalThis.entered, "behind sets and awaits"); +} diff --git a/JSTests/stress/resources/bun-async-context-propagation/awaits-gate.js b/JSTests/stress/resources/bun-async-context-propagation/awaits-gate.js new file mode 100644 index 0000000000000..a9d03a703c92d --- /dev/null +++ b/JSTests/stress/resources/bun-async-context-propagation/awaits-gate.js @@ -0,0 +1 @@ +await enteredGate; diff --git a/JSTests/stress/resources/bun-async-context-propagation/later-importer.js b/JSTests/stress/resources/bun-async-context-propagation/later-importer.js new file mode 100644 index 0000000000000..485854e3890de --- /dev/null +++ b/JSTests/stress/resources/bun-async-context-propagation/later-importer.js @@ -0,0 +1,3 @@ +import "./later.js"; + +log.push(["later importer", $vm.asyncContext()]); diff --git a/JSTests/stress/resources/bun-async-context-propagation/later.js b/JSTests/stress/resources/bun-async-context-propagation/later.js new file mode 100644 index 0000000000000..c52c8c468676d --- /dev/null +++ b/JSTests/stress/resources/bun-async-context-propagation/later.js @@ -0,0 +1,3 @@ +log.push(["later before its await", $vm.asyncContext()]); +await laterGate; +log.push(["later behind its await", $vm.asyncContext()]); diff --git a/JSTests/stress/resources/bun-async-context-propagation/sets-then-awaits.js b/JSTests/stress/resources/bun-async-context-propagation/sets-then-awaits.js new file mode 100644 index 0000000000000..3c47ccd7dbf37 --- /dev/null +++ b/JSTests/stress/resources/bun-async-context-propagation/sets-then-awaits.js @@ -0,0 +1,4 @@ +import "./sets.js"; +import "./awaits-gate.js"; + +log.push(["behind sets and awaits", $vm.asyncContext()]); diff --git a/JSTests/stress/resources/bun-async-context-propagation/sets.js b/JSTests/stress/resources/bun-async-context-propagation/sets.js new file mode 100644 index 0000000000000..91d1da203cb44 --- /dev/null +++ b/JSTests/stress/resources/bun-async-context-propagation/sets.js @@ -0,0 +1 @@ +$vm.setAsyncContext(entered); diff --git a/Source/JavaScriptCore/aot/runtime/AOTModuleEvaluation.cpp b/Source/JavaScriptCore/aot/runtime/AOTModuleEvaluation.cpp index ea446754fbe9f..53fa915c271b7 100644 --- a/Source/JavaScriptCore/aot/runtime/AOTModuleEvaluation.cpp +++ b/Source/JavaScriptCore/aot/runtime/AOTModuleEvaluation.cpp @@ -901,14 +901,11 @@ bool Modules::evaluateImportedCommonJSFiles(ModuleID root, JobsOfImportedFiles j SetForScope innermostFilesBeingRun(m_innermostFilesBeingRun, &filesBeingRun); SetForScope modulesWhoseFilesRunAheadOfLoader(vm.aotState().modulesWhoseFilesRunAheadOfLoader, filesBeingRun.isAheadOfJobsOfLoader ? this : vm.aotState().modulesWhoseFilesRunAheadOfLoader); // The start of the root calls the files one after the other, and leaves where one of them has queued a job or where the root would begin. - if (jobs == JobsOfImportedFiles::WaitForLoader && data(root).evaluatesItself() && data(root).isEntryPoint() && queue.isEmpty() && !m_rootWhoseFilesRun) { + // Not where the files run under the async context of a load: each of them has a scope of its own then, in the loop below, as each has a job of its own under `bun run`. + if (jobs == JobsOfImportedFiles::WaitForLoader && data(root).evaluatesItself() && data(root).isEntryPoint() && queue.isEmpty() && !m_rootWhoseFilesRun && !ModuleLoaderAsyncContextScope::wouldEnter(vm, asyncContextOfLoad)) { ensureRecord(root)->retain(); if (storedStatus(root) == StoredModuleStatus::InRecord) { - { - // As for a file below. The files run in one call here, so one of them leaves the next what it entered. - ModuleLoaderAsyncContextScope asyncContextOfFiles(vm, m_instance.globalObject, asyncContextOfLoad); - callBodyUnderOwnMark(root, false, true); - } + callBodyUnderOwnMark(root, false, true); didRunFile = true; if (filesBeingRun.fileHandedToLoader) [[unlikely]] { ModuleID file = *filesBeingRun.fileHandedToLoader; diff --git a/Source/JavaScriptCore/aot/runtime/AOTModules.cpp b/Source/JavaScriptCore/aot/runtime/AOTModules.cpp index 403f9bf6fcfe5..f90fc7123a2ca 100644 --- a/Source/JavaScriptCore/aot/runtime/AOTModules.cpp +++ b/Source/JavaScriptCore/aot/runtime/AOTModules.cpp @@ -722,11 +722,15 @@ static void queueImportJob(JSGlobalObject* realm, JSPromise* importPromise, Modu realm->queueMicrotask(realm->vm(), InternalMicrotask::CompiledModuleImport, unusedPayload, importPromise, jsNumber(module.index()), referrerAsyncOrder ? jsNumber(static_cast(*referrerAsyncOrder)) : jsUndefined(), asyncContext); } +JSValue Modules::asyncContextForImport() const +{ + return loader()->asyncContextForLoad(*m_instance.vm, m_instance.globalObject); +} + Strong Modules::currentAsyncContext() const { - VM& vm = *m_instance.vm; - JSValue asyncContext = AsyncContextSwapScope::current(vm, m_instance.globalObject); - return asyncContext.isUndefined() ? Strong() : Strong(vm, asyncContext); + JSValue asyncContext = asyncContextForImport(); + return asyncContext.isUndefined() ? Strong() : Strong(*m_instance.vm, asyncContext); } // A synchronous module load runs the jobs it diverts in its own realm, so the realm of a job is the one its import promise was created in. @@ -886,7 +890,7 @@ JSPromise* Modules::startImport(ModuleID module, std::optional referrer if (isCalledByProgram && (!isBuiltinModule || isBuiltinModuleThatLoaderHas) && loaderOnlyHandsOverNamespace(module)) { JSPromise* promise = JSPromise::create(vm, realm->promiseStructure()); if (promise->isThenFastAndNonObservable()) [[likely]] { - queueStepOfImport(promise, jsNumber(module.index()), std::size(jobsOfImportOfEvaluatedModule), AsyncContextSwapScope::current(vm, realm)); + queueStepOfImport(promise, jsNumber(module.index()), std::size(jobsOfImportOfEvaluatedModule), asyncContextForImport()); return promise; } } @@ -908,7 +912,7 @@ JSPromise* Modules::startImport(ModuleID module, std::optional referrer m_orderOfCountedJobs.append(serial); m_modulesOfCountedImports.set(module.index()); } - queueImportJob(realm, promise, module, referrerAsyncOrder, AsyncContextSwapScope::current(vm, realm)); + queueImportJob(realm, promise, module, referrerAsyncOrder, asyncContextForImport()); return promise; } } @@ -945,7 +949,7 @@ JSPromise* Modules::startImport(ModuleID module, std::optional referrer } JSPromise* importPromise = JSPromise::create(*m_instance.vm, realm->promiseStructure()); - queueImportJob(realm, importPromise, module, referrerAsyncOrder, AsyncContextSwapScope::current(*m_instance.vm, realm)); + queueImportJob(realm, importPromise, module, referrerAsyncOrder, asyncContextForImport()); return importPromise; } @@ -2517,8 +2521,8 @@ void ModuleRecord::executeAsync(JSGlobalObject* globalObject) ASSERT(status() == Status::Evaluating || status() == Status::EvaluatingAsync); ASSERT(hasTLA()); - // As CyclicModuleRecord::executeAsync(), which registers the job before it runs the body: the modules that wait for this one run under the async context of whoever evaluates it, not under what the body has entered. - JSValue asyncContext = AsyncContextSwapScope::current(vm, globalObject); + // As CyclicModuleRecord::executeAsync(), which registers the job before it runs the body: the modules that wait for this one run under the async context that ModuleEvaluationAsyncContextScope names, not under what the body enters. + JSValue asyncContext = moduleLoader()->asyncContextOfModulesThatWait(vm, globalObject); JSValue result = modules().callBodyOfModuleEnteredByLoader(m_module); RETURN_IF_EXCEPTION(scope, void()); auto* promise = dynamicDowncast(result); diff --git a/Source/JavaScriptCore/aot/runtime/AOTModules.h b/Source/JavaScriptCore/aot/runtime/AOTModules.h index 7d76e669b7ec8..4b3cab14ecfff 100644 --- a/Source/JavaScriptCore/aot/runtime/AOTModules.h +++ b/Source/JavaScriptCore/aot/runtime/AOTModules.h @@ -553,6 +553,8 @@ class Modules { std::span level(size_t depth) { return found.mutableSpan().subspan(levels[depth].first, (depth + 1 == levels.size() ? found.size() : levels[depth + 1].first) - levels[depth].first); } }; bool isLeftToLoad(PlanOfLoad&, PlanOfLoad::Found&); + // What an import() that is called now keeps of the async context of its caller: JSModuleLoader::asyncContextForLoad(). + JSValue asyncContextForImport() const; // For an import() that is kept outside the queue of jobs: the async context of its call, which the jobs that take it up again are queued with. Empty where there is none. Strong currentAsyncContext() const; struct CountedImport { diff --git a/Source/JavaScriptCore/runtime/AsyncContextSwapScope.h b/Source/JavaScriptCore/runtime/AsyncContextSwapScope.h index a83cb4252c4bc..ceee96388d6cc 100644 --- a/Source/JavaScriptCore/runtime/AsyncContextSwapScope.h +++ b/Source/JavaScriptCore/runtime/AsyncContextSwapScope.h @@ -30,6 +30,7 @@ #include "InternalFieldTuple.h" #include "JSCast.h" #include "JSGlobalObject.h" +#include "VM.h" #include #include @@ -147,6 +148,8 @@ class AsyncContextSwapScope { { } + bool didEnter() const { return m_asyncContextData; } + // The previous value is put back on exit even when nothing had to be // installed, so whatever the job itself leaves in the slot (enterWith()) // ends with the job. @@ -179,17 +182,62 @@ class AsyncContextSwapScope { // A load that captured no store has no scope. Its jobs run as such jobs always did: under what is in the slot, and what // they leave there stays. That is: // - an import() outside any store, and the load of an entry point, whose module enters what the program goes on in; -// - a synchronous load (JSModuleLoader::loadModuleSync()), which captures nothing: every step of it runs inside the call -// of require(), so what its modules enter is the caller's, as it is for a CommonJS module. -class ModuleLoaderAsyncContextScope final : public AsyncContextSwapScope { +// - a synchronous load (JSModuleLoader::loadModuleSync()), which captures nothing: what it runs inside the call of +// require() runs in the context of the caller, and what its modules enter is the caller's, as for a CommonJS module. +// +// While a synchronous load drains its queue, no job of the loader enters the context of its load. The drain also takes +// the jobs of other loads that come due meanwhile: of an import() in flight, and of an import() that a module calls +// during the drain. It runs them inside the call of require(), so they run in the context of its caller, as the steps +// of the synchronous load do. (The job that resumes a module behind its own await is no such job: it has the context +// of that await, as any await has.) +class ModuleLoaderAsyncContextScope : public AsyncContextSwapScope { public: ALWAYS_INLINE ModuleLoaderAsyncContextScope(VM& vm, JSGlobalObject* globalObject, JSValue asyncContext) : AsyncContextSwapScope(vm, DoNotEnter) { - if (asyncContext.isEmpty() || asyncContext.isUndefined()) [[likely]] + if (!wouldEnter(vm, asyncContext)) [[likely]] return; enter(globalObject, asyncContext); } + + static ALWAYS_INLINE bool wouldEnter(VM& vm, JSValue asyncContext) + { + if (asyncContext.isEmpty() || asyncContext.isUndefined()) [[likely]] + return false; + return !vm.synchronousModuleQueue(); + } +}; + +// For a job of the module loader that evaluates modules. A module with a top-level await leaves a job behind that runs +// the modules that wait for it (CyclicModuleRecord::executeAsync()). That job continues the job that began to evaluate +// the module: JSModuleLoader::asyncContextOfModulesThatWait() says what it gets. +// - Where the job entered the context of its load, the slot is the job's own: the job behind the await gets what is +// current where the awaiting module begins, as any continuation does. +// - Where the job did not enter it (the drain of a synchronous load), it gets the context of the load. +// - Where the load keeps no context, it gets none, as before there was this scope. What the slot holds in a job that +// entered nothing is not the job's to hand on: it can be the context of whoever runs the job. +class ModuleEvaluationAsyncContextScope final : public ModuleLoaderAsyncContextScope { +public: + ALWAYS_INLINE ModuleEvaluationAsyncContextScope(VM& vm, JSGlobalObject* globalObject, JSValue asyncContext) + : ModuleLoaderAsyncContextScope(vm, globalObject, asyncContext) + , m_vm(vm) + , m_asyncContextOfLoad(asyncContext) + , m_outer(std::exchange(vm.m_moduleEvaluationAsyncContextScope, this)) + { + } + + ALWAYS_INLINE ~ModuleEvaluationAsyncContextScope() + { + m_vm.m_moduleEvaluationAsyncContextScope = m_outer; + } + + using ModuleLoaderAsyncContextScope::didEnter; + JSValue asyncContextOfLoad() const { return m_asyncContextOfLoad; } + +private: + VM& m_vm; + JSValue m_asyncContextOfLoad; + ModuleEvaluationAsyncContextScope* m_outer; }; } // namespace JSC diff --git a/Source/JavaScriptCore/runtime/Completion.cpp b/Source/JavaScriptCore/runtime/Completion.cpp index 8a771bfd5b6ab..46d0034852f9d 100644 --- a/Source/JavaScriptCore/runtime/Completion.cpp +++ b/Source/JavaScriptCore/runtime/Completion.cpp @@ -255,7 +255,7 @@ JSPromise* loadAndEvaluateModule(JSGlobalObject* globalObject, SourceCode&& sour RETURN_IF_EXCEPTION(scope, rejectPromise(scope, globalObject)); #if USE(BUN_JSC_ADDITIONS) // The load keeps the async context of its caller: JSModuleLoader::asyncContextOfLoad(). - auto graphLoadingState = ModuleGraphLoadingState::create(vm, statePromise, scriptFetcher, AsyncContextSwapScope::current(vm, globalObject)); + auto graphLoadingState = ModuleGraphLoadingState::create(vm, statePromise, scriptFetcher, globalObject->moduleLoader()->asyncContextForLoad(vm, globalObject)); #else auto graphLoadingState = ModuleGraphLoadingState::create(vm, statePromise, scriptFetcher); #endif diff --git a/Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp b/Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp index 02be5a44b3af3..18e18d4c8444b 100644 --- a/Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp +++ b/Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp @@ -29,9 +29,6 @@ #if ENABLE(AOT) #include "AOTModuleRecord.h" #endif -#if USE(BUN_JSC_ADDITIONS) -#include "AsyncContextSwapScope.h" -#endif #include "BuiltinNames.h" #include "ExceptionHelpers.h" #include "Interpreter.h" @@ -739,8 +736,10 @@ void CyclicModuleRecord::executeAsync(JSGlobalObject* globalObject) // Also handled in JSMicrotask.cpp. // 8. Perform PerformPromiseThen(capability.[[Promise]], onFulfilled, onRejected). #if USE(BUN_JSC_ADDITIONS) - // The modules that wait for this one run in that job, under the async context of whoever evaluates this one. - promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::AsyncModuleExecutionDone, nullptr, this, AsyncContextSwapScope::current(vm, globalObject)); + // The modules that wait for this one run in that job. It continues the job of the module loader that evaluates this + // one: ModuleEvaluationAsyncContextScope says under which async context. An evaluation that no such job began hands + // on none. + promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::AsyncModuleExecutionDone, nullptr, this, moduleLoader()->asyncContextOfModulesThatWait(vm, globalObject)); #else promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::AsyncModuleExecutionDone, nullptr, this); #endif diff --git a/Source/JavaScriptCore/runtime/JSMicrotask.cpp b/Source/JavaScriptCore/runtime/JSMicrotask.cpp index a13aa753a5527..28846ea4ea280 100644 --- a/Source/JavaScriptCore/runtime/JSMicrotask.cpp +++ b/Source/JavaScriptCore/runtime/JSMicrotask.cpp @@ -2466,7 +2466,7 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas case InternalMicrotask::AsyncModuleExecutionDone: { #if USE(BUN_JSC_ADDITIONS) - ModuleLoaderAsyncContextScope asyncContextScope(vm, globalObject, argument3); + ModuleEvaluationAsyncContextScope asyncContextScope(vm, globalObject, argument3); #endif auto* module = uncheckedDowncast(argument2); JSGlobalObject* realm = nullptr; @@ -2481,7 +2481,7 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas #if ENABLE(AOT) case InternalMicrotask::CompiledModuleImport: { - ModuleLoaderAsyncContextScope asyncContextScope(vm, globalObject, argument3); + ModuleEvaluationAsyncContextScope asyncContextScope(vm, globalObject, argument3); RELEASE_AND_RETURN(scope, AOT::runCompiledModuleImport(globalObject, argument0, argument1, argument2, argument3)); } @@ -2489,12 +2489,12 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas // argument2 is the module, paired with the async context of the import() where there is one. JSValue module = argument2; JSValue asyncContext = AsyncContextSwapScope::unwrapContextTuple(module); - ModuleLoaderAsyncContextScope asyncContextScope(vm, globalObject, asyncContext); + ModuleEvaluationAsyncContextScope asyncContextScope(vm, globalObject, asyncContext); RELEASE_AND_RETURN(scope, AOT::runCompiledModuleImportSettled(globalObject, argument0, argument1, module, static_cast(payload), asyncContext)); } case InternalMicrotask::CompiledModuleImportStep: { - ModuleLoaderAsyncContextScope asyncContextScope(vm, globalObject, argument3); + ModuleEvaluationAsyncContextScope asyncContextScope(vm, globalObject, argument3); RELEASE_AND_RETURN(scope, AOT::runCompiledModuleImportStep(argument0, argument1, argument2, payload, argument3)); } #endif @@ -2567,7 +2567,7 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas case InternalMicrotask::ModuleLoadLinkEvaluateSettled: { #if USE(BUN_JSC_ADDITIONS) - ModuleLoaderAsyncContextScope asyncContextScope(vm, globalObject, uncheckedDowncast(argument2)->asyncContext()); + ModuleEvaluationAsyncContextScope asyncContextScope(vm, globalObject, uncheckedDowncast(argument2)->asyncContext()); #endif moduleLoadLinkEvaluateSettled(globalObject, vm, scope, std::array { argument0, argument1, argument2 }, payload); return; @@ -2600,7 +2600,7 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas case InternalMicrotask::DynamicImportLoadSettled: { #if USE(BUN_JSC_ADDITIONS) - ModuleLoaderAsyncContextScope asyncContextScope(vm, globalObject, uncheckedDowncast(argument2)->asyncContext()); + ModuleEvaluationAsyncContextScope asyncContextScope(vm, globalObject, uncheckedDowncast(argument2)->asyncContext()); #endif dynamicImportLoadSettled(globalObject, vm, scope, std::array { argument0, argument1, argument2 }, payload, /* deferred */ false); return; @@ -2608,7 +2608,7 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas case InternalMicrotask::DynamicImportDeferLoadSettled: { #if USE(BUN_JSC_ADDITIONS) - ModuleLoaderAsyncContextScope asyncContextScope(vm, globalObject, uncheckedDowncast(argument2)->asyncContext()); + ModuleEvaluationAsyncContextScope asyncContextScope(vm, globalObject, uncheckedDowncast(argument2)->asyncContext()); #endif dynamicImportLoadSettled(globalObject, vm, scope, std::array { argument0, argument1, argument2 }, payload, /* deferred */ true); return; diff --git a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp index 9e5b64cefdf14..0b50ae5d4a2a2 100644 --- a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp +++ b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp @@ -428,7 +428,6 @@ JSPromise* JSModuleLoader::loadModule(JSGlobalObject* globalObject, const Identi #if USE(BUN_JSC_ADDITIONS) // require(esm) of a module whose fetch an import already started. if (vm.synchronousModuleQueue() && entry->status() == ModuleRegistryEntry::Status::Fetching && promise->status() == JSPromise::Status::Pending) { - entry->clearAsyncContext(); fetchSynchronously(globalObject, promise, specifier, referrer, parameters.copyRef(), scriptFetcher.copyRef()); RETURN_IF_EXCEPTION(scope, nullptr); } @@ -452,8 +451,9 @@ JSPromise* JSModuleLoader::loadModule(JSGlobalObject* globalObject, const Identi AbstractModuleRecord::ModuleRequest request { specifier, ScriptFetchParameters::create(type) }; #endif #if USE(BUN_JSC_ADDITIONS) - // The load keeps the async context of its caller. A synchronous load needs none: it ends inside the call. - JSValue asyncContext = flags.contains(ModuleLoadFlag::Synchronous) ? JSValue() : AsyncContextSwapScope::current(vm, globalObject); + // The load keeps the async context of its caller. A synchronous load keeps none: what it runs inside the call of + // require() runs in the context of the caller. + JSValue asyncContext = flags.contains(ModuleLoadFlag::Synchronous) ? JSValue() : asyncContextForLoad(vm, globalObject); auto* context = ModuleLoadingContext::create(vm, this, request, WTF::move(scriptFetcher), flags, referrerAsyncOrder, asyncContext); #else auto* context = ModuleLoadingContext::create(vm, this, request, WTF::move(scriptFetcher), flags, referrerAsyncOrder); @@ -551,7 +551,7 @@ JSPromise* JSModuleLoader::requestImportModuleForKey(JSGlobalObject* globalObjec resultPromise->markAsHandled(); #if USE(BUN_JSC_ADDITIONS) // The job has no other use for its context: it is the async context of the import(), under which the job asks the namespace for then(). - promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::ImportModuleNamespace, resultPromise, AsyncContextSwapScope::current(vm, globalObject)); + promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::ImportModuleNamespace, resultPromise, asyncContextForLoad(vm, globalObject)); #else promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::ImportModuleNamespace, resultPromise, jsUndefined()); #endif @@ -759,6 +759,22 @@ JSValue JSModuleLoader::asyncContextOfLoad(JSCell* payload) return state->asyncContext(); return uncheckedDowncast(payload)->asyncContext(); } + +JSValue JSModuleLoader::asyncContextForLoad(VM& vm, JSGlobalObject* globalObject) const +{ + JSValue asyncContext = AsyncContextSwapScope::current(vm, globalObject); + if (asyncContext.isUndefined() || !m_asyncContextOfLoadFilter) [[likely]] + return asyncContext; + return m_asyncContextOfLoadFilter(globalObject, asyncContext); +} + +JSValue JSModuleLoader::asyncContextOfModulesThatWait(VM& vm, JSGlobalObject* globalObject) const +{ + auto* job = vm.m_moduleEvaluationAsyncContextScope; + if (!job) + return { }; + return job->didEnter() ? asyncContextForLoad(vm, globalObject) : job->asyncContextOfLoad(); +} #endif JSPromise* JSModuleLoader::hostLoadImportedModule(JSGlobalObject* globalObject, const ModuleReferrer& referrer, const ModuleRequest& moduleRequest, JSCell* payload, RefPtr scriptFetcher, bool useImportMap, JSPromise* deferredPromise) @@ -924,7 +940,6 @@ JSPromise* JSModuleLoader::hostLoadImportedModule(JSGlobalObject* globalObject, JSPromise* modulePromise = mapEntry->ensureModulePromise(globalObject); if (modulePromise->status() == JSPromise::Status::Pending) { if (fetchPromise->status() == JSPromise::Status::Pending) { - mapEntry->clearAsyncContext(); fetchSynchronously(globalObject, fetchPromise, resolved, moduleReferrer(referrerKey), RefPtr { moduleRequest.m_attributes }, scriptFetcher.copyRef()); RETURN_IF_EXCEPTION(scope, nullptr); } else if (fetchPromise->status() == JSPromise::Status::Fulfilled) { @@ -1802,14 +1817,7 @@ JSPromise* JSModuleLoader::makeModule(JSGlobalObject* globalObject, const Identi MarkedArgumentBuffer args; Vector exportNames; - JSObject* lazyExportsSource; - { - // As JSModuleRecord::evaluate() does for the body of an ES module. - std::optional loaderAsyncContext; - if (JSValue asyncContext = this->asyncContext()) - loaderAsyncContext.emplace(vm, globalObject, asyncContext); - lazyExportsSource = syntheticSourceProvider->generate(globalObject, moduleKey, exportNames, args); - } + JSObject* lazyExportsSource = syntheticSourceProvider->generate(globalObject, moduleKey, exportNames, args); RETURN_IF_EXCEPTION(scope, promise->rejectWithCaughtException(vm, scope)); auto* moduleRecord = SyntheticModuleRecord::tryCreateWithExportNamesAndValues(globalObject, this, moduleKey, exportNames, args, lazyExportsSource); diff --git a/Source/JavaScriptCore/runtime/JSModuleLoader.h b/Source/JavaScriptCore/runtime/JSModuleLoader.h index 8573c726107c6..b79fc044287cd 100644 --- a/Source/JavaScriptCore/runtime/JSModuleLoader.h +++ b/Source/JavaScriptCore/runtime/JSModuleLoader.h @@ -50,7 +50,8 @@ enum class ModuleLoadFlag : uint8_t { Dynamic = 1 << 1, UseImportMap = 1 << 2, Deferred = 1 << 3, - // JSModuleLoader::loadModuleSync(): every step of the load runs inside the call, in the async context of its caller. + // JSModuleLoader::loadModuleSync(): the load keeps no async context. What it runs inside the call runs in the + // context of the caller. Synchronous = 1 << 4, }; @@ -100,6 +101,17 @@ class JSModuleLoader final : public JSCell { // keeps it in its cells (ModuleLoadingContext, ModuleLoaderPayload, ModuleGraphLoadingState), and every request // that the load makes takes it from there, not from what is current where the request is made. static JSValue asyncContextOfLoad(JSCell* payload); + + // What a load that begins here keeps of the async context of its caller. The host can set a filter for what no + // load may keep. Bun.ModuleGraph has the context of a graph in the slot, beside the stores of AsyncLocalStorage, and + // what runs in the context of a graph ends with the graph: a hook of the host, or a module of another loader, must + // not. So Bun lets a load that begins in the context of a graph keep nothing. + using AsyncContextOfLoadFilter = JSValue (*)(JSGlobalObject*, JSValue asyncContext); + void setAsyncContextOfLoadFilter(AsyncContextOfLoadFilter filter) { m_asyncContextOfLoadFilter = filter; } + JS_EXPORT_PRIVATE JSValue asyncContextForLoad(VM&, JSGlobalObject*) const; + // For CyclicModuleRecord::executeAsync(): the async context of the job that runs the modules that wait for a + // top-level await. See ModuleEvaluationAsyncContextScope. Empty where no job of the loader evaluates. + JSValue asyncContextOfModulesThatWait(VM&, JSGlobalObject*) const; #endif DECLARE_INFO; @@ -331,6 +343,7 @@ class JSModuleLoader final : public JSCell { WriteBarrier m_moduleScope; #if USE(BUN_JSC_ADDITIONS) WriteBarrier m_asyncContext; + AsyncContextOfLoadFilter m_asyncContextOfLoadFilter { nullptr }; #endif ModuleMap> m_moduleMap; diff --git a/Source/JavaScriptCore/runtime/ModuleLoadingContext.h b/Source/JavaScriptCore/runtime/ModuleLoadingContext.h index 50def0b07379a..9f11a3bea0d99 100644 --- a/Source/JavaScriptCore/runtime/ModuleLoadingContext.h +++ b/Source/JavaScriptCore/runtime/ModuleLoadingContext.h @@ -79,7 +79,8 @@ class ModuleLoadingContext final : public JSCell { #if USE(BUN_JSC_ADDITIONS) // The async context of the load that this request belongs to: what was current where import() was called. The jobs // that this context goes to (ModuleLoadStep, ModuleLoadTopSettled, ModuleLoadLinkEvaluateSettled) run under it where - // it is a store. Empty for a synchronous load, whose jobs all run inside the call of require(). + // it is a store, but not in the drain of a synchronous load: ModuleLoaderAsyncContextScope. Empty for a synchronous + // load, which keeps none. JSValue asyncContext() const { return m_asyncContext.get(); } #endif diff --git a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp index 8f163b462b9ae..604ef96af3257 100644 --- a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp +++ b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp @@ -150,7 +150,8 @@ JSPromise* ModuleRegistryEntry::ensureModulePromise(JSGlobalObject* globalObject JSPromise* fetchPromise = ensureFetchPromise(globalObject); #if USE(BUN_JSC_ADDITIONS) - if (asyncContext && !asyncContext.isUndefined()) + // Not for a loader that has an async context of its own: the host runs the body of its modules in that one. + if (asyncContext && !asyncContext.isUndefined() && !m_loader->asyncContext()) m_asyncContext.set(vm, this, asyncContext); #else UNUSED_PARAM(asyncContext); diff --git a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.h b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.h index 2689daa39ab01..b6ec3b3eba0dc 100644 --- a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.h +++ b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.h @@ -75,7 +75,9 @@ class ModuleRegistryEntry final : public JSCell { AbstractModuleRecord* record() const; JSPromise* ensureFetchPromise(JSGlobalObject*); // asyncContext: of the load that asks. The first load that asks has the record made: ModuleRegistryFetchSettled makes - // it, which runs the body of a CommonJS module, under that context where it is a store. + // it, which runs the body of a CommonJS module, under that context where it is a store (ModuleLoaderAsyncContextScope: + // not in the drain of a synchronous load). An entry of a loader that has an async context of its own + // (JSModuleLoader::asyncContext()) keeps none: the host runs the body of its modules in the loader's. JSPromise* ensureModulePromise(JSGlobalObject*, JSValue asyncContext = { }); #if USE(BUN_JSC_ADDITIONS) // For the job that makes the record. The entry does not keep the context behind it. @@ -85,9 +87,6 @@ class ModuleRegistryEntry final : public JSCell { m_asyncContext.clear(); return asyncContext; } - // For a synchronous load that takes the fetch over: the record is made inside the call of require(), in the - // context of its caller. - void clearAsyncContext() { m_asyncContext.clear(); } #endif JSPromise* loadPromise() const; JSValue error(JSGlobalObject*) const; diff --git a/Source/JavaScriptCore/runtime/SyntheticModuleRecord.cpp b/Source/JavaScriptCore/runtime/SyntheticModuleRecord.cpp index 40ba4ca17720e..1205bf2ff6e82 100644 --- a/Source/JavaScriptCore/runtime/SyntheticModuleRecord.cpp +++ b/Source/JavaScriptCore/runtime/SyntheticModuleRecord.cpp @@ -27,15 +27,9 @@ #include "SyntheticModuleRecord.h" #include "ArgList.h" -#if USE(BUN_JSC_ADDITIONS) -#include "AsyncContextSwapScope.h" -#endif #include "BuiltinNames.h" #include "JSCInlines.h" #include "JSModuleEnvironment.h" -#if USE(BUN_JSC_ADDITIONS) -#include "JSModuleLoader.h" -#endif #include "JSModuleNamespaceObject.h" #include "JSONObject.h" @@ -124,14 +118,7 @@ void SyntheticModuleRecord::runDeferredGenerator(JSGlobalObject* globalObject) Ref generator = *m_deferredGenerator; MarkedArgumentBuffer exportValues; Vector exportNames; - JSObject* lazyExportsSource; - { - // As JSModuleRecord::evaluate() does for the body of an ES module. - std::optional loaderAsyncContext; - if (JSValue asyncContext = moduleLoader()->asyncContext()) - loaderAsyncContext.emplace(vm, globalObject, asyncContext); - lazyExportsSource = generator->generate(globalObject, moduleKey(), exportNames, exportValues); - } + JSObject* lazyExportsSource = generator->generate(globalObject, moduleKey(), exportNames, exportValues); if (Exception* exception = scope.exception()) [[unlikely]] { failed(exception); return; diff --git a/Source/JavaScriptCore/runtime/VM.cpp b/Source/JavaScriptCore/runtime/VM.cpp index b05b3bd8c4f08..7a4c80baf00d7 100644 --- a/Source/JavaScriptCore/runtime/VM.cpp +++ b/Source/JavaScriptCore/runtime/VM.cpp @@ -709,6 +709,7 @@ static void didOpenOrCloseSynchronousModuleQueue(VM& vm) VM::SynchronousModuleQueueScope::SynchronousModuleQueueScope(VM& vm) : m_vm(vm) + , m_moduleEvaluationAsyncContextScope(std::exchange(vm.m_moduleEvaluationAsyncContextScope, nullptr)) { m_queue.prev = std::exchange(vm.m_synchronousModuleQueue, &m_queue); if (!m_queue.prev) @@ -717,6 +718,7 @@ VM::SynchronousModuleQueueScope::SynchronousModuleQueueScope(VM& vm) VM::SynchronousModuleQueueScope::~SynchronousModuleQueueScope() { + m_vm.m_moduleEvaluationAsyncContextScope = m_moduleEvaluationAsyncContextScope; m_vm.m_synchronousModuleQueue = m_queue.prev; if (!m_queue.prev) didOpenOrCloseSynchronousModuleQueue(m_vm); diff --git a/Source/JavaScriptCore/runtime/VM.h b/Source/JavaScriptCore/runtime/VM.h index 0538c4cc3f141..d736ee36e92fe 100644 --- a/Source/JavaScriptCore/runtime/VM.h +++ b/Source/JavaScriptCore/runtime/VM.h @@ -147,6 +147,7 @@ class MicrotaskCallCache; class MicrotaskQueue; class NativeExecutable; #if USE(BUN_JSC_ADDITIONS) +class ModuleEvaluationAsyncContextScope; class QueuedTask; enum class InternalMicrotask : uint8_t; namespace FFI { class CallbackEntryScope; } @@ -1553,6 +1554,9 @@ class VM : public ThreadSafeRefCountedWithSuppressingSaferCPPChecking { SynchronousModuleQueue* prev { nullptr }; }; SynchronousModuleQueue* m_synchronousModuleQueue { nullptr }; + // The scope of the job of the module loader that evaluates modules now. CyclicModuleRecord::executeAsync() asks it + // for the async context of the modules that wait for a top-level await. + ModuleEvaluationAsyncContextScope* m_moduleEvaluationAsyncContextScope { nullptr }; #if ENABLE(AOT) // While the innermost queue is this one, reactions go to the microtask queue. It stays in the chain, where the collector finds it. SynchronousModuleQueue* m_synchronousModuleQueueSetAside { nullptr }; @@ -1578,6 +1582,8 @@ class VM : public ThreadSafeRefCountedWithSuppressingSaferCPPChecking { private: VM& m_vm; SynchronousModuleQueue m_queue; + // What a synchronous load evaluates by itself is no job of the module loader. + ModuleEvaluationAsyncContextScope* m_moduleEvaluationAsyncContextScope; }; private: #endif From 1a52269eca8183cab00275841873020c599cbd0c Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 1 Nov 2026 10:54:53 +0000 Subject: [PATCH 4/7] [JSC] AOT: an import() whose text waits keeps the async context of its call #942 holds an import() that a file calls while a graph is loaded until the text of the module comes, in a later turn of the event loop. The import() began again there, under the async context of that turn, so its modules did not read the store of the call. TextThatWaits keeps the async context of the call, as CountedImport and DeferredImport do, and deliverFirstTextThatWaits() installs it. handSynchronousLoadToLoader() hands the load of a require() to the loader. It passes ModuleLoadFlag::Synchronous, as JSModuleLoader::loadModuleSync() does: that load keeps no async context. --- Source/JavaScriptCore/aot/runtime/AOTModuleEvaluation.cpp | 3 ++- Source/JavaScriptCore/aot/runtime/AOTModules.cpp | 4 +++- Source/JavaScriptCore/aot/runtime/AOTModules.h | 2 ++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Source/JavaScriptCore/aot/runtime/AOTModuleEvaluation.cpp b/Source/JavaScriptCore/aot/runtime/AOTModuleEvaluation.cpp index c32fd3ac045ea..b224b23a7b4b7 100644 --- a/Source/JavaScriptCore/aot/runtime/AOTModuleEvaluation.cpp +++ b/Source/JavaScriptCore/aot/runtime/AOTModuleEvaluation.cpp @@ -1494,7 +1494,8 @@ void Modules::handSynchronousLoadToLoader(FilesBeingRun& files) SetForScope fileBeingHandedToLoader(m_fileBeingHandedToLoader, fileOf(running)); SetForScope isAskingHost(m_isAskingHost, true); takeUpWaysOfLoads(); - JSPromise* promise = loader()->loadModule(m_instance.globalObject, moduleKey(files.root), nullptr, nullptr, { ModuleLoadFlag::Evaluate }); + // The load stands for the one that require() began: as JSModuleLoader::loadModuleSync(), it keeps no async context. + JSPromise* promise = loader()->loadModule(m_instance.globalObject, moduleKey(files.root), nullptr, nullptr, { ModuleLoadFlag::Evaluate, ModuleLoadFlag::Synchronous }); scope.assertNoException(); m_synchronousLoadsHandedToLoader.append({ files.root, Strong(vm, promise) }); runSynchronousStepsInFrontOf(fileOf(running)); diff --git a/Source/JavaScriptCore/aot/runtime/AOTModules.cpp b/Source/JavaScriptCore/aot/runtime/AOTModules.cpp index 29a1df9e67775..5ab423ded4a7f 100644 --- a/Source/JavaScriptCore/aot/runtime/AOTModules.cpp +++ b/Source/JavaScriptCore/aot/runtime/AOTModules.cpp @@ -2207,7 +2207,7 @@ void Modules::holdText(JSPromise* promise, ModuleID module, JSCell* requestedRec size_t index = m_textsThatWait.size(); while (index && m_textsThatWait[index - 1].place > place) --index; - m_textsThatWait.insert(index, TextThatWaits { place, Strong(vm, promise), module, Strong(vm, requestedRecord), referrerAsyncOrder }); + m_textsThatWait.insert(index, TextThatWaits { place, Strong(vm, promise), module, Strong(vm, requestedRecord), referrerAsyncOrder, requestedRecord ? Strong() : currentAsyncContext() }); host().callInLaterTurn(m_instance.globalObject); } @@ -2235,6 +2235,8 @@ bool Modules::deliverFirstTextThatWaits() promise->fulfill(vm, text.requestedRecord.get()); return true; } + // The load of `bun run` keeps the async context of the call of import() while it waits for the text. + AsyncContextSwapScope asyncContextOfImport(vm, realm, text.asyncContext.get()); JSPromise* loaded = startImport(text.module, text.referrerAsyncOrder, ImportCaller::Host); if (scope.exception()) [[unlikely]] { promise->rejectWithCaughtException(vm, scope); diff --git a/Source/JavaScriptCore/aot/runtime/AOTModules.h b/Source/JavaScriptCore/aot/runtime/AOTModules.h index c3ec1d57b6e65..f6310271eaf2e 100644 --- a/Source/JavaScriptCore/aot/runtime/AOTModules.h +++ b/Source/JavaScriptCore/aot/runtime/AOTModules.h @@ -1013,6 +1013,8 @@ class Modules { // Null for an import(). Strong requestedRecord { }; std::optional referrerAsyncOrder { }; + // For an import(): the async context of its call, which the import() goes on under when its text comes. Empty where there is none. + Strong asyncContext { }; }; Vector m_textsThatWait; bool isTextReadOnOtherThread(ModuleID file); From e8f515385f1f248170d2e0140aef2a8c7b41b3bb Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 1 Nov 2026 12:19:30 +0000 Subject: [PATCH 5/7] [JSC] AOT: an import() whose text has come goes on as a job of its own would deliverFirstTextThatWaits() can run in the job of another import(), where main delivers the calls that come with the call in front. The import() that it starts again runs under the async context of its own call, or under none where that call had none, and ModuleEvaluationAsyncContextScope names its load for the modules that wait. In the drain of a synchronous load it enters no async context, as the jobs of the loader. --- Source/JavaScriptCore/aot/runtime/AOTModules.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Source/JavaScriptCore/aot/runtime/AOTModules.cpp b/Source/JavaScriptCore/aot/runtime/AOTModules.cpp index 5263a856431e6..9635318a22135 100644 --- a/Source/JavaScriptCore/aot/runtime/AOTModules.cpp +++ b/Source/JavaScriptCore/aot/runtime/AOTModules.cpp @@ -3027,8 +3027,12 @@ bool Modules::deliverFirstTextThatWaits(TextOfImport textOfImport) promise->fulfill(vm, text.requestedRecord.get()); return true; } - // The load of `bun run` keeps the async context of the call of import() while it waits for the text. - AsyncContextSwapScope asyncContextOfImport(vm, m_instance.globalObject, text.asyncContext.get()); + // The import() goes on as a job of its own would: under the async context of its call, which the load of `bun run` keeps while it waits for the text, and under none where the call had none. + // Not in the drain of a synchronous load, where no job of the loader enters an async context. + std::optional asyncContextOfCall; + if (!vm.synchronousModuleQueue()) + asyncContextOfCall.emplace(vm, m_instance.globalObject, text.asyncContext.get()); + ModuleEvaluationAsyncContextScope jobOfImport(vm, m_instance.globalObject, text.asyncContext.get()); bool isMadeAgain = text.waitsApart || text.caller == ImportCaller::Host; JSPromise* loaded = startImport(text.module, text.referrerAsyncOrder, isMadeAgain ? text.caller : ImportCaller::Host, textOfImport, isMadeAgain ? promise : nullptr); if (scope.exception()) [[unlikely]] { From 596c7bb5cb3efff2b1d74312ac4ff34b66c1fc83 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 1 Nov 2026 13:37:42 +0000 Subject: [PATCH 6/7] [JSC] AOT: the import() that starts again keeps the async context of its call in a drain too deliverFirstTextThatWaits() left the slot alone in the drain of a synchronous load, and startImport() then kept the store of the caller of require() for every later job of the import(). It installs the async context of the call there too, as handCountedImportsToLoader() does. --- Source/JavaScriptCore/aot/runtime/AOTModules.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Source/JavaScriptCore/aot/runtime/AOTModules.cpp b/Source/JavaScriptCore/aot/runtime/AOTModules.cpp index 9635318a22135..37164df1c5480 100644 --- a/Source/JavaScriptCore/aot/runtime/AOTModules.cpp +++ b/Source/JavaScriptCore/aot/runtime/AOTModules.cpp @@ -3028,10 +3028,8 @@ bool Modules::deliverFirstTextThatWaits(TextOfImport textOfImport) return true; } // The import() goes on as a job of its own would: under the async context of its call, which the load of `bun run` keeps while it waits for the text, and under none where the call had none. - // Not in the drain of a synchronous load, where no job of the loader enters an async context. - std::optional asyncContextOfCall; - if (!vm.synchronousModuleQueue()) - asyncContextOfCall.emplace(vm, m_instance.globalObject, text.asyncContext.get()); + // What startImport() keeps for the later jobs of the import() is that async context, whatever was current here. + AsyncContextSwapScope asyncContextOfCall(vm, m_instance.globalObject, text.asyncContext.get()); ModuleEvaluationAsyncContextScope jobOfImport(vm, m_instance.globalObject, text.asyncContext.get()); bool isMadeAgain = text.waitsApart || text.caller == ImportCaller::Host; JSPromise* loaded = startImport(text.module, text.referrerAsyncOrder, isMadeAgain ? text.caller : ImportCaller::Host, textOfImport, isMadeAgain ? promise : nullptr); From b5c2090caabcfb1221c252819506629ce956365b Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 2 Nov 2026 08:57:40 +0000 Subject: [PATCH 7/7] [JSC] AOT: an import() that waits behind a load keeps the async context of its call #974 holds an import() behind an earlier load where the two share a cycle that is left to run, or where the earlier graph has the module. resumeImportsBehindLoad() makes the call again in a job of the earlier load, so the modules that only the later graph has read the store of the earlier import(). ImportBehindLoad keeps the async context of the call, and the call is made again under it, as for a call whose text waits. --- Source/JavaScriptCore/aot/runtime/AOTModules.cpp | 5 ++++- Source/JavaScriptCore/aot/runtime/AOTModules.h | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Source/JavaScriptCore/aot/runtime/AOTModules.cpp b/Source/JavaScriptCore/aot/runtime/AOTModules.cpp index e1d749e974245..aecfb79d9d48e 100644 --- a/Source/JavaScriptCore/aot/runtime/AOTModules.cpp +++ b/Source/JavaScriptCore/aot/runtime/AOTModules.cpp @@ -4949,7 +4949,7 @@ JSPromise* Modules::startImport(ModuleID module, std::optional referrer void Modules::holdImportBehindLoad(ModuleID root, ModuleID module, std::optional referrerAsyncOrder, JSPromise* promise, uint32_t jobsWaited) { - m_importsBehindLoads.append(ImportBehindLoad { root, module, referrerAsyncOrder, Strong(*m_instance.vm, promise), m_roundsOfJobs - std::min(jobsWaited, m_roundsOfJobs), root == module || leadsTo(root, module) }); + m_importsBehindLoads.append(ImportBehindLoad { root, module, referrerAsyncOrder, Strong(*m_instance.vm, promise), m_roundsOfJobs - std::min(jobsWaited, m_roundsOfJobs), root == module || leadsTo(root, module), currentAsyncContext() }); queueJobOfRound(module); } @@ -5082,6 +5082,9 @@ void Modules::resumeImportsBehindLoad(ModuleID root) // The job of this round is among them. It is spent in a job of the call's own: the graph it waited for may not have run yet. SetForScope jobsWaited(m_jobsThatCallHasWaited, import.isOfGraph ? 0 : m_roundsOfJobs - import.roundAtCall + 1); SetForScope isOfGraphThatRan(m_isCallMadeAgainOfGraphThatRan, import.isOfGraph); + // The call is made again as a job of its own would make it: under the async context of the call, and under none where it had none. + AsyncContextSwapScope asyncContextOfCall(vm, m_instance.globalObject, import.asyncContext.get()); + ModuleEvaluationAsyncContextScope jobOfImport(vm, m_instance.globalObject, import.asyncContext.get()); JSPromise* loaded = startImport(import.module, import.referrerAsyncOrder, ImportCaller::CompiledCode, TextOfImport::HasCome, promise); if (scope.exception()) [[unlikely]] { if (vm.isTerminationException(scope.exception())) diff --git a/Source/JavaScriptCore/aot/runtime/AOTModules.h b/Source/JavaScriptCore/aot/runtime/AOTModules.h index ea4d1fe61f9fe..2277b98fbee80 100644 --- a/Source/JavaScriptCore/aot/runtime/AOTModules.h +++ b/Source/JavaScriptCore/aot/runtime/AOTModules.h @@ -1381,6 +1381,8 @@ class Modules { uint32_t roundAtCall; // The module runs with the graph that the call waits for: the call has nothing of its own to load, and settles behind that one. bool isOfGraph; + // The async context of the call, which the call is made again under. Empty where there is none. + Strong asyncContext { }; }; Vector m_importsBehindLoads; void holdImportBehindLoad(ModuleID root, ModuleID, std::optional referrerAsyncOrder, JSPromise*, uint32_t jobsWaited);