Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 67 additions & 22 deletions Source/JavaScriptCore/runtime/JSModuleLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment thread
robobun marked this conversation as resolved.
Resolved
#endif
}
}

Expand Down Expand Up @@ -706,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<ModuleLoaderPayload>())
return entryLoadPromise;
JSPromise* fulfilled = JSPromise::create(vm, globalObject->promiseStructure());
fulfilled->markAsHandled();
fulfilled->fulfill(vm, loaded);
return fulfilled;
};
Comment thread
robobun marked this conversation as resolved.
Resolved
#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,
Expand All @@ -726,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
}
}

Expand Down Expand Up @@ -810,19 +839,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
Expand Down Expand Up @@ -854,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());
Expand All @@ -881,6 +918,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);
Expand Down Expand Up @@ -981,20 +1021,25 @@ 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);
return;
}
// 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.
Expand Down
25 changes: 25 additions & 0 deletions Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Comment thread
robobun marked this conversation as resolved.
Resolved
}

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) {
Expand All @@ -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
8 changes: 8 additions & 0 deletions Source/JavaScriptCore/runtime/ModuleRegistryEntry.h
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -123,6 +130,7 @@ class ModuleRegistryEntry final : public JSCell {
Status m_status { Status::New };
#if USE(BUN_JSC_ADDITIONS)
bool m_isLoaded { false };
WriteBarrier<JSPromise> m_fetchSource;
#endif
};

Expand Down
Loading