From a62a2027d828125390866245c85330a6edc94937 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 8 Aug 2026 05:31:05 +0000 Subject: [PATCH 1/5] JSModuleLoader: decide needsErrorReaction by membership in innerModuleLoading A require(esm) nested inside the host load hook drains the synchronous module queue: Bun evaluates CommonJS module bodies during makeModule, and such a body can require an ESM sibling of the graph that is currently loading. The drain then runs pending ModuleLoadStep reactions for other requests of the same referrer module, so module.loadedModules() can grow across the hostLoadImportedModule call without the current request having completed, and can grow by more than one. The size-delta check then misidentifies whether this request still needs a ModuleGraphLoadingError reaction (a missed reaction loses the rejection, leaving the graph load pending forever), and the growth assertion fires on debug builds: ASSERTION FAILED: module->loadedModules().size() <= loadedModulesCountBefore + 1 Decide by membership of the request itself instead. --- .../JavaScriptCore/runtime/JSModuleLoader.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp index 22ba1ea14e9bd..03b258f74ce36 100644 --- a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp +++ b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp @@ -981,7 +981,6 @@ void JSModuleLoader::innerModuleLoading(JSGlobalObject* globalObject, ModuleGrap } // 2.d.iii. Else, // 2.d.iii.1. Perform HostLoadImportedModule(module, request, state.[[HostDefined]], state). - unsigned loadedModulesCountBefore = module->loadedModules().size(); JSPromise* promise = hostLoadImportedModule(globalObject, cyclic, request, state, state->scriptFetcher(), true); if (scope.exception()) [[unlikely]] { state->setDrainingInnerLoad(false); @@ -989,12 +988,18 @@ void JSModuleLoader::innerModuleLoading(JSGlobalObject* globalObject, ModuleGrap } // 2.d.iii.2. NOTE: HostLoadImportedModule will call FinishLoadingImportedModule, which re-enters the graph loading process through ContinueModuleLoading. // - // If module.[[LoadedModules]] grew across the HostLoadImportedModule call, the requested - // module was loaded synchronously, which means it was already loaded before. In that case - // there is no need to attach a ModuleGraphLoadingError reaction, so we skip it. - bool needsErrorReaction = module->loadedModules().size() == loadedModulesCountBefore; - ASSERT(module->loadedModules().size() <= loadedModulesCountBefore + 1); - ASSERT(needsErrorReaction != module->loadedModules().contains(ModuleMapKey { request.m_specifier.impl(), request.type() })); + // If the requested module is in module.[[LoadedModules]] after the + // HostLoadImportedModule call, it was loaded synchronously and + // FinishLoadingImportedModule already continued this state, so no + // ModuleGraphLoadingError reaction is needed. Membership — not a + // loadedModules size delta — is the test: a require(esm) nested + // inside the host hook (a CommonJS module evaluated during + // makeModule requiring a sibling of this graph) drains the + // synchronous module queue, which can run this module's pending + // ModuleLoadStep reactions for OTHER requests during the call, so + // the size can grow without THIS request having completed — and + // grow by more than one. + bool needsErrorReaction = !module->loadedModules().contains(ModuleMapKey { request.m_specifier.impl(), request.type() }); if (needsErrorReaction) promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::ModuleGraphLoadingError, nullptr, state); // 2.d.iv. If state.[[IsLoading]] is false, return UNUSED. From ba00b79ca4337b5b7074add77933f0a7ccff9354 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 16 Sep 2026 09:50:29 +0000 Subject: [PATCH 2/5] [JSC] A synchronous module load uses a fetch that the host already delivered hostLoadImportedModule() connects the host's fetch promise to the entry's fetch promise with pipeFrom(), which is a microtask. Beneath loadModuleSync (Bun's require(esm)) the host fetches synchronously, so its promise is already fulfilled, but the job that hands the source over is parked in VM::m_synchronousModuleQueue. Until that job runs, the entry is Fetching with a pending fetch promise, as if the fetch were still in flight. Two callers read it that way: 1. A second static request for the module (two siblings import it) ran the host's fetch again. In Bun the module was transpiled twice, its macros ran twice, and an onLoad plugin saw it twice. When the second request came from a macro's module, the second transpile was the macro-mode one, and it replaced the source that the program runs. 2. A top-level load that has to complete before the queue drains again waited for the promise, and nothing could settle it. A require() from a CommonJS module that is evaluated while another module's imports are requested reported `require() async module ... is unsupported`. A Bun macro's import() of its own file spun forever. The entry now keeps the host's promise. Both callers take its result when it is already settled: the static request in place of the second fetch, at the same point, so the order of the loader's jobs does not change, and the top-level loadModule() before it chains on the fetch promise. A fetch that is really in flight is handled as before. * Source/JavaScriptCore/runtime/JSModuleLoader.cpp: (JSC::JSModuleLoader::loadModule): (JSC::JSModuleLoader::hostLoadImportedModule): * Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp: (JSC::ModuleRegistryEntry::visitChildrenImpl): (JSC::ModuleRegistryEntry::setFetchSource): (JSC::ModuleRegistryEntry::takeSettledFetchSource): (JSC::ModuleRegistryEntry::fetchComplete): * Source/JavaScriptCore/runtime/ModuleRegistryEntry.h: --- .../JavaScriptCore/runtime/JSModuleLoader.cpp | 41 +++++++++++++------ .../runtime/ModuleRegistryEntry.cpp | 25 +++++++++++ .../runtime/ModuleRegistryEntry.h | 8 ++++ 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp index 03b258f74ce36..8caf3f24ecf44 100644 --- a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp +++ b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp @@ -387,6 +387,13 @@ JSPromise* JSModuleLoader::loadModule(JSGlobalObject* globalObject, const Identi if (entry->status() != ModuleRegistryEntry::Status::New) promise = entry->ensureFetchPromise(globalObject); +#if USE(BUN_JSC_ADDITIONS) + // Beneath a synchronous load (loadModuleSync), the job that hands a delivered fetch to this promise + // can be parked in a queue that does not drain before this load has to complete (Bun: a macro's + // import() of a module that the require(esm) above it requested). + if (vm.m_synchronousModuleQueue) + entry->takeSettledFetchSource(vm); +#endif } } @@ -810,19 +817,24 @@ JSPromise* JSModuleLoader::hostLoadImportedModule(JSGlobalObject* globalObject, JSPromise* modulePromise = mapEntry->ensureModulePromise(globalObject); if (modulePromise->status() == JSPromise::Status::Pending) { if (fetchPromise->status() == JSPromise::Status::Pending) { - // Transpilation still in flight — re-issue through the - // embedder's synchronous fetch. fetchPromise was already - // pipeFrom()'d by the async path which set - // isFirstResolvingFunctionCalledFlag, so use the unguarded - // fulfill/reject. The ModuleRegistryFetchSettled reaction on - // fetchPromise lands on the sync queue and drives the rest of - // the chain (including loadPromise). - JSPromise* promise = fetch(globalObject, identifierToJSValue(vm, resolved), moduleReferrer(referrerKey), nullptr, scriptFetcher.copyRef()); - RETURN_IF_EXCEPTION(scope, nullptr); - if (promise->status() == JSPromise::Status::Fulfilled) - fetchPromise->fulfillPromise(vm, promise->result()); - else if (promise->status() == JSPromise::Status::Rejected) - fetchPromise->rejectPromise(vm, promise->result()); + // The host has often delivered this fetch already: beneath a synchronous load it + // fetches synchronously, and only the pipeFrom() job that hands the result to + // fetchPromise is parked in a queue. Fetch a second time only if it has not. + if (!mapEntry->takeSettledFetchSource(vm)) { + // Transpilation still in flight — re-issue through the + // embedder's synchronous fetch. fetchPromise was already + // pipeFrom()'d by the async path which set + // isFirstResolvingFunctionCalledFlag, so use the unguarded + // fulfill/reject. The ModuleRegistryFetchSettled reaction on + // fetchPromise lands on the sync queue and drives the rest of + // the chain (including loadPromise). + JSPromise* promise = fetch(globalObject, identifierToJSValue(vm, resolved), moduleReferrer(referrerKey), nullptr, scriptFetcher.copyRef()); + RETURN_IF_EXCEPTION(scope, nullptr); + if (promise->status() == JSPromise::Status::Fulfilled) + fetchPromise->fulfillPromise(vm, promise->result()); + else if (promise->status() == JSPromise::Status::Rejected) + fetchPromise->rejectPromise(vm, promise->result()); + } } else if (fetchPromise->status() == JSPromise::Status::Fulfilled) { // fetchPromise already settled but its // ModuleRegistryFetchSettled reaction is sitting on the @@ -881,6 +893,9 @@ JSPromise* JSModuleLoader::hostLoadImportedModule(JSGlobalObject* globalObject, mapEntry->setStatus(ModuleRegistryEntry::Status::Fetching); mapEntry->ensureFetchPromise(globalObject)->pipeFrom(vm, promise); +#if USE(BUN_JSC_ADDITIONS) + mapEntry->setFetchSource(vm, promise); +#endif } JSPromise* modulePromise = mapEntry->ensureModulePromise(globalObject); RETURN_IF_EXCEPTION(scope, nullptr); diff --git a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp index eaafa5340e054..4d058d1745b92 100644 --- a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp +++ b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp @@ -69,6 +69,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_fetchSource); +#endif } DEFINE_VISIT_CHILDREN(ModuleRegistryEntry); @@ -267,6 +270,25 @@ bool ModuleRegistryEntry::isLoaded() const return m_isLoaded || (m_loadPromise && m_loadPromise->status() == JSPromise::Status::Fulfilled); } +void ModuleRegistryEntry::setFetchSource(VM& vm, JSPromise* source) +{ + m_fetchSource.set(vm, this, source); +} + +bool ModuleRegistryEntry::takeSettledFetchSource(VM& vm) +{ + JSPromise* source = m_fetchSource.get(); + if (!source || source->status() == JSPromise::Status::Pending || !m_fetchPromise || m_fetchPromise->status() != JSPromise::Status::Pending) + return false; + // m_fetchPromise was pipeFrom()'d, so only the unguarded forms settle it. The pipe's job finds it settled later + // and does nothing. + if (source->status() == JSPromise::Status::Fulfilled) + m_fetchPromise->fulfillPromise(vm, source->result()); + else + m_fetchPromise->rejectPromise(vm, source->result()); + return true; +} + JSPromise* ModuleRegistryEntry::loadedPromise(JSGlobalObject* globalObject) { if (!m_loadPromise && m_isLoaded) { @@ -290,6 +312,9 @@ void ModuleRegistryEntry::fetchComplete(JSGlobalObject* globalObject, AbstractMo ASSERT(m_status == Status::Fetching); m_record.set(vm, this, record); m_status = Status::Fetched; +#if USE(BUN_JSC_ADDITIONS) + m_fetchSource.clear(); +#endif } } // namespace JSC diff --git a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.h b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.h index 3c535c70d6798..fc1d196522137 100644 --- a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.h +++ b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.h @@ -102,6 +102,13 @@ class ModuleRegistryEntry final : public JSCell { bool isLoaded() const; // loadPromise(), materializing it first for an entry that was markLoaded(). JSPromise* loadedPromise(JSGlobalObject*); + // The host's fetch promise that pipeFrom() connects to the fetch promise. The connection is a microtask, so the + // host can have delivered while the fetch promise is still pending. A synchronous load (loadModuleSync) that + // cannot wait for that microtask takes the result with takeSettledFetchSource(), and does not fetch again. + void setFetchSource(VM&, JSPromise*); + // Returns false when there is nothing to take: no source, a source that is still pending, or a fetch promise + // that is already settled. + bool takeSettledFetchSource(VM&); #endif private: @@ -123,6 +130,7 @@ class ModuleRegistryEntry final : public JSCell { Status m_status { Status::New }; #if USE(BUN_JSC_ADDITIONS) bool m_isLoaded { false }; + WriteBarrier m_fetchSource; #endif }; From 0460fb44d2ffa750ef3c9c65dab7fb422523886f Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 16 Sep 2026 09:51:30 +0000 Subject: [PATCH 3/5] [JSC] import() beneath a synchronous module load does not wait for another load's promise For an entry that has its record, hostLoadImportedModule() calls FinishLoadingImportedModule at once, but returns the entry's load promise, which only the load that created the entry fulfills. moduleLoadTopSettled AND-joins that promise into the result of a dynamic import. Beneath loadModuleSync (Bun's require(esm)) the reactions that fulfill it can be parked in VM::m_synchronousModuleQueue. A Bun macro on the main thread waits for `await import()` while require(esm) transpiles a dependency. When an ancestor in the require()'s graph had already imported the macro's file, the import() linked and evaluated the file, then waited for that promise, and the wait spun forever. Beneath a synchronous load a dynamic import now gets a fulfilled promise there. It does not need the entry's promise: ContinueDynamicImport runs LoadRequestedModules on the record itself, then links and evaluates it. The second import() of the same file takes the [[LoadedModules]] fast path, which returned the same promise, so that path does the same. A top-level graph load keeps the entry's promise, because it links when that promise is fulfilled. * Source/JavaScriptCore/runtime/JSModuleLoader.cpp: (JSC::JSModuleLoader::hostLoadImportedModule): --- .../JavaScriptCore/runtime/JSModuleLoader.cpp | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp index 8caf3f24ecf44..3cd94c96263c0 100644 --- a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp +++ b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp @@ -713,6 +713,24 @@ JSPromise* JSModuleLoader::hostLoadImportedModule(JSGlobalObject* globalObject, ModuleMapKey moduleMapKey { specifier.impl(), type }; +#if USE(BUN_JSC_ADDITIONS) + // An entry that has its record gets FinishLoadingImportedModule at once, and the caller gets the entry's + // load promise, which only the load that created the entry fulfills. Beneath a synchronous load + // (loadModuleSync), the reactions that do so can be parked in a queue that does not drain before this + // caller's load has to complete (Bun: a macro on the main thread waits for its import() inside + // require(esm)). A dynamic import does not need that promise: ContinueDynamicImport runs + // LoadRequestedModules on the record, then links and evaluates it. A top-level graph load (loadModule with + // a ModuleGraphLoadingState) links when that promise is fulfilled, so it keeps it. + auto loadPromiseAfterFinish = [&](JSPromise* entryLoadPromise, AbstractModuleRecord* loaded) -> JSPromise* { + if (!vm.m_synchronousModuleQueue || entryLoadPromise->status() != JSPromise::Status::Pending || !payload->inherits()) + return entryLoadPromise; + JSPromise* fulfilled = JSPromise::create(vm, globalObject->promiseStructure()); + fulfilled->markAsHandled(); + fulfilled->fulfill(vm, loaded); + return fulfilled; + }; +#endif + // HostLoadImportedModule is required to be idempotent for the same // (referrer, moduleRequest) pair. referrer.[[LoadedModules]] is that cache; // FinishLoadingImportedModule populates it, and innerModuleLoading consults it, @@ -733,7 +751,11 @@ JSPromise* JSModuleLoader::hostLoadImportedModule(JSGlobalObject* globalObject, ASSERT(loadedPromise); finishLoadingImportedModule(globalObject, referrer, moduleRequest, payload, loaded, scriptFetcher); RETURN_IF_EXCEPTION(scope, nullptr); +#if USE(BUN_JSC_ADDITIONS) + return loadPromiseAfterFinish(loadedPromise, loaded); +#else return loadedPromise; +#endif } } @@ -866,9 +888,12 @@ JSPromise* JSModuleLoader::hostLoadImportedModule(JSGlobalObject* globalObject, JSPromise* promise = mapEntry->loadPromise(); #endif if (promise) { - if (mapEntry->record()) { - finishLoadingImportedModule(globalObject, referrer, moduleRequest, payload, mapEntry->record(), scriptFetcher); + if (AbstractModuleRecord* loaded = mapEntry->record()) { + finishLoadingImportedModule(globalObject, referrer, moduleRequest, payload, loaded, scriptFetcher); RETURN_IF_EXCEPTION(scope, nullptr); +#if USE(BUN_JSC_ADDITIONS) + promise = loadPromiseAfterFinish(promise, loaded); +#endif } else { auto* context = ModuleLoadingContext::create(vm, this, ModuleLoadingContext::Step::Cached, referrer, moduleRequest, payload, mapEntry, scriptFetcher); JSPromise* resultPromise = JSPromise::create(vm, globalObject->promiseStructure()); From d953c3bdef41c355feebd3c497f82ca9370fc2d4 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 16 Sep 2026 10:59:50 +0000 Subject: [PATCH 4/5] [JSC] A top-level load beneath a synchronous load continues from the host's fetch promise The top-level loadModule() settled the entry's fetch promise from the delivered fetch. That queued ModuleRegistryFetchSettled ahead of ModuleLoadTopSettled. The first ran makeModule(). The second reached the inline replay in hostLoadImportedModule() while the module promise was still pending, and ran makeModule() again, so a synthetic module's generator ran twice. The same order occurs without this change when a parked pipeFrom() job runs ahead of a top-level load's reaction: an import() of a CommonJS module that the enclosing require(esm) has just fetched read the module's exports twice. loadModule() now chains ModuleLoadTopSettled on the host's promise and leaves the entry alone. hostLoadImportedModule() settles the entry from the delivered fetch, as it does for a static request, and the module is built once. * Source/JavaScriptCore/runtime/JSModuleLoader.cpp: (JSC::JSModuleLoader::loadModule): * Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp: (JSC::ModuleRegistryEntry::deliveredFetch const): (JSC::ModuleRegistryEntry::takeSettledFetchSource): * Source/JavaScriptCore/runtime/ModuleRegistryEntry.h: --- Source/JavaScriptCore/runtime/JSModuleLoader.cpp | 9 ++++++--- Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp | 10 +++++++++- Source/JavaScriptCore/runtime/ModuleRegistryEntry.h | 7 ++++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp index 3cd94c96263c0..e417c9a341a69 100644 --- a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp +++ b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp @@ -390,9 +390,12 @@ JSPromise* JSModuleLoader::loadModule(JSGlobalObject* globalObject, const Identi #if USE(BUN_JSC_ADDITIONS) // Beneath a synchronous load (loadModuleSync), the job that hands a delivered fetch to this promise // can be parked in a queue that does not drain before this load has to complete (Bun: a macro's - // import() of a module that the require(esm) above it requested). - if (vm.m_synchronousModuleQueue) - entry->takeSettledFetchSource(vm); + // import() of a module that the require(esm) above it requested). Continue from the host's promise. + // hostLoadImportedModule() then settles the entry from it, as it does for a static request. + if (vm.m_synchronousModuleQueue) { + if (JSPromise* delivered = entry->deliveredFetch()) + promise = delivered; + } #endif } } diff --git a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp index 4d058d1745b92..66c95e190f791 100644 --- a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp +++ b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp @@ -275,10 +275,18 @@ void ModuleRegistryEntry::setFetchSource(VM& vm, JSPromise* source) m_fetchSource.set(vm, this, source); } -bool ModuleRegistryEntry::takeSettledFetchSource(VM& vm) +JSPromise* ModuleRegistryEntry::deliveredFetch() const { JSPromise* source = m_fetchSource.get(); if (!source || source->status() == JSPromise::Status::Pending || !m_fetchPromise || m_fetchPromise->status() != JSPromise::Status::Pending) + return nullptr; + return source; +} + +bool ModuleRegistryEntry::takeSettledFetchSource(VM& vm) +{ + JSPromise* source = deliveredFetch(); + if (!source) return false; // m_fetchPromise was pipeFrom()'d, so only the unguarded forms settle it. The pipe's job finds it settled later // and does nothing. diff --git a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.h b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.h index fc1d196522137..0c6ff866b120b 100644 --- a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.h +++ b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.h @@ -104,10 +104,11 @@ class ModuleRegistryEntry final : public JSCell { JSPromise* loadedPromise(JSGlobalObject*); // The host's fetch promise that pipeFrom() connects to the fetch promise. The connection is a microtask, so the // host can have delivered while the fetch promise is still pending. A synchronous load (loadModuleSync) that - // cannot wait for that microtask takes the result with takeSettledFetchSource(), and does not fetch again. + // cannot wait for that microtask uses the delivered result, and does not fetch again. void setFetchSource(VM&, JSPromise*); - // Returns false when there is nothing to take: no source, a source that is still pending, or a fetch promise - // that is already settled. + // The fetch source, when it is settled and the fetch promise is still pending. Null otherwise. + JSPromise* deliveredFetch() const; + // Settles the fetch promise from deliveredFetch(). Returns false when there is none. bool takeSettledFetchSource(VM&); #endif From c5122dfb8501dadc1602ebf9eb11ddef8a0831a2 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 16 Sep 2026 12:08:29 +0000 Subject: [PATCH 5/5] [JSC] ModuleRegistryEntry: release the fetch source on every terminal path fetchComplete() was the only place that cleared m_fetchSource. An entry that fails (setFetchError, setInstantiationError, setEvaluationError), an entry that provideModule() completes, and an entry whose delivered fetch was taken kept the host's promise for the lifetime of the entry. setStatus() now clears it for every status past Fetching, and provideModule() and takeSettledFetchSource() clear it themselves. * Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp: (JSC::ModuleRegistryEntry::setStatus): (JSC::ModuleRegistryEntry::provideModule): (JSC::ModuleRegistryEntry::takeSettledFetchSource): --- Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp index 66c95e190f791..fa9ca50f30f33 100644 --- a/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp +++ b/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp @@ -227,6 +227,10 @@ void ModuleRegistryEntry::setEvaluationError(JSGlobalObject* globalObject, JSVal void ModuleRegistryEntry::setStatus(Status status) { m_status = status; +#if USE(BUN_JSC_ADDITIONS) + if (status != Status::New && status != Status::Fetching) + m_fetchSource.clear(); +#endif } void ModuleRegistryEntry::provideFetch(JSGlobalObject* globalObject, SourceCode&& sourceCode) @@ -256,6 +260,7 @@ void ModuleRegistryEntry::provideModule(VM& vm, AbstractModuleRecord* record) ASSERT(m_status == Status::New || m_status == Status::Fetching); m_record.set(vm, this, record); m_status = Status::Fetched; + m_fetchSource.clear(); // Promises some in-flight load already created for this entry are settled now (with the record standing in for // the source: moduleLoadTopSettled has nothing to provide for it); their pending reactions see the settled module // promise and bail. @@ -288,6 +293,7 @@ bool ModuleRegistryEntry::takeSettledFetchSource(VM& vm) JSPromise* source = deliveredFetch(); if (!source) return false; + m_fetchSource.clear(); // m_fetchPromise was pipeFrom()'d, so only the unguarded forms settle it. The pipe's job finds it settled later // and does nothing. if (source->status() == JSPromise::Status::Fulfilled)