11// Licensed to the .NET Foundation under one or more agreements.
22// The .NET Foundation licenses this file to you under the MIT license.
33
4- import type { JsModuleExports , JsAsset , AssemblyAsset , WasmAsset , IcuAsset , EmscriptenModuleInternal , InstantiateWasmSuccessCallback , WebAssemblyBootResourceType , AssetEntryInternal , LoadBootResourceCallback } from "./types" ;
4+ import type { JsModuleExports , JsAsset , AssemblyAsset , WasmAsset , IcuAsset , EmscriptenModuleInternal , InstantiateWasmSuccessCallback , WebAssemblyBootResourceType , AssetEntryInternal , PromiseCompletionSource , LoadBootResourceCallback } from "./types" ;
55
6- import { dotnetAssert , dotnetGetInternals , dotnetBrowserHostExports , dotnetUpdateInternals , dotnetLogger } from "./cross-module" ;
7- import { ENVIRONMENT_IS_WEB } from "./per-module" ;
8- import { createPromiseCompletionSource } from "./promise-completion-source" ;
6+ import { dotnetAssert , dotnetLogger , dotnetInternals , dotnetBrowserHostExports , dotnetUpdateInternals , Module } from "./cross-module" ;
7+ import { ENVIRONMENT_IS_WEB , ENVIRONMENT_IS_SHELL , ENVIRONMENT_IS_NODE } from "./per-module" ;
8+ import { createPromiseCompletionSource , delay } from "./promise-completion-source" ;
99import { locateFile , makeURLAbsoluteWithApplicationBase } from "./bootstrap" ;
1010import { fetchLike } from "./polyfills" ;
1111import { loaderConfig } from "./config" ;
1212
13+ let throttlingPCS : PromiseCompletionSource < void > | undefined ;
14+ let currentParallelDownloads = 0 ;
15+ let downloadedAssetsCount = 0 ;
16+ let totalAssetsToDownload = 0 ;
1317let loadBootResourceCallback : LoadBootResourceCallback | undefined = undefined ;
1418
1519export function setLoadBootResourceCallback ( callback : LoadBootResourceCallback | undefined ) : void {
@@ -18,28 +22,39 @@ export function setLoadBootResourceCallback(callback: LoadBootResourceCallback |
1822
1923export let wasmBinaryPromise : Promise < Response > | undefined = undefined ;
2024export const nativeModulePromiseController = createPromiseCompletionSource < EmscriptenModuleInternal > ( ( ) => {
21- dotnetUpdateInternals ( dotnetGetInternals ( ) ) ;
25+ dotnetUpdateInternals ( dotnetInternals ) ;
2226} ) ;
2327
24- export async function loadJSModule ( asset : JsAsset ) : Promise < JsModuleExports > {
25- const assetInternal = asset as AssetEntryInternal ;
26- if ( assetInternal . name && ! asset . resolvedUrl ) {
27- asset . resolvedUrl = locateFile ( assetInternal . name , true ) ;
28- }
29- assetInternal . behavior = "js-module-dotnet" ;
30- if ( typeof loadBootResourceCallback === "function" ) {
31- const type = runtimeToBlazorAssetTypeMap [ assetInternal . behavior ] ;
32- dotnetAssert . check ( type , `Unsupported asset behavior: ${ assetInternal . behavior } ` ) ;
33- const customLoadResult = loadBootResourceCallback ( type , assetInternal . name , asset . resolvedUrl ! , assetInternal . integrity ! , assetInternal . behavior ) ;
34- dotnetAssert . check ( typeof customLoadResult === "string" , "loadBootResourceCallback for JS modules must return string URL" ) ;
35- asset . resolvedUrl = makeURLAbsoluteWithApplicationBase ( customLoadResult ) ;
36- }
28+ export async function loadDotnetModule ( asset : JsAsset ) : Promise < JsModuleExports > {
29+ return loadJSModule ( asset ) ;
30+ }
3731
38- if ( ! asset . resolvedUrl ) throw new Error ( "Invalid config, resources is not set" ) ;
39- return await import ( /* webpackIgnore: true */ asset . resolvedUrl ) ;
32+ export async function loadJSModule ( asset : JsAsset ) : Promise < any > {
33+ let mod : JsModuleExports = asset . moduleExports ;
34+ totalAssetsToDownload ++ ;
35+ if ( ! mod ) {
36+ const assetInternal = asset as AssetEntryInternal ;
37+ if ( assetInternal . name && ! asset . resolvedUrl ) {
38+ asset . resolvedUrl = locateFile ( assetInternal . name , true ) ;
39+ }
40+ assetInternal . behavior = "js-module-dotnet" ;
41+ if ( typeof loadBootResourceCallback === "function" ) {
42+ const type = runtimeToBlazorAssetTypeMap [ assetInternal . behavior ] ;
43+ dotnetAssert . check ( type , `Unsupported asset behavior: ${ assetInternal . behavior } ` ) ;
44+ const customLoadResult = loadBootResourceCallback ( type , assetInternal . name , asset . resolvedUrl ! , assetInternal . integrity ! , assetInternal . behavior ) ;
45+ dotnetAssert . check ( typeof customLoadResult === "string" , "loadBootResourceCallback for JS modules must return string URL" ) ;
46+ asset . resolvedUrl = makeURLAbsoluteWithApplicationBase ( customLoadResult ) ;
47+ }
48+
49+ if ( ! asset . resolvedUrl ) throw new Error ( "Invalid config, resources is not set" ) ;
50+ mod = await import ( /* webpackIgnore: true */ asset . resolvedUrl ) ;
51+ }
52+ onDownloadedAsset ( ) ;
53+ return mod ;
4054}
4155
4256export function fetchWasm ( asset : WasmAsset ) : Promise < Response > {
57+ totalAssetsToDownload ++ ;
4358 const assetInternal = asset as AssetEntryInternal ;
4459 if ( assetInternal . name && ! asset . resolvedUrl ) {
4560 asset . resolvedUrl = locateFile ( assetInternal . name ) ;
@@ -56,10 +71,12 @@ export async function instantiateWasm(imports: WebAssembly.Imports, successCallb
5671 const data = await res . arrayBuffer ( ) ;
5772 const module = await WebAssembly . compile ( data ) ;
5873 const instance = await WebAssembly . instantiate ( module , imports ) ;
74+ onDownloadedAsset ( ) ;
5975 successCallback ( instance , module ) ;
6076 } else {
6177 const instantiated = await WebAssembly . instantiateStreaming ( wasmBinaryPromise , imports ) ;
6278 await checkResponseOk ( ) ;
79+ onDownloadedAsset ( ) ;
6380 successCallback ( instantiated . instance , instantiated . module ) ;
6481 }
6582
@@ -78,17 +95,22 @@ export async function instantiateWasm(imports: WebAssembly.Imports, successCallb
7895}
7996
8097export async function fetchIcu ( asset : IcuAsset ) : Promise < void > {
98+ totalAssetsToDownload ++ ;
8199 const assetInternal = asset as AssetEntryInternal ;
82100 if ( assetInternal . name && ! asset . resolvedUrl ) {
83101 asset . resolvedUrl = locateFile ( assetInternal . name ) ;
84102 }
85103 assetInternal . behavior = "icu" ;
86104 const bytes = await fetchBytes ( assetInternal ) ;
87105 await nativeModulePromiseController . promise ;
88- dotnetBrowserHostExports . loadIcuData ( bytes ) ;
106+ onDownloadedAsset ( ) ;
107+ if ( bytes ) {
108+ dotnetBrowserHostExports . loadIcuData ( bytes ) ;
109+ }
89110}
90111
91112export async function fetchDll ( asset : AssemblyAsset ) : Promise < void > {
113+ totalAssetsToDownload ++ ;
92114 const assetInternal = asset as AssetEntryInternal ;
93115 if ( assetInternal . name && ! asset . resolvedUrl ) {
94116 asset . resolvedUrl = locateFile ( assetInternal . name ) ;
@@ -97,38 +119,176 @@ export async function fetchDll(asset: AssemblyAsset): Promise<void> {
97119 const bytes = await fetchBytes ( assetInternal ) ;
98120 await nativeModulePromiseController . promise ;
99121
100- dotnetBrowserHostExports . registerDllBytes ( bytes , asset ) ;
122+ onDownloadedAsset ( ) ;
123+ if ( bytes ) {
124+ dotnetBrowserHostExports . registerDllBytes ( bytes , asset ) ;
125+ }
126+ }
127+
128+ export async function fetchPdb ( asset : AssemblyAsset ) : Promise < void > {
129+ totalAssetsToDownload ++ ;
130+ const assetInternal = asset as AssetEntryInternal ;
131+ if ( assetInternal . name && ! asset . resolvedUrl ) {
132+ asset . resolvedUrl = locateFile ( assetInternal . name ) ;
133+ }
134+ assetInternal . behavior = "pdb" ;
135+ assetInternal . isOptional = assetInternal . isOptional || loaderConfig . ignorePdbLoadErrors ;
136+ const bytes = await fetchBytes ( assetInternal ) ;
137+ await nativeModulePromiseController . promise ;
138+
139+ onDownloadedAsset ( ) ;
140+ if ( bytes ) {
141+ dotnetBrowserHostExports . registerPdbBytes ( bytes , asset ) ;
142+ }
101143}
102144
103145export async function fetchVfs ( asset : AssemblyAsset ) : Promise < void > {
146+ totalAssetsToDownload ++ ;
104147 const assetInternal = asset as AssetEntryInternal ;
105148 if ( assetInternal . name && ! asset . resolvedUrl ) {
106149 asset . resolvedUrl = locateFile ( assetInternal . name ) ;
107150 }
108151 assetInternal . behavior = "vfs" ;
109152 const bytes = await fetchBytes ( assetInternal ) ;
110153 await nativeModulePromiseController . promise ;
111-
112- dotnetBrowserHostExports . installVfsFile ( bytes , asset ) ;
154+ onDownloadedAsset ( ) ;
155+ if ( bytes ) {
156+ dotnetBrowserHostExports . installVfsFile ( bytes , asset ) ;
157+ }
113158}
114159
115- async function fetchBytes ( asset : AssetEntryInternal ) : Promise < Uint8Array > {
160+ async function fetchBytes ( asset : AssetEntryInternal ) : Promise < Uint8Array | null > {
116161 dotnetAssert . check ( asset && asset . resolvedUrl , "Bad asset.resolvedUrl" ) ;
117162 const response = await loadResource ( asset ) ;
118163 if ( ! response . ok ) {
164+ if ( asset . isOptional ) {
165+ dotnetLogger . warn ( `Optional resource '${ asset . name } ' failed to load from '${ asset . resolvedUrl } '. HTTP status: ${ response . status } ${ response . statusText } ` ) ;
166+ return null ;
167+ }
119168 throw new Error ( `Failed to load resource '${ asset . name } ' from '${ asset . resolvedUrl } '. HTTP status: ${ response . status } ${ response . statusText } ` ) ;
120169 }
121- const buffer = await response . arrayBuffer ( ) ;
170+ const buffer = await ( asset . buffer || response . arrayBuffer ( ) ) ;
122171 return new Uint8Array ( buffer ) ;
123172}
124173
125- async function loadResource ( asset : AssetEntryInternal ) : Promise < Response > {
174+ function loadResource ( asset : AssetEntryInternal ) : Promise < Response > {
175+ if ( "dotnetwasm" === asset . behavior ) {
176+ // `response.arrayBuffer()` can't be called twice.
177+ return loadResourceFetch ( asset ) ;
178+ }
179+ if ( ENVIRONMENT_IS_SHELL || ENVIRONMENT_IS_NODE || asset . resolvedUrl && asset . resolvedUrl . indexOf ( "file://" ) !== - 1 ) {
180+ // no need to retry or throttle local file access
181+ return loadResourceFetch ( asset ) ;
182+ }
183+ if ( ! loaderConfig . enableDownloadRetry ) {
184+ // only throttle, no retry
185+ return loadResourceThrottle ( asset ) ;
186+ }
187+ // retry and throttle
188+ return loadResourceRetry ( asset ) ;
189+ }
190+
191+ const noRetryStatusCodes = new Set < number > ( [ 400 , 401 , 403 , 404 , 405 , 406 , 409 , 410 , 411 , 413 , 414 , 415 , 422 , 426 , 501 , 505 , ] ) ;
192+ async function loadResourceRetry ( asset : AssetEntryInternal ) : Promise < Response > {
193+ let response : Response ;
194+ response = await loadResourceAttempt ( ) ;
195+ if ( response . ok || asset . isOptional || noRetryStatusCodes . has ( response . status ) ) {
196+ return response ;
197+ }
198+ if ( response . status === 429 ) {
199+ // Too Many Requests
200+ await delay ( 100 ) ;
201+ }
202+ response = await loadResourceAttempt ( ) ;
203+ if ( response . ok || noRetryStatusCodes . has ( response . status ) ) {
204+ return response ;
205+ }
206+ await delay ( 100 ) ; // wait 100ms before the last retry
207+ response = await loadResourceAttempt ( ) ;
208+ if ( response . ok ) {
209+ return response ;
210+ }
211+ throw new Error ( `Failed to load resource '${ asset . name } ' from '${ asset . resolvedUrl } ' after multiple attempts. Last HTTP status: ${ response . status } ${ response . statusText } ` ) ;
212+
213+ async function loadResourceAttempt ( ) : Promise < Response > {
214+ let response : Response ;
215+ try {
216+ response = await loadResourceThrottle ( asset ) ;
217+ if ( ! response ) {
218+ response = {
219+ ok : false ,
220+ status : - 1 ,
221+ statusText : "No response" ,
222+ } as any ;
223+ }
224+ } catch ( err : any ) {
225+ response = {
226+ ok : false ,
227+ status : - 1 ,
228+ statusText : err . message || "Exception during fetch" ,
229+ } as any ;
230+ }
231+ return response ;
232+ }
233+ }
234+
235+ // in order to prevent net::ERR_INSUFFICIENT_RESOURCES if we start downloading too many files at same time on a device with low resources
236+ async function loadResourceThrottle ( asset : AssetEntryInternal ) : Promise < Response > {
237+ while ( throttlingPCS ) {
238+ await throttlingPCS . promise ;
239+ }
240+ try {
241+ ++ currentParallelDownloads ;
242+ if ( currentParallelDownloads === loaderConfig . maxParallelDownloads ) {
243+ dotnetLogger . debug ( "Throttling further parallel downloads" ) ;
244+ throttlingPCS = createPromiseCompletionSource < void > ( ) ;
245+ }
246+ const responsePromise = loadResourceFetch ( asset ) ;
247+ const response = await responsePromise ;
248+ dotnetAssert . check ( response , "Bad response in loadResourceThrottle" ) ;
249+
250+ asset . buffer = await response . arrayBuffer ( ) ;
251+ return response ;
252+ } finally {
253+ -- currentParallelDownloads ;
254+ if ( throttlingPCS && currentParallelDownloads == loaderConfig . maxParallelDownloads ! - 1 ) {
255+ dotnetLogger . debug ( "Resuming more parallel downloads" ) ;
256+ const oldThrottlingPCS = throttlingPCS ;
257+ throttlingPCS = undefined ;
258+ oldThrottlingPCS . resolve ( ) ;
259+ }
260+ }
261+ }
262+
263+ async function loadResourceFetch ( asset : AssetEntryInternal ) : Promise < Response > {
264+ if ( asset . buffer ) {
265+ return < Response > < any > {
266+ ok : true ,
267+ headers : {
268+ length : 0 ,
269+ get : ( ) => null
270+ } ,
271+ url : asset . resolvedUrl ,
272+ arrayBuffer : ( ) => Promise . resolve ( asset . buffer ! ) ,
273+ json : ( ) => {
274+ throw new Error ( "NotImplementedException" ) ;
275+ } ,
276+ text : ( ) => {
277+ throw new Error ( "NotImplementedException" ) ;
278+ }
279+ } ;
280+ }
281+ if ( asset . pendingDownload ) {
282+ return asset . pendingDownload . response ;
283+ }
126284 if ( typeof loadBootResourceCallback === "function" ) {
127285 const type = runtimeToBlazorAssetTypeMap [ asset . behavior ] ;
128286 dotnetAssert . check ( type , `Unsupported asset behavior: ${ asset . behavior } ` ) ;
129287 const customLoadResult = loadBootResourceCallback ( type , asset . name , asset . resolvedUrl ! , asset . integrity ! , asset . behavior ) ;
130288 if ( typeof customLoadResult === "string" ) {
131289 asset . resolvedUrl = makeURLAbsoluteWithApplicationBase ( customLoadResult ) ;
290+ } else if ( typeof customLoadResult === "object" ) {
291+ return customLoadResult as any ;
132292 }
133293 }
134294 dotnetAssert . check ( asset . resolvedUrl , "Bad asset.resolvedUrl" ) ;
@@ -157,6 +317,17 @@ async function loadResource(asset: AssetEntryInternal): Promise<Response> {
157317 return fetchLike ( asset . resolvedUrl ! , fetchOptions ) ;
158318}
159319
320+ function onDownloadedAsset ( ) {
321+ ++ downloadedAssetsCount ;
322+ if ( Module . onDownloadResourceProgress ) {
323+ Module . onDownloadResourceProgress ( downloadedAssetsCount , totalAssetsToDownload ) ;
324+ }
325+ }
326+
327+ export function verifyAllAssetsDownloaded ( ) : void {
328+ dotnetAssert . check ( downloadedAssetsCount === totalAssetsToDownload , `Not all assets were downloaded. Downloaded ${ downloadedAssetsCount } out of ${ totalAssetsToDownload } ` ) ;
329+ }
330+
160331const runtimeToBlazorAssetTypeMap : { [ key : string ] : WebAssemblyBootResourceType | undefined } = {
161332 "resource" : "assembly" ,
162333 "assembly" : "assembly" ,
0 commit comments