[browser][coreCLR] Fix download() → create() lifecycle for WASM loader#127383
[browser][coreCLR] Fix download() → create() lifecycle for WASM loader#127383pavelsavara wants to merge 1 commit intodotnet:mainfrom
download() → create() lifecycle for WASM loader#127383Conversation
There was a problem hiding this comment.
Pull request overview
Restructures the WASM loader lifecycle to support a two-phase download() → create() flow (including an HTTP-cache-only prefetch mode), and extends coverage to prevent regressions.
Changes:
- Adds loader state tracking so
download()can be followed bycreate()without re-running one-time initialization. - Introduces
download(httpCacheOnly?: boolean)and implements HTTP-cache-only prefetching for boot assets (plus<link rel="prefetch">hints for JS modules). - Extends/introduces WASM build tests and enables the previously-disabled CoreCLR scenario.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/native/libs/Common/JavaScript/types/public-api.ts | Updates the public download(httpCacheOnly?: boolean) API signature and docs. |
| src/native/libs/Common/JavaScript/loader/run.ts | Implements the new lifecycle/state machine and cache-only download path. |
| src/native/libs/Common/JavaScript/loader/host-builder.ts | Wires download(httpCacheOnly?: boolean) through to createRuntime. |
| src/native/libs/Common/JavaScript/loader/dotnet.d.ts | Updates TypeScript declarations for the new download signature. |
| src/native/libs/Common/JavaScript/loader/assets.ts | Adds HTTP-cache-only prefetch implementation and JS module prefetch link hints. |
| src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js | Adds browser test scenarios for download()→create() and download(true)→create(). |
| src/mono/wasm/Wasm.Build.Tests/DownloadThenInitTests.cs | Extends existing test and adds a new test validating the cache-only mode. |
| eng/testing/scenarios/BuildWasmAppsJobsListCoreCLR.txt | Enables DownloadThenInitTests for CoreCLR scenario runs. |
| for (const mod of modules) { | ||
| if (!mod.resolvedUrl && mod.name) { | ||
| mod.resolvedUrl = locateFile(mod.name, true); | ||
| } | ||
| if (mod.resolvedUrl) { | ||
| const link = globalThis.document.createElement("link"); | ||
| link.rel = "prefetch"; | ||
| link.href = mod.resolvedUrl; | ||
| link.as = "script"; | ||
| globalThis.document.head.appendChild(link); |
There was a problem hiding this comment.
prefetchJSModuleLinks assumes document/document.head exist whenever ENVIRONMENT_IS_WEB is true, but ENVIRONMENT_IS_WEB also includes Web Workers where document is undefined. This will throw during download() in worker/sidecar scenarios. Add a stricter guard (e.g., check globalThis.document?.head before creating/appending links).
| for (const mod of modules) { | |
| if (!mod.resolvedUrl && mod.name) { | |
| mod.resolvedUrl = locateFile(mod.name, true); | |
| } | |
| if (mod.resolvedUrl) { | |
| const link = globalThis.document.createElement("link"); | |
| link.rel = "prefetch"; | |
| link.href = mod.resolvedUrl; | |
| link.as = "script"; | |
| globalThis.document.head.appendChild(link); | |
| const document = globalThis.document; | |
| const documentHead = document?.head; | |
| if (!document || !documentHead) return; | |
| for (const mod of modules) { | |
| if (!mod.resolvedUrl && mod.name) { | |
| mod.resolvedUrl = locateFile(mod.name, true); | |
| } | |
| if (mod.resolvedUrl) { | |
| const link = document.createElement("link"); | |
| link.rel = "prefetch"; | |
| link.href = mod.resolvedUrl; | |
| link.as = "script"; | |
| documentHead.appendChild(link); |
| async function prefetchUrl(url: string, hash?: string | null | "", name?: string, behavior?: string): Promise<void> { | ||
| // Respect loadBootResourceCallback so prefetch goes through the same custom loader as create() | ||
| if (typeof loadBootResourceCallback === "function" && behavior && name) { | ||
| const blazorType = behaviorToBlazorAssetTypeMap[behavior]; | ||
| if (blazorType) { | ||
| const customLoadResult = loadBootResourceCallback(blazorType, name, url, hash ?? "", behavior as AssetBehaviors); | ||
| if (typeof customLoadResult === "string") { | ||
| url = makeURLAbsoluteWithApplicationBase(customLoadResult); | ||
| } else if (typeof customLoadResult === "object") { | ||
| // Custom loader returned a Response promise — await and discard | ||
| const response = await (customLoadResult as Promise<Response>); | ||
| await response.arrayBuffer(); | ||
| return; | ||
| } | ||
| } |
There was a problem hiding this comment.
prefetchUrl treats any non-string object result from loadBootResourceCallback as a Promise<Response>, but the callback contract allows null/undefined (default handling) and can also return non-Response objects (e.g., BootModule). With null, this path will skip the default fetch and then fail when calling arrayBuffer(), silently disabling prefetch. Tighten the check (exclude null/undefined) and validate the awaited value is Response-like (has arrayBuffer) before consuming; otherwise fall back to the default fetch.
| const maxParallel = loaderConfig.maxParallelDownloads || 1000000; | ||
| const pending: Promise<void>[] = []; | ||
| const queue: { url: string; hash?: string | null | ""; name: string; behavior: string }[] = []; | ||
|
|
||
| function enqueueAsset(asset: { name?: string; resolvedUrl?: string; hash?: string | null | "" }, behavior: string): void { | ||
| if (!asset.resolvedUrl && asset.name) { | ||
| asset.resolvedUrl = locateFile(asset.name); | ||
| } | ||
| if (asset.resolvedUrl && asset.name) { | ||
| queue.push({ url: asset.resolvedUrl, hash: asset.hash, name: asset.name, behavior }); | ||
| } |
There was a problem hiding this comment.
prefetchAllResources stores behavior as a plain string and then casts to AssetBehaviors later. Since you already import AssetBehaviors, type the queue/parameters as AssetBehaviors to prevent accidental unsupported behavior strings from compiling and to avoid the cast.
| // Calling download() again is a noop | ||
| if (downloadOnly && downloadStarted) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
downloadStarted is used as a simple boolean guard, so a second download() call returns immediately (without awaiting the first call’s work) and it also prevents switching from download(true) (HTTP-cache-only) to download(false) (download into WASM memory). Consider tracking the in-flight download promise and the completed mode (e.g., none/cacheOnly/intoMemory) so subsequent calls can await completion and/or upgrade modes when requested.
Fixes #124896
The
download()API was broken — callingdownload()followed bycreate()failed because the native module couldn't be re-initialized. This PR restructures thecreateRuntime()flow to properly support the two-phase download→create lifecycle.Changes
Loader core (
run.ts)downloadStarted,downloadedIntoMemory,configInitialized) sodownload()andcreate()can be called sequentially without re-running initialization.onConfigLoaded,modulesAfterConfigLoaded, polyfills, console wiring) now runs exactly once regardless of how many timescreateRuntimeis entered.download()already loaded assets into memory,create()takes a fast path: initialize CoreCLR, fireonDotnetReady, loadmodulesAfterRuntimeReady— skipping redundant downloads.download()calls are a no-op.HTTP-cache-only prefetch mode (
assets.ts)download(httpCacheOnly: true)parameter. When set, resources are fetched into the browser HTTP cache and discarded — no WASM memory is consumed. A subsequentcreate()will re-fetch from cache.prefetchAllResources()enqueues all data assets (assemblies, ICU, VFS, WASM, PDBs, symbols, satellites) and fetches them with throttling (maxParallelDownloads).prefetchJSModuleLinks()injects<link rel="prefetch">hints for JS modules instead of fetching them, since they useimport().prefetchUrl()respectsloadBootResourceCallback, integrity checks, anddisableNoCacheFetch.Public API (
public-api.ts,dotnet.d.ts,host-builder.ts)download()signature updated todownload(httpCacheOnly?: boolean).Tests
NoResourcesReFetchedAfterDownloadFinishedto verifyonConfigLoadedfires duringdownload(), resources are fetched during download, none re-fetched duringcreate(), andcreate()completes.HttpCacheOnlyThenCreateWorkstest coveringdownload(true)→create()withloadBootResourcecallback verification.DownloadThenInitHttpCacheOnlyinmain.js.DownloadThenInitTestsfor CoreCLR inBuildWasmAppsJobsListCoreCLR.txt.