</PropertyGroup>
<ItemGroup>
<WasmExtraFilesToDeploy Include="main.js" />
+ <WasmExtraFilesToDeploy Include="advanced-sample.lib.module.js" />
<!-- add export GL object from Module -->
<EmccExportedRuntimeMethod Include="GL" />
<NativeFileReference Include="fibonacci.c" />
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+export function onRuntimeConfigLoaded(config) {
+ console.log("advanced-sample onRuntimeConfigLoaded")
+}
+
+export async function onRuntimeReady({ getAssemblyExports, getConfig }) {
+ console.log("advanced-sample onRuntimeReady")
+}
\ No newline at end of file
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'wasm-unsafe-eval';" />
<script type='module' src="./main.js"></script>
- <script type='module' src="./_framework/dotnet.js"></script>
- <link rel="preload" href="./_framework/blazor.boot.json" as="fetch" crossorigin="anonymous">
+ <script type='module' src="./dotnet.js"></script>
+ <link rel="preload" href="./blazor.boot.json" as="fetch" crossorigin="use-credentials">
<link rel="prefetch" href="./dotnet.native.js" as="fetch" crossorigin="anonymous">
<link rel="prefetch" href="./dotnet.runtime.js" as="fetch" crossorigin="anonymous">
<link rel="prefetch" href="./dotnet.native.wasm" as="fetch" crossorigin="anonymous">
<!-- users should consider if they optimize for the first load or subsequent load from memory snapshot -->
<link rel="prefetch" href="./icudt.dat" as="fetch" crossorigin="anonymous">
- <link rel="prefetch" href="./managed/System.Private.CoreLib.wasm" as="fetch" crossorigin="anonymous">
+ <link rel="prefetch" href="./System.Private.CoreLib.wasm" as="fetch" crossorigin="anonymous">
</head>
<body>
// config is loaded and could be tweaked before the rest of the runtime startup sequence
config.environmentVariables["MONO_LOG_LEVEL"] = "debug";
config.browserProfilerOptions = {};
+ config.resources.modulesAfterConfigLoaded = {
+ "advanced-sample.lib.module.js": ""
+ }
},
preInit: () => { console.log('user code Module.preInit'); },
preRun: () => { console.log('user code Module.preRun'); },
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="module" src="./frame-main.js"></script>
<script type='module' src="./_framework/dotnet.js"></script>
- <link rel="preload" href="./_framework/blazor.boot.json" as="fetch" crossorigin="anonymous">
- <link rel="prefetch" href="./dotnet.native.js" as="fetch" crossorigin="anonymous">
- <link rel="prefetch" href="./dotnet.runtime.js" as="fetch" crossorigin="anonymous">
- <link rel="prefetch" href="./dotnet.native.wasm" as="fetch" crossorigin="anonymous">
+ <link rel="preload" href="./_framework/blazor.boot.json" as="fetch" crossorigin="use-credentials">
+ <link rel="prefetch" href="./_framework/dotnet.native.js" as="fetch" crossorigin="anonymous">
+ <link rel="prefetch" href="./_framework/dotnet.runtime.js" as="fetch" crossorigin="anonymous">
+ <link rel="prefetch" href="./_framework/dotnet.native.wasm" as="fetch" crossorigin="anonymous">
<!-- users should consider if they optimize for the first load or subsequent load from memory snapshot -->
- <link rel="prefetch" href="./managed/System.Private.CoreLib.dll" as="fetch" crossorigin="anonymous">
+ <link rel="prefetch" href="./_framework/System.Private.CoreLib.dll" as="fetch" crossorigin="anonymous">
</head>
<body>
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using System;
+using System.Security.Cryptography;
namespace HttpServer
{
public int Finished { get; set; } = 0;
}
+ public sealed record FileContent(byte[] buffer, string hash);
+
public sealed class Program
{
private bool Verbose = false;
private ConcurrentDictionary<string, Session> Sessions = new ConcurrentDictionary<string, Session>();
- private Dictionary<string, byte[]> cache = new Dictionary<string, byte[]>(StringComparer.OrdinalIgnoreCase);
+ private Dictionary<string, FileContent> cache = new(StringComparer.OrdinalIgnoreCase);
public static int Main()
{
ReceivePostAsync(context);
}
- private async Task<byte[]?> GetFileContent(string path)
+ private async Task<FileContent?> GetFileContent(string path)
{
if (Verbose)
await Console.Out.WriteLineAsync($"get content for: {path}");
if (Verbose)
await Console.Out.WriteLineAsync($"adding content to cache for: {path}");
- cache[path] = content;
+ using HashAlgorithm hashAlgorithm = SHA256.Create();
+ byte[] hash = hashAlgorithm.ComputeHash(content);
+ var fc = new FileContent(content, "sha256-" + Convert.ToBase64String(hash));
+
+ cache[path] = fc;
- return content;
+ return fc;
}
private async void ReceivePostAsync(HttpListenerContext context)
else if (path.StartsWith("/"))
path = path.Substring(1);
- byte[]? buffer;
+ FileContent? fc;
try
{
- buffer = await GetFileContent(path);
+ fc = await GetFileContent(path);
- if (buffer != null && throttleMbps > 0)
+ if (fc != null && throttleMbps > 0)
{
- double delaySeconds = (buffer.Length * 8) / (throttleMbps * 1024 * 1024);
+ double delaySeconds = (fc.buffer.Length * 8) / (throttleMbps * 1024 * 1024);
int delayMs = (int)(delaySeconds * 1000);
if (session != null)
{
}
}
}
- catch (Exception)
+ catch (System.IO.DirectoryNotFoundException)
+ {
+ if (Verbose)
+ Console.WriteLine($"Not found: {path}");
+ fc = null;
+ }
+ catch (Exception ex)
{
- buffer = null;
+ if (Verbose)
+ Console.WriteLine($"Exception: {ex}");
+ fc = null;
}
- if (buffer != null)
+ if (fc != null)
{
string? contentType = null;
if (path.EndsWith(".wasm"))
{
Console.WriteLine("Faking 500 " + url);
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
- await stream.WriteAsync(buffer, 0, 0).ConfigureAwait(false);
+ await stream.WriteAsync(fc.buffer, 0, 0).ConfigureAwait(false);
await stream.FlushAsync();
context.Response.Close();
return;
if (contentType != null)
context.Response.ContentType = contentType;
- context.Response.ContentLength64 = buffer.Length;
- context.Response.AppendHeader("cache-control", "public, max-age=31536000");
+ // context.Response.AppendHeader("cache-control", "public, max-age=31536000");
context.Response.AppendHeader("Cross-Origin-Embedder-Policy", "require-corp");
context.Response.AppendHeader("Cross-Origin-Opener-Policy", "same-origin");
+ context.Response.AppendHeader("ETag", fc.hash);
// test download re-try
if (url.Query.Contains("testAbort"))
{
Console.WriteLine("Faking abort " + url);
- await stream.WriteAsync(buffer, 0, 10).ConfigureAwait(false);
+ context.Response.ContentLength64 = fc.buffer.Length;
+ await stream.WriteAsync(fc.buffer, 0, 10).ConfigureAwait(false);
await stream.FlushAsync();
await Task.Delay(100);
context.Response.Abort();
return;
}
+ var ifNoneMatch = context.Request.Headers.Get("If-None-Match");
+ if (ifNoneMatch == fc.hash)
+ {
+ context.Response.StatusCode = 304;
+ await stream.FlushAsync();
+ stream.Close();
+ context.Response.Close();
+ return;
+ }
try
{
- await stream.WriteAsync(buffer).ConfigureAwait(false);
+ context.Response.ContentLength64 = fc.buffer.Length;
+ await stream.WriteAsync(fc.buffer).ConfigureAwait(false);
}
catch (Exception e)
{
In order to start downloading application resources as soon as possible you can add HTML elements to `<head>` of your page similar to:
```html
-<link rel="preload" href="./_framework/blazor.boot.json" as="fetch" crossorigin="anonymous">
+<link rel="preload" href="./_framework/blazor.boot.json" as="fetch" crossorigin="use-credentials">
<link rel="prefetch" href="./_framework/dotnet.native.js" as="fetch" crossorigin="anonymous">
<link rel="prefetch" href="./_framework/dotnet.runtime.js" as="fetch" crossorigin="anonymous">
<link rel="prefetch" href="./_framework/dotnet.native.wasm" as="fetch" crossorigin="anonymous">
*/
cacheBootResources?: boolean;
/**
+ * Configures use of the `integrity` directive for fetching assets
+ */
+ disableIntegrityCheck?: boolean;
+ /**
+ * Configures use of the `no-cache` directive for fetching assets
+ */
+ disableNoCacheFetch?: boolean;
+ /**
* Enables diagnostic log messages during startup
*/
diagnosticTracing?: boolean;
* When returned string is not qualified with `./` or absolute URL, it will be resolved against the application base URI.
*/
type LoadBootResourceCallback = (type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string, behavior: AssetBehaviors) => string | Promise<Response> | null | undefined;
-interface ResourceRequest {
- name: string;
- behavior: AssetBehaviors;
- resolvedUrl?: string;
- hash?: string | null | "";
-}
interface LoadingResource {
name: string;
url: string;
response: Promise<Response>;
}
-interface AssetEntry extends ResourceRequest {
+interface AssetEntry {
+ /**
+ * the name of the asset, including extension.
+ */
+ name: string;
+ /**
+ * determines how the asset will be handled once loaded
+ */
+ behavior: AssetBehaviors;
+ /**
+ * this should be absolute url to the asset
+ */
+ resolvedUrl?: string;
+ /**
+ * the integrity hash of the asset (if any)
+ */
+ hash?: string | null | "";
/**
* If specified, overrides the path of the asset in the virtual filesystem and similar data structures once downloaded.
*/
}
declare const createDotnetRuntime: CreateDotnetRuntimeType;
-export { AssetBehaviors, AssetEntry, CreateDotnetRuntimeType, DotnetHostBuilder, DotnetModuleConfig, EmscriptenModule, GlobalizationMode, IMemoryView, ModuleAPI, MonoConfig, ResourceRequest, RuntimeAPI, createDotnetRuntime as default, dotnet, exit };
+export { AssetBehaviors, AssetEntry, CreateDotnetRuntimeType, DotnetHostBuilder, DotnetModuleConfig, EmscriptenModule, GlobalizationMode, IMemoryView, ModuleAPI, MonoConfig, RuntimeAPI, createDotnetRuntime as default, dotnet, exit };
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+import MonoWasmThreads from "consts:monoWasmThreads";
+
import type { AssetEntryInternal, PromiseAndController } from "../types/internal";
-import type { AssetBehaviors, AssetEntry, LoadingResource, ResourceList, ResourceRequest, SingleAssetBehaviors as SingleAssetBehaviors, WebAssemblyBootResourceType } from "../types";
+import type { AssetBehaviors, AssetEntry, LoadingResource, ResourceList, SingleAssetBehaviors as SingleAssetBehaviors, WebAssemblyBootResourceType } from "../types";
import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL, loaderHelpers, mono_assert, runtimeHelpers } from "./globals";
import { createPromiseController } from "./promise-controller";
import { mono_log_debug } from "./logging";
import { mono_exit } from "./exit";
-import { addCachedReponse, findCachedResponse, isCacheAvailable } from "./assetsCache";
+import { addCachedReponse, findCachedResponse } from "./assetsCache";
import { getIcuResourceName } from "./icu";
-import { mono_log_warn } from "./logging";
import { makeURLAbsoluteWithApplicationBase } from "./polyfills";
let throttlingPromise: PromiseAndController<void> | undefined;
// in order to prevent net::ERR_INSUFFICIENT_RESOURCES if we start downloading too many files at same time
let parallel_count = 0;
+const containedInSnapshotAssets: AssetEntryInternal[] = [];
+const alwaysLoadedAssets: AssetEntryInternal[] = [];
+const singleAssets: Map<string, AssetEntryInternal> = new Map();
-const jsModulesAssetTypes: {
+const jsRuntimeModulesAssetTypes: {
[k: string]: boolean
} = {
"js-module-threads": true,
"js-module-native": true,
};
+const jsModulesAssetTypes: {
+ [k: string]: boolean
+} = {
+ ...jsRuntimeModulesAssetTypes,
+ "js-module-library-initializer": true,
+};
+
+const singleAssetTypes: {
+ [k: string]: boolean
+} = {
+ ...jsRuntimeModulesAssetTypes,
+ "dotnetwasm": true,
+ "heap": true,
+ "manifest": true,
+};
+
+// append query to asset url to prevent reusing state
+const appendQueryAssetTypes: {
+ [k: string]: boolean
+} = {
+ ...jsModulesAssetTypes,
+ "manifest": true,
+};
+
// don't `fetch` javaScript and wasm files
const skipDownloadsByAssetTypes: {
[k: string]: boolean
return !(asset.behavior == "icu" && asset.name != loaderHelpers.preferredIcuAsset);
}
-function getSingleAssetWithResolvedUrl(resources: ResourceList | undefined, behavior: SingleAssetBehaviors): AssetEntry {
- const keys = Object.keys(resources || {});
+function convert_single_asset(modulesAssets: AssetEntryInternal[], resource: ResourceList | undefined, behavior: SingleAssetBehaviors): AssetEntryInternal {
+ const keys = Object.keys(resource || {});
mono_assert(keys.length == 1, `Expect to have one ${behavior} asset in resources`);
const name = keys[0];
+
const asset = {
name,
- hash: resources![name],
+ hash: resource![name],
behavior,
- resolvedUrl: appendUniqueQuery(loaderHelpers.locateFile(name), behavior)
};
- const customSrc = invokeLoadBootResource(asset);
- if (typeof (customSrc) === "string") {
- asset.resolvedUrl = makeURLAbsoluteWithApplicationBase(customSrc);
- } else if (customSrc) {
- mono_log_warn(`For ${behavior} resource: ${name}, custom loaders must supply a URI string.`);
- // we apply a default URL
+ set_single_asset(asset);
+
+ // so that we can use it on the worker too
+ modulesAssets.push(asset);
+ return asset;
+}
+
+function set_single_asset(asset: AssetEntryInternal) {
+ if (singleAssetTypes[asset.behavior]) {
+ singleAssets.set(asset.behavior, asset);
}
+}
+function get_single_asset(behavior: SingleAssetBehaviors): AssetEntryInternal {
+ mono_assert(singleAssetTypes[behavior], `Unknown single asset behavior ${behavior}`);
+ const asset = singleAssets.get(behavior);
+ mono_assert(asset, `Single asset for ${behavior} not found`);
return asset;
}
export function resolve_single_asset_path(behavior: SingleAssetBehaviors): AssetEntryInternal {
- const resources = loaderHelpers.config.resources;
- mono_assert(resources, "Can't find resources in config");
-
- switch (behavior) {
- case "dotnetwasm":
- return getSingleAssetWithResolvedUrl(resources.wasmNative, behavior);
- case "js-module-threads":
- return getSingleAssetWithResolvedUrl(resources.jsModuleWorker, behavior);
- case "js-module-native":
- return getSingleAssetWithResolvedUrl(resources.jsModuleNative, behavior);
- case "js-module-runtime":
- return getSingleAssetWithResolvedUrl(resources.jsModuleRuntime, behavior);
- default:
- throw new Error(`Unknown single asset behavior ${behavior}`);
+ const asset = get_single_asset(behavior);
+ asset.resolvedUrl = loaderHelpers.locateFile(asset.name);
+
+ if (jsRuntimeModulesAssetTypes[asset.behavior]) {
+ // give loadBootResource chance to override the url for JS modules with 'dotnetjs' type
+ const customLoadResult = invokeLoadBootResource(asset);
+ if (customLoadResult) {
+ mono_assert(typeof customLoadResult === "string", "loadBootResource response for 'dotnetjs' type should be a URL string");
+ asset.resolvedUrl = customLoadResult;
+ } else {
+ asset.resolvedUrl = appendUniqueQuery(asset.resolvedUrl, asset.behavior);
+ }
+ } else if (asset.behavior !== "dotnetwasm") {
+ throw new Error(`Unknown single asset behavior ${behavior}`);
}
+ return asset;
}
export async function mono_download_assets(): Promise<void> {
loaderHelpers.maxParallelDownloads = loaderHelpers.config.maxParallelDownloads || loaderHelpers.maxParallelDownloads;
loaderHelpers.enableDownloadRetry = loaderHelpers.config.enableDownloadRetry || loaderHelpers.enableDownloadRetry;
try {
- const alwaysLoadedAssets: AssetEntryInternal[] = [];
- const containedInSnapshotAssets: AssetEntryInternal[] = [];
const promises_of_assets: Promise<AssetEntryInternal>[] = [];
- prepareAssets(containedInSnapshotAssets, alwaysLoadedAssets);
-
const countAndStartDownload = (asset: AssetEntryInternal) => {
if (!skipInstantiateByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) {
loaderHelpers.expected_instantiated_assets_count++;
}
}
-function prepareAssets(containedInSnapshotAssets: AssetEntryInternal[], alwaysLoadedAssets: AssetEntryInternal[]) {
+export function prepareAssets() {
const config = loaderHelpers.config;
+ const modulesAssets: AssetEntryInternal[] = [];
// if assets exits, we will assume Net7 legacy and not process resources object
if (config.assets) {
- for (const a of config.assets) {
- const asset: AssetEntryInternal = a;
+ for (const asset of config.assets) {
mono_assert(typeof asset === "object", () => `asset must be object, it was ${typeof asset} : ${asset}`);
mono_assert(typeof asset.behavior === "string", "asset behavior must be known string");
mono_assert(typeof asset.name === "string", "asset name must be string");
} else {
alwaysLoadedAssets.push(asset);
}
+ set_single_asset(asset);
}
} else if (config.resources) {
const resources = config.resources;
+
+ mono_assert(resources.wasmNative, "resources.wasmNative must be defined");
+ mono_assert(resources.jsModuleNative, "resources.jsModuleNative must be defined");
+ mono_assert(resources.jsModuleRuntime, "resources.jsModuleRuntime must be defined");
+ mono_assert(!MonoWasmThreads || resources.jsModuleWorker, "resources.jsModuleWorker must be defined");
+ convert_single_asset(modulesAssets, resources.wasmNative, "dotnetwasm");
+ convert_single_asset(modulesAssets, resources.jsModuleNative, "js-module-native");
+ convert_single_asset(modulesAssets, resources.jsModuleRuntime, "js-module-runtime");
+ if (MonoWasmThreads) {
+ convert_single_asset(modulesAssets, resources.jsModuleWorker, "js-module-threads");
+ }
+
if (resources.assembly) {
for (const name in resources.assembly) {
containedInSnapshotAssets.push({
}
}
+ // FIXME: should we also load Net7 backward compatible `config.configs` in a same way ?
if (config.appsettings) {
for (let i = 0; i < config.appsettings.length; i++) {
const configUrl = config.appsettings[i];
const configFileName = fileName(configUrl);
if (configFileName === "appsettings.json" || configFileName === `appsettings.${config.applicationEnvironment}.json`) {
alwaysLoadedAssets.push({
- name: configFileName,
- resolvedUrl: appendUniqueQuery(loaderHelpers.locateFile(configUrl), "vfs"),
- behavior: "vfs"
+ name: configUrl,
+ behavior: "vfs",
+ // TODO what should be the virtualPath ?
+ noCache: true,
+ useCredentials: true
});
}
+ // FIXME: why are not loading all the other named files in appsettings ? https://github.com/dotnet/runtime/issues/89861
}
}
- config.assets = [...containedInSnapshotAssets, ...alwaysLoadedAssets];
+ config.assets = [...containedInSnapshotAssets, ...alwaysLoadedAssets, ...modulesAssets];
+}
+
+export function prepareAssetsWorker() {
+ const config = loaderHelpers.config;
+ mono_assert(config.assets, "config.assets must be defined");
+
+ for (const asset of config.assets) {
+ set_single_asset(asset);
+ }
}
export function delay(ms: number): Promise<void> {
export function appendUniqueQuery(attemptUrl: string, behavior: AssetBehaviors): string {
// apply unique query to js modules to make the module state independent of the other runtime instances
- if (loaderHelpers.modulesUniqueQuery && jsModulesAssetTypes[behavior]) {
+ if (loaderHelpers.modulesUniqueQuery && appendQueryAssetTypes[behavior]) {
attemptUrl = attemptUrl + loaderHelpers.modulesUniqueQuery;
}
let resourcesLoaded = 0;
const totalResources = new Set<string>();
-function download_resource(request: ResourceRequest): LoadingResource {
+function download_resource(asset: AssetEntryInternal): LoadingResource {
try {
- mono_assert(request.resolvedUrl, "Request's resolvedUrl must be set");
- const fetchResponse = download_resource_with_cache(request);
- const response = { name: request.name, url: request.resolvedUrl, response: fetchResponse };
+ mono_assert(asset.resolvedUrl, "Request's resolvedUrl must be set");
+ const fetchResponse = download_resource_with_cache(asset);
+ const response = { name: asset.name, url: asset.resolvedUrl, response: fetchResponse };
- totalResources.add(request.name!);
+ totalResources.add(asset.name!);
response.response.then(() => {
- if (request.behavior == "assembly") {
- loaderHelpers.loadedAssemblies.push(request.resolvedUrl!);
+ if (asset.behavior == "assembly") {
+ loaderHelpers.loadedAssemblies.push(asset.resolvedUrl!);
}
resourcesLoaded++;
} catch (err) {
const response = <Response><any>{
ok: false,
- url: request.resolvedUrl,
+ url: asset.resolvedUrl,
status: 500,
statusText: "ERR29: " + err,
arrayBuffer: () => { throw err; },
json: () => { throw err; }
};
return {
- name: request.name, url: request.resolvedUrl!, response: Promise.resolve(response)
+ name: asset.name, url: asset.resolvedUrl!, response: Promise.resolve(response)
};
}
}
-async function download_resource_with_cache(request: ResourceRequest): Promise<Response> {
- let response = await findCachedResponse(request);
+async function download_resource_with_cache(asset: AssetEntryInternal): Promise<Response> {
+ let response = await findCachedResponse(asset);
if (!response) {
- response = await fetchResource(request);
- addCachedReponse(request, response);
+ response = await fetchResource(asset);
+ addCachedReponse(asset, response);
}
return response;
}
-const credentialsIncludeAssetBehaviors: AssetBehaviors[] = ["vfs"]; // Previously only configuration
-
-function fetchResource(request: ResourceRequest): Promise<Response> {
+function fetchResource(asset: AssetEntryInternal): Promise<Response> {
// Allow developers to override how the resource is loaded
- let url = request.resolvedUrl!;
+ let url = asset.resolvedUrl!;
if (loaderHelpers.loadBootResource) {
- const customLoadResult = invokeLoadBootResource(request);
+ const customLoadResult = invokeLoadBootResource(asset);
if (customLoadResult instanceof Promise) {
// They are supplying an entire custom response, so just use that
return customLoadResult;
} else if (typeof customLoadResult === "string") {
- url = makeURLAbsoluteWithApplicationBase(customLoadResult);
+ url = customLoadResult;
}
}
- const fetchOptions: RequestInit = {
- cache: "no-cache"
- };
-
- if (credentialsIncludeAssetBehaviors.includes(request.behavior)) {
+ const fetchOptions: RequestInit = {};
+ if (!loaderHelpers.config.disableNoCacheFetch) {
+ // FIXME: "no-cache" is how blazor works in Net7, but this prevents caching on HTTP level
+ // if we would like to get rid of our own cache and only use HTTP cache, we need to remove this
+ // https://github.com/dotnet/runtime/issues/74815
+ fetchOptions.cache = "no-cache";
+ }
+ if (asset.useCredentials) {
// Include credentials so the server can allow download / provide user specific file
fetchOptions.credentials = "include";
} else {
- // Any other resource than configuration should provide integrity check
- // Note that if cacheBootResources was explicitly disabled, we also bypass hash checking
- // This is to give developers an easy opt-out from the entire caching/validation flow if
- // there's anything they don't like about it.
- fetchOptions.integrity = isCacheAvailable() ? (request.hash ?? "") : undefined;
+ // `disableIntegrityCheck` is to give developers an easy opt-out from the integrity check
+ if (!loaderHelpers.config.disableIntegrityCheck && asset.hash) {
+ // Any other resource than configuration should provide integrity check
+ fetchOptions.integrity = asset.hash;
+ }
}
return loaderHelpers.fetch_like(url, fetchOptions);
"js-module-threads": "dotnetjs"
};
-function invokeLoadBootResource(request: ResourceRequest): string | Promise<Response> | null | undefined {
+function invokeLoadBootResource(asset: AssetEntryInternal): string | Promise<Response> | null | undefined {
if (loaderHelpers.loadBootResource) {
- const requestHash = request.hash ?? "";
- const url = request.resolvedUrl!;
+ const requestHash = asset.hash ?? "";
+ const url = asset.resolvedUrl!;
- const resourceType = monoToBlazorAssetTypeMap[request.behavior];
+ const resourceType = monoToBlazorAssetTypeMap[asset.behavior];
if (resourceType) {
- return loaderHelpers.loadBootResource(resourceType, request.name, url, requestHash, request.behavior);
+ const customLoadResult = loaderHelpers.loadBootResource(resourceType, asset.name, url, requestHash, asset.behavior);
+ if (typeof customLoadResult === "string") {
+ return makeURLAbsoluteWithApplicationBase(customLoadResult);
+ }
+ return customLoadResult;
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-import type { AssetBehaviors, MonoConfig, ResourceRequest } from "../types";
-import { loaderHelpers } from "./globals";
+import type { MonoConfig } from "../types";
+import type { AssetEntryInternal } from "../types/internal";
+import { ENVIRONMENT_IS_WEB, loaderHelpers } from "./globals";
-const cacheSkipAssetBehaviors: AssetBehaviors[] = ["vfs"]; // Previously only configuration
const usedCacheKeys: { [key: string]: boolean } = {};
const networkLoads: { [name: string]: LoadLogEntry } = {};
const cacheLoads: { [name: string]: LoadLogEntry } = {};
let cacheIfUsed: Cache | null;
-export function isCacheAvailable(): boolean {
- return !!cacheIfUsed;
-}
-
export function logDownloadStatsToConsole(): void {
const cacheLoadsEntries = Object.values(cacheLoads);
const networkLoadsEntries = Object.values(networkLoads);
// We have no perf stats to display, likely because caching is not in use.
return;
}
-
- const linkerDisabledWarning = loaderHelpers.config.linkerEnabled ? "%c" : "\n%cThis application was built with linking (tree shaking) disabled. Published applications will be significantly smaller if you install wasm-tools workload. See also https://aka.ms/dotnet-wasm-features";
+ const useStyle = ENVIRONMENT_IS_WEB ? "%c" : "";
+ const style = ENVIRONMENT_IS_WEB ? ["background: purple; color: white; padding: 1px 3px; border-radius: 3px;",
+ "font-weight: bold;",
+ "font-weight: normal;",
+ ] : [];
+ const linkerDisabledWarning = !loaderHelpers.config.linkerEnabled ? "\nThis application was built with linking (tree shaking) disabled. \nPublished applications will be significantly smaller if you install wasm-tools workload. \nSee also https://aka.ms/dotnet-wasm-features" : "";
// eslint-disable-next-line no-console
- console.groupCollapsed(`%cdotnet%c Loaded ${toDataSizeString(totalResponseBytes)} resources${linkerDisabledWarning}`, "background: purple; color: white; padding: 1px 3px; border-radius: 3px;", "font-weight: bold;", "font-weight: normal;");
+ console.groupCollapsed(`${useStyle}dotnet${useStyle} Loaded ${toDataSizeString(totalResponseBytes)} resources${useStyle}${linkerDisabledWarning}`, ...style);
if (cacheLoadsEntries.length) {
// eslint-disable-next-line no-console
}
}
-export async function findCachedResponse(request: ResourceRequest): Promise<Response | undefined> {
+export async function findCachedResponse(asset: AssetEntryInternal): Promise<Response | undefined> {
const cache = cacheIfUsed;
- if (!cache || cacheSkipAssetBehaviors.includes(request.behavior) || !request.hash || request.hash.length === 0) {
+ if (!cache || asset.noCache || !asset.hash || asset.hash.length === 0) {
return undefined;
}
- const cacheKey = getCacheKey(request);
+ const cacheKey = getCacheKey(asset);
usedCacheKeys[cacheKey] = true;
let cachedResponse: Response | undefined;
// It's in the cache.
const responseBytes = parseInt(cachedResponse.headers.get("content-length") || "0");
- cacheLoads[request.name] = { responseBytes };
+ cacheLoads[asset.name] = { responseBytes };
return cachedResponse;
}
-export function addCachedReponse(request: ResourceRequest, networkResponse: Response): void {
+export function addCachedReponse(asset: AssetEntryInternal, networkResponse: Response): void {
const cache = cacheIfUsed;
- if (!cache || cacheSkipAssetBehaviors.includes(request.behavior) || !request.hash || request.hash.length === 0) {
+ if (!cache || asset.noCache || !asset.hash || asset.hash.length === 0) {
return;
}
+ const clonedResponse = networkResponse.clone();
- const cacheKey = getCacheKey(request);
- addToCacheAsync(cache, request.name, cacheKey, networkResponse); // Don't await - add to cache in background
+ // postpone adding to cache until after we load the assembly, so that we could do the dotnet loading of the asset first
+ setTimeout(() => {
+ const cacheKey = getCacheKey(asset);
+ addToCacheAsync(cache, asset.name, cacheKey, clonedResponse); // Don't await - add to cache in background
+ }, 0);
}
-function getCacheKey(request: ResourceRequest) {
- return `${request.resolvedUrl}.${request.hash}`;
+function getCacheKey(asset: AssetEntryInternal) {
+ return `${asset.resolvedUrl}.${asset.hash}`;
}
-async function addToCacheAsync(cache: Cache, name: string, cacheKey: string, response: Response) {
+async function addToCacheAsync(cache: Cache, name: string, cacheKey: string, clonedResponse: Response) {
// We have to clone in order to put this in the cache *and* not prevent other code from
// reading the original response stream.
- const responseData = await response.clone().arrayBuffer();
+ const responseData = await clonedResponse.arrayBuffer();
// Now is an ideal moment to capture the performance stats for the request, since it
// only just completed and is most likely to still be in the buffer. However this is
// only done on a 'best effort' basis. Even if we do receive an entry, some of its
// properties may be blanked out if it was a CORS request.
- const performanceEntry = getPerformanceEntry(response.url);
+ const performanceEntry = getPerformanceEntry(clonedResponse.url);
const responseBytes = (performanceEntry && performanceEntry.encodedBodySize) || undefined;
networkLoads[name] = { responseBytes };
// We can't rely on the server sending content-length (ASP.NET Core doesn't by default)
const responseToCache = new Response(responseData, {
headers: {
- "content-type": response.headers.get("content-type") || "",
- "content-length": (responseBytes || response.headers.get("content-length") || "").toString(),
+ "content-type": clonedResponse.headers.get("content-type") || "",
+ "content-length": (responseBytes || clonedResponse.headers.get("content-length") || "").toString(),
},
});
import type { DotnetModuleConfig, MonoConfig, ResourceGroups, ResourceList } from "../types";
import { ENVIRONMENT_IS_WEB, exportedRuntimeAPI, loaderHelpers, runtimeHelpers } from "./globals";
import { mono_log_error, mono_log_debug } from "./logging";
-import { invokeLibraryInitializers } from "./libraryInitializers";
+import { importLibraryInitializers, invokeLibraryInitializers } from "./libraryInitializers";
import { mono_exit } from "./exit";
import { makeURLAbsoluteWithApplicationBase } from "./polyfills";
+import { appendUniqueQuery } from "./assets";
export function deep_merge_config(target: MonoConfigInternal, source: MonoConfigInternal): MonoConfigInternal {
// no need to merge the same object
normalizeConfig();
- await invokeLibraryInitializers("onRuntimeConfigLoaded", [loaderHelpers.config], "modulesAfterConfigLoaded");
-
if (module.onConfigLoaded) {
try {
await module.onConfigLoaded(loaderHelpers.config, exportedRuntimeAPI);
throw err;
}
}
+
+ await importLibraryInitializers(loaderHelpers.config.resources?.modulesAfterConfigLoaded);
+ await invokeLibraryInitializers("onRuntimeConfigLoaded", [loaderHelpers.config]);
+ normalizeConfig();
+
loaderHelpers.afterConfigLoaded.promise_control.resolve(loaderHelpers.config);
} catch (err) {
const errMessage = `Failed to load config file ${configFilePath} ${err} ${(err as Error)?.stack}`;
let loadConfigResponse: Response;
if (!loaderResponse) {
- loadConfigResponse = await defaultLoadBootConfig(defaultConfigSrc);
+ loadConfigResponse = await defaultLoadBootConfig(appendUniqueQuery(defaultConfigSrc, "manifest"));
} else if (typeof loaderResponse === "string") {
loadConfigResponse = await defaultLoadBootConfig(makeURLAbsoluteWithApplicationBase(loaderResponse));
} else {
_loaded_files: [],
loadedFiles: [],
loadedAssemblies: [],
+ libraryInitializers: [],
actual_downloaded_assets_count: 0,
actual_instantiated_assets_count: 0,
expected_downloaded_assets_count: 0,
getPromiseController,
assertIsControllablePromise,
mono_download_assets,
- resolve_asset_path: resolve_single_asset_path,
+ resolve_single_asset_path,
setup_proxy_console,
logDownloadStatsToConsole,
purgeUnusedCacheEntriesAsync,
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-import { mono_log_warn } from "./logging";
-import { MonoConfig } from "../types";
+import { mono_log_debug, mono_log_warn } from "./logging";
import { appendUniqueQuery } from "./assets";
import { loaderHelpers } from "./globals";
import { mono_exit } from "./exit";
+import { ResourceList } from "../types";
-export type LibraryInitializerTypes =
- "modulesAfterConfigLoaded"
- | "modulesAfterRuntimeReady";
-
-async function fetchLibraryInitializers(config: MonoConfig, type: LibraryInitializerTypes): Promise<void> {
- if (!loaderHelpers.libraryInitializers) {
- loaderHelpers.libraryInitializers = [];
- }
-
- const libraryInitializers = type == "modulesAfterConfigLoaded"
- ? config.resources?.modulesAfterConfigLoaded
- : config.resources?.modulesAfterRuntimeReady;
-
+export async function importLibraryInitializers(libraryInitializers: ResourceList | undefined): Promise<void> {
if (!libraryInitializers) {
return;
}
async function importInitializer(path: string): Promise<void> {
try {
const adjustedPath = appendUniqueQuery(loaderHelpers.locateFile(path), "js-module-library-initializer");
+ mono_log_debug(`Attempting to import '${adjustedPath}' for ${path}`);
const initializer = await import(/* webpackIgnore: true */ adjustedPath);
loaderHelpers.libraryInitializers!.push({ scriptName: path, exports: initializer });
}
}
-export async function invokeLibraryInitializers(functionName: string, args: any[], type?: LibraryInitializerTypes) {
- if (type) {
- await fetchLibraryInitializers(loaderHelpers.config, type);
- }
-
+export async function invokeLibraryInitializers(functionName: string, args: any[]) {
if (!loaderHelpers.libraryInitializers) {
return;
}
import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_WEB, exportedRuntimeAPI, globalObjectsRoot, mono_assert } from "./globals";
import { deep_merge_config, deep_merge_module, mono_wasm_load_config } from "./config";
import { mono_exit } from "./exit";
-import { setup_proxy_console, mono_log_info } from "./logging";
-import { resolve_single_asset_path, start_asset_download } from "./assets";
+import { setup_proxy_console, mono_log_info, mono_log_debug } from "./logging";
+import { mono_download_assets, prepareAssets, prepareAssetsWorker, resolve_single_asset_path, start_asset_download } from "./assets";
import { detect_features_and_polyfill } from "./polyfills";
import { runtimeHelpers, loaderHelpers } from "./globals";
import { init_globalization } from "./icu";
import { setupPreloadChannelToMainThread } from "./worker";
-import { invokeLibraryInitializers } from "./libraryInitializers";
+import { importLibraryInitializers, invokeLibraryInitializers } from "./libraryInitializers";
import { initCacheToUseIfEnabled } from "./assetsCache";
const module = globalObjectsRoot.module;
}
function importModules() {
- runtimeHelpers.runtimeModuleUrl = resolve_single_asset_path("js-module-runtime").resolvedUrl!;
- runtimeHelpers.nativeModuleUrl = resolve_single_asset_path("js-module-native").resolvedUrl!;
+ const jsModuleRuntimeAsset = resolve_single_asset_path("js-module-runtime");
+ const jsModuleNativeAsset = resolve_single_asset_path("js-module-native");
+ mono_log_debug(`Attempting to import '${jsModuleRuntimeAsset.resolvedUrl}' for ${jsModuleRuntimeAsset.name}`);
+ mono_log_debug(`Attempting to import '${jsModuleNativeAsset.resolvedUrl}' for ${jsModuleNativeAsset.name}`);
return [
// keep js module names dynamic by using config, in the future we can use feature detection to load different flavors
- import(/* webpackIgnore: true */runtimeHelpers.runtimeModuleUrl),
- import(/* webpackIgnore: true */runtimeHelpers.nativeModuleUrl),
+ import(/* webpackIgnore: true */jsModuleRuntimeAsset.resolvedUrl!),
+ import(/* webpackIgnore: true */jsModuleNativeAsset.resolvedUrl!),
];
}
// download config
await mono_wasm_load_config(module);
+ prepareAssets();
+
const promises = importModules();
await initCacheToUseIfEnabled();
init_globalization();
- // TODO call mono_download_assets(); here in parallel ?
+ setTimeout(() => {
+ mono_download_assets(); // intentionally not awaited
+ }, 0);
+
const es6Modules = await Promise.all(promises);
+
await initializeModules(es6Modules as any);
await runtimeHelpers.dotnetReady.promise;
- await invokeLibraryInitializers("onRuntimeReady", [globalObjectsRoot.api], "modulesAfterRuntimeReady");
+ await importLibraryInitializers(loaderHelpers.config.resources?.modulesAfterRuntimeReady);
+ await invokeLibraryInitializers("onRuntimeReady", [globalObjectsRoot.api]);
return exportedRuntimeAPI;
}
await loaderHelpers.afterConfigLoaded.promise;
+ prepareAssetsWorker();
+
const promises = importModules();
const es6Modules = await Promise.all(promises);
await initializeModules(es6Modules as any);
/// We replace Module["PThreads"].allocateUnusedWorker with this version that knows about assets
function replacementAllocateUnusedWorker(): void {
mono_log_debug("replacementAllocateUnusedWorker");
- const asset = loaderHelpers.resolve_asset_path("js-module-threads");
+ const asset = loaderHelpers.resolve_single_asset_path("js-module-threads");
const uri = asset.resolvedUrl;
mono_assert(uri !== undefined, "could not resolve the uri for the js-module-threads asset");
const worker = new Worker(uri);
// - init the rest of the polyfills
await mono_wasm_pre_init_essential_async();
- // - start download assets like DLLs
- await mono_wasm_pre_init_full();
-
endMeasure(mark, MeasuredBlock.preInit);
} catch (err) {
loaderHelpers.mono_exit(1, err);
if (loaderHelpers.config.debugLevel !== 0 && loaderHelpers.config.cacheBootResources) {
loaderHelpers.logDownloadStatsToConsole();
}
- loaderHelpers.purgeUnusedCacheEntriesAsync(); // Don't await - it's fine to run in background
+ const afterStartupRushIsOver = 10000;// 10 seconds
+ setTimeout(() => {
+ loaderHelpers.purgeUnusedCacheEntriesAsync(); // Don't await - it's fine to run in background
+ }, afterStartupRushIsOver);
// call user code
try {
Module.removeRunDependency("mono_wasm_pre_init_essential_async");
}
-async function mono_wasm_pre_init_full(): Promise<void> {
- mono_log_debug("mono_wasm_pre_init_full");
- Module.addRunDependency("mono_wasm_pre_init_full");
-
- await loaderHelpers.mono_download_assets();
-
- Module.removeRunDependency("mono_wasm_pre_init_full");
-}
-
async function mono_wasm_after_user_runtime_initialized(): Promise<void> {
mono_log_debug("mono_wasm_after_user_runtime_initialized");
try {
// The .NET Foundation licenses this file to you under the MIT license.
import type { IMemoryView } from "../marshal";
-import type { CreateDotnetRuntimeType, DotnetHostBuilder, DotnetModuleConfig, RuntimeAPI, MonoConfig, ModuleAPI, AssetEntry, ResourceRequest, GlobalizationMode, AssetBehaviors } from ".";
+import type { CreateDotnetRuntimeType, DotnetHostBuilder, DotnetModuleConfig, RuntimeAPI, MonoConfig, ModuleAPI, AssetEntry, GlobalizationMode, AssetBehaviors } from ".";
import type { EmscriptenModule } from "./emscripten";
import type { dotnet, exit } from "../loader/index";
export {
EmscriptenModule,
- RuntimeAPI, ModuleAPI, DotnetHostBuilder, DotnetModuleConfig, CreateDotnetRuntimeType, MonoConfig, IMemoryView, AssetEntry, ResourceRequest, GlobalizationMode, AssetBehaviors,
+ RuntimeAPI, ModuleAPI, DotnetHostBuilder, DotnetModuleConfig, CreateDotnetRuntimeType, MonoConfig, IMemoryView, AssetEntry, GlobalizationMode, AssetBehaviors,
dotnet, exit
};
*/
cacheBootResources?: boolean,
/**
+ * Configures use of the `integrity` directive for fetching assets
+ */
+ disableIntegrityCheck?: boolean,
+ /**
+ * Configures use of the `no-cache` directive for fetching assets
+ */
+ disableNoCacheFetch?: boolean,
+ /**
* Enables diagnostic log messages during startup
*/
diagnosticTracing?: boolean
*/
export type LoadBootResourceCallback = (type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string, behavior: AssetBehaviors) => string | Promise<Response> | null | undefined;
-export interface ResourceRequest {
- name: string, // the name of the asset, including extension.
- behavior: AssetBehaviors, // determines how the asset will be handled once loaded
- resolvedUrl?: string; // this should be absolute url to the asset
- hash?: string | null | ""; // the integrity hash of the asset (if any)
-}
-
export interface LoadingResource {
name: string;
url: string;
}
// Types of assets that can be in the _framework/blazor.boot.json file (taken from /src/tasks/WasmAppBuilder/WasmAppBuilder.cs)
-export interface AssetEntry extends ResourceRequest {
+export interface AssetEntry {
+ /**
+ * the name of the asset, including extension.
+ */
+ name: string,
+ /**
+ * determines how the asset will be handled once loaded
+ */
+ behavior: AssetBehaviors,
+ /**
+ * this should be absolute url to the asset
+ */
+ resolvedUrl?: string;
+ /**
+ * the integrity hash of the asset (if any)
+ */
+ hash?: string | null | ""; //
/**
* If specified, overrides the path of the asset in the virtual filesystem and similar data structures once downloaded.
*/
// when adding new fields, please consider if it should be impacting the snapshot hash. If not, please drop it in the snapshot getCacheKey()
export type MonoConfigInternal = MonoConfig & {
linkerEnabled?: boolean,
- assets?: AssetEntry[],
+ assets?: AssetEntryInternal[],
runtimeOptions?: string[], // array of runtime options as strings
aotProfilerOptions?: AOTProfilerOptions, // dictionary-style Object. If omitted, aot profiler will not be initialized.
browserProfilerOptions?: BrowserProfilerOptions, // dictionary-style Object. If omitted, browser profiler will not be initialized.
}
export interface AssetEntryInternal extends AssetEntry {
- // this is almost the same as pendingDownload, but it could have multiple values in time, because of re-try download logic
+ // this could have multiple values in time, because of re-try download logic
pendingDownloadInternal?: LoadingResource
+ noCache?: boolean
+ useCredentials?: boolean
}
export type LoaderHelpers = {
getPromiseController: <T>(promise: ControllablePromise<T>) => PromiseController<T>,
assertIsControllablePromise: <T>(promise: Promise<T>) => asserts promise is ControllablePromise<T>,
mono_download_assets: () => Promise<void>,
- resolve_asset_path: (behavior: AssetBehaviors) => AssetEntryInternal,
+ resolve_single_asset_path: (behavior: AssetBehaviors) => AssetEntryInternal,
setup_proxy_console: (id: string, console: Console, origin: string) => void
fetch_like: (url: string, init?: RequestInit) => Promise<Response>;
locateFile: (path: string, prefix?: string) => string,
jsSynchronizationContextInstalled: boolean,
cspPolicy: boolean,
- runtimeModuleUrl: string
- nativeModuleUrl: string
allAssetsInMemory: PromiseAndController<void>,
dotnetReady: PromiseAndController<any>,
memorySnapshotSkippedOrDone: PromiseAndController<void>,