wasm-idle 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +52 -5
  2. package/dist/index.d.ts +4 -1
  3. package/dist/index.js +2 -0
  4. package/dist/playground/assetBridge.d.ts +11 -2
  5. package/dist/playground/assetBridge.js +190 -46
  6. package/dist/playground/assets.d.ts +9 -1
  7. package/dist/playground/assets.js +29 -5
  8. package/dist/playground/awk.js +1 -0
  9. package/dist/playground/bash.d.ts +1 -0
  10. package/dist/playground/bash.js +7 -1
  11. package/dist/playground/bqn.js +1 -0
  12. package/dist/playground/bqnPreflight.d.ts +2 -1
  13. package/dist/playground/bqnPreflight.js +1 -0
  14. package/dist/playground/c3.js +1 -0
  15. package/dist/playground/cachedClang.d.ts +10 -0
  16. package/dist/playground/cachedClang.js +330 -0
  17. package/dist/playground/clang.d.ts +8 -0
  18. package/dist/playground/clang.js +47 -8
  19. package/dist/playground/clangWorkerProtocol.d.ts +57 -0
  20. package/dist/playground/clangWorkerProtocol.js +11 -0
  21. package/dist/playground/clojurescript.js +1 -0
  22. package/dist/playground/clojurescriptPreflight.d.ts +2 -1
  23. package/dist/playground/clojurescriptPreflight.js +1 -0
  24. package/dist/playground/compiledArtifactCache.d.ts +21 -0
  25. package/dist/playground/compiledArtifactCache.js +67 -0
  26. package/dist/playground/elixir.js +4 -1
  27. package/dist/playground/forth.js +1 -0
  28. package/dist/playground/forthPreflight.d.ts +2 -1
  29. package/dist/playground/forthPreflight.js +1 -0
  30. package/dist/playground/fortran.js +7 -1
  31. package/dist/playground/haskell.js +4 -1
  32. package/dist/playground/haskellAssets.d.ts +2 -1
  33. package/dist/playground/haskellAssets.js +8 -8
  34. package/dist/playground/index.d.ts +1 -1
  35. package/dist/playground/index.js +2 -2
  36. package/dist/playground/j.js +1 -0
  37. package/dist/playground/jPreflight.d.ts +2 -1
  38. package/dist/playground/jPreflight.js +1 -0
  39. package/dist/playground/janet.js +1 -0
  40. package/dist/playground/java.js +20 -8
  41. package/dist/playground/julia.js +1 -0
  42. package/dist/playground/lfortranAssets.d.ts +2 -1
  43. package/dist/playground/lfortranAssets.js +1 -0
  44. package/dist/playground/lisp.js +4 -1
  45. package/dist/playground/lispAssets.d.ts +2 -1
  46. package/dist/playground/lispAssets.js +10 -0
  47. package/dist/playground/lldbManifest.d.ts +2 -2
  48. package/dist/playground/lldbManifest.js +17 -3
  49. package/dist/playground/lldbSession.d.ts +1 -0
  50. package/dist/playground/lldbSession.js +2 -1
  51. package/dist/playground/nim.js +1 -0
  52. package/dist/playground/ocaml.d.ts +1 -0
  53. package/dist/playground/ocaml.js +12 -1
  54. package/dist/playground/options.d.ts +2 -0
  55. package/dist/playground/pascal.js +1 -0
  56. package/dist/playground/perl.js +1 -0
  57. package/dist/playground/prolog.js +1 -0
  58. package/dist/playground/python.d.ts +7 -0
  59. package/dist/playground/python.js +41 -8
  60. package/dist/playground/runtimeAssetCache.d.ts +26 -0
  61. package/dist/playground/runtimeAssetCache.js +84 -0
  62. package/dist/playground/runtimeSession.d.ts +21 -0
  63. package/dist/playground/runtimeSession.js +119 -0
  64. package/dist/playground/rust.d.ts +1 -0
  65. package/dist/playground/rust.js +11 -1
  66. package/dist/playground/rustExecutableGraph.d.ts +1 -0
  67. package/dist/playground/rustExecutableGraph.js +1 -0
  68. package/dist/playground/staticRuntimePreflight.d.ts +2 -1
  69. package/dist/playground/staticRuntimePreflight.js +4 -2
  70. package/dist/playground/staticRuntimePreflightExecute.js +1 -0
  71. package/dist/playground/staticRuntimePreflightProtocol.d.ts +2 -1
  72. package/dist/playground/staticWorkerRuntime.d.ts +3 -1
  73. package/dist/playground/staticWorkerRuntime.js +44 -3
  74. package/dist/playground/tcl.js +1 -0
  75. package/dist/playground/tinygo.d.ts +2 -0
  76. package/dist/playground/tinygo.js +57 -3
  77. package/dist/playground/tinygoExecutableGraph.d.ts +4 -1
  78. package/dist/playground/tinygoExecutableGraph.js +59 -7
  79. package/dist/playground/typescript.js +4 -1
  80. package/dist/playground/wasmGoVersion.d.ts +1 -1
  81. package/dist/playground/wasmGoVersion.js +1 -1
  82. package/dist/playground/wasmOcamlVersion.d.ts +2 -2
  83. package/dist/playground/wasmOcamlVersion.js +1 -1
  84. package/dist/playground/wasmRustVersion.d.ts +90 -24
  85. package/dist/playground/wasmRustVersion.js +98 -23
  86. package/dist/playground/wasmTinyGoVersion.d.ts +40 -29
  87. package/dist/playground/wasmTinyGoVersion.js +43 -31
  88. package/dist/playground/worker/assets.d.ts +3 -0
  89. package/dist/playground/worker/assets.js +41 -0
  90. package/dist/playground/worker/cachedClangCompile.d.ts +1 -0
  91. package/dist/playground/worker/cachedClangCompile.js +85 -0
  92. package/dist/playground/worker/cachedClangExecute.d.ts +1 -0
  93. package/dist/playground/worker/cachedClangExecute.js +67 -0
  94. package/dist/playground/worker/clang.js +2 -8
  95. package/dist/playground/worker/clangStreaming.d.ts +1 -1
  96. package/dist/playground/worker/clangStreaming.js +20 -2
  97. package/dist/playground/worker/elixir.js +10 -8
  98. package/dist/playground/worker/fortran.js +4 -7
  99. package/dist/playground/worker/haskell.js +2 -0
  100. package/dist/playground/worker/lisp.js +2 -0
  101. package/dist/playground/worker/ocaml.js +13 -1
  102. package/dist/playground/worker/python.js +3 -2
  103. package/dist/playground/worker/runtimeAssetFetch.d.ts +11 -1
  104. package/dist/playground/worker/runtimeAssetFetch.js +64 -1
  105. package/dist/playground/worker/runtimeModule.d.ts +8 -0
  106. package/dist/playground/worker/runtimeModule.js +49 -0
  107. package/dist/playground/worker/rust.js +9 -2
  108. package/dist/playground/worker/staticRuntimePreflight.js +2 -0
  109. package/dist/playground/worker/typescript.js +4 -1
  110. package/dist/playground/worker/zig.js +7 -4
  111. package/dist/playground/zig.js +4 -1
  112. package/package.json +10 -7
@@ -2,6 +2,7 @@ export interface WorkerRuntimeAssetConfig {
2
2
  baseUrl: string;
3
3
  maxAssetBytes?: number;
4
4
  useAssetBridge: boolean;
5
+ useModuleBridge?: boolean;
5
6
  }
6
7
  export interface WorkerRuntimeAssetAllowlist {
7
8
  baseUrl: string;
@@ -16,4 +17,6 @@ export declare function configureWorkerRuntimeAssets(config: WorkerRuntimeAssetC
16
17
  export declare function configureWorkerRuntimeAssetAllowlist(allowlist: WorkerRuntimeAssetAllowlist | null): void;
17
18
  export declare function handleWorkerAssetMessage(data: any): boolean;
18
19
  export declare function loadWorkerRuntimeAsset(asset: string): Promise<LoadedWorkerAsset>;
20
+ export declare function hasWorkerRuntimeModuleBridge(): boolean;
21
+ export declare function loadWorkerRuntimeModule(asset: string): Promise<WebAssembly.Module>;
19
22
  export {};
@@ -14,6 +14,7 @@ let activeDirectAssetUrls = new Map();
14
14
  let interceptorsInstalled = false;
15
15
  let nextAssetRequestId = 0;
16
16
  const pendingAssetRequests = new Map();
17
+ const pendingModuleRequests = new Map();
17
18
  const cancelResponseBody = (response, reason) => {
18
19
  try {
19
20
  void Promise.resolve(response.body?.cancel(reason)).catch(() => undefined);
@@ -441,6 +442,10 @@ export function configureWorkerRuntimeAssets(config) {
441
442
  (!Number.isSafeInteger(config.maxAssetBytes) || config.maxAssetBytes <= 0)) {
442
443
  throw new TypeError('Runtime asset maxAssetBytes must be a positive safe integer');
443
444
  }
445
+ for (const pending of pendingModuleRequests.values()) {
446
+ pending.reject(new Error('Runtime module configuration changed'));
447
+ }
448
+ pendingModuleRequests.clear();
444
449
  activeDirectAssetBaseUrl = null;
445
450
  activeDirectAssetUrls = new Map();
446
451
  activeConfig = config;
@@ -508,6 +513,20 @@ export function handleWorkerAssetMessage(data) {
508
513
  const response = data?.assetResponse;
509
514
  if (!response)
510
515
  return false;
516
+ const moduleRequest = pendingModuleRequests.get(response.id);
517
+ if (moduleRequest) {
518
+ pendingModuleRequests.delete(response.id);
519
+ if (!response.ok) {
520
+ moduleRequest.reject(new Error(response.error || 'Runtime module request failed'));
521
+ }
522
+ else if (!(response.module instanceof WebAssembly.Module)) {
523
+ moduleRequest.reject(new Error('Runtime module response is invalid'));
524
+ }
525
+ else {
526
+ moduleRequest.resolve(response.module);
527
+ }
528
+ return true;
529
+ }
511
530
  const pending = pendingAssetRequests.get(response.id);
512
531
  if (!pending)
513
532
  return true;
@@ -531,3 +550,25 @@ export async function loadWorkerRuntimeAsset(asset) {
531
550
  }
532
551
  return await loadTrackedAsset(assetUrl);
533
552
  }
553
+ export function hasWorkerRuntimeModuleBridge() {
554
+ return activeConfig?.useAssetBridge === true || activeConfig?.useModuleBridge === true;
555
+ }
556
+ export async function loadWorkerRuntimeModule(asset) {
557
+ if (!hasWorkerRuntimeModuleBridge())
558
+ throw new Error('Runtime module bridge unavailable');
559
+ const assetUrl = trackedAssetUrl(asset);
560
+ if (!assetUrl || !isTrackedAssetUrl(assetUrl)) {
561
+ throw new Error('Untracked runtime module request');
562
+ }
563
+ const id = ++nextAssetRequestId;
564
+ return await new Promise((resolve, reject) => {
565
+ pendingModuleRequests.set(id, { resolve, reject });
566
+ try {
567
+ self.postMessage({ assetRequest: { id, asset, module: true } });
568
+ }
569
+ catch (error) {
570
+ pendingModuleRequests.delete(id);
571
+ reject(error);
572
+ }
573
+ });
574
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,85 @@
1
+ import { BrowserClangRuntime, loadRuntimeManifest, resolveRuntimeManifestUrl } from '@wasm-idle/llvm-core/clang';
2
+ import { configureWorkerRuntimeAssets, handleWorkerAssetMessage } from './assets';
3
+ import { compileWorkerRuntimeAsset } from './runtimeModule';
4
+ import { BUNDLED_CLANG_LANGUAGE_SYSROOT_PROFILES } from '../clangAssetIntegrity';
5
+ const worker = globalThis;
6
+ let started = false;
7
+ worker.onmessage = async ({ data }) => {
8
+ if (handleWorkerAssetMessage(data))
9
+ return;
10
+ if (started)
11
+ return;
12
+ started = true;
13
+ const output = [];
14
+ try {
15
+ if (data.request.debug || (data.request.debugMode && data.request.debugMode !== 'none')) {
16
+ throw new Error('Cached C/C++ execution does not support debug artifacts.');
17
+ }
18
+ configureWorkerRuntimeAssets(data.assets);
19
+ const manifest = await loadRuntimeManifest(resolveRuntimeManifestUrl(data.runtimeBaseUrl), fetch, undefined, data.maxAssetBytes);
20
+ if (data.languageSysroots)
21
+ manifest.compiler.sysroot.profiles = BUNDLED_CLANG_LANGUAGE_SYSROOT_PROFILES;
22
+ const moduleAssets = new Map([
23
+ [
24
+ manifest.compiler.clang.asset,
25
+ new URL(manifest.compiler.clang.asset, data.runtimeBaseUrl).href
26
+ ],
27
+ [
28
+ manifest.compiler.lld.asset,
29
+ new URL(manifest.compiler.lld.asset, data.runtimeBaseUrl).href
30
+ ]
31
+ ].map(([asset, url]) => [url, asset]));
32
+ class CachedRuntime extends BrowserClangRuntime {
33
+ async getModule(url, progress, signal) {
34
+ const asset = moduleAssets.get(url);
35
+ if (!asset)
36
+ return super.getModule(url, progress, signal);
37
+ signal?.throwIfAborted();
38
+ const module = await compileWorkerRuntimeAsset(asset);
39
+ signal?.throwIfAborted();
40
+ progress?.set?.(1);
41
+ return module;
42
+ }
43
+ }
44
+ // Every source cache miss gets fresh compiler memory and files. The host
45
+ // session keeps the immutable tool Modules and verified asset bytes alive.
46
+ const runtime = new CachedRuntime({
47
+ runtimeBaseUrl: data.runtimeBaseUrl,
48
+ maxAssetBytes: data.maxAssetBytes,
49
+ manifest,
50
+ log: data.log ?? false,
51
+ stdout: (chunk) => output.push(chunk),
52
+ progress: (value) => worker.postMessage({
53
+ type: 'progress',
54
+ progress: {
55
+ stage: 'bootstrap',
56
+ completed: Math.round(value * 100),
57
+ total: 100,
58
+ percent: value * 100,
59
+ message: 'Loading compiler runtime'
60
+ }
61
+ })
62
+ });
63
+ await runtime.ready;
64
+ worker.postMessage({
65
+ type: 'progress',
66
+ progress: {
67
+ stage: 'compile',
68
+ completed: 0,
69
+ total: 100,
70
+ percent: 0,
71
+ message: 'Compiling source'
72
+ }
73
+ });
74
+ const artifact = await runtime.compileArtifact(data.request.code, data.request);
75
+ worker.postMessage({ type: 'compiled', artifact, stdout: output.join(''), stderr: '' });
76
+ }
77
+ catch (error) {
78
+ worker.postMessage({
79
+ type: 'error',
80
+ error: error instanceof Error ? error.message : String(error),
81
+ stdout: output.join(''),
82
+ stderr: ''
83
+ });
84
+ }
85
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,67 @@
1
+ import { executeBrowserClangArtifact } from '@wasm-idle/llvm-core/clang';
2
+ import { CLANG_STDIN_DATA, CLANG_STDIN_EOF, CLANG_STDIN_HEADER_BYTES, CLANG_STDIN_WAITING } from '../clangWorkerProtocol';
3
+ const worker = globalThis;
4
+ worker.onmessage = async ({ data }) => {
5
+ worker.onmessage = null;
6
+ try {
7
+ const hasStaticInput = typeof data.stdin === 'string';
8
+ let staticInput = hasStaticInput ? new TextEncoder().encode(data.stdin) : null;
9
+ let reachedEof = false;
10
+ const control = hasStaticInput ? null : new Int32Array(data.inputBuffer, 0, 2);
11
+ const payload = hasStaticInput
12
+ ? null
13
+ : new Uint8Array(data.inputBuffer, CLANG_STDIN_HEADER_BYTES);
14
+ if (payload && payload.byteLength === 0)
15
+ throw new Error('C/C++ stdin buffer is empty.');
16
+ const files = (data.workspaceFiles || []).map(({ path, content }) => ({
17
+ path,
18
+ contents: content
19
+ }));
20
+ if (data.code !== undefined) {
21
+ files.push({
22
+ path: data.activePath || (data.artifact.language === 'C' ? 'main.c' : 'main.cc'),
23
+ contents: data.code
24
+ });
25
+ }
26
+ // The public executor creates a fresh WASI host, files, descriptors and Wasm
27
+ // instance. Only the immutable compiled Module is reused across executions.
28
+ const result = await executeBrowserClangArtifact(data.artifact, {
29
+ args: data.programArgs,
30
+ env: data.env,
31
+ files,
32
+ stdin: () => {
33
+ if (hasStaticInput) {
34
+ const next = staticInput;
35
+ staticInput = null;
36
+ return next?.byteLength ? next : null;
37
+ }
38
+ if (reachedEof)
39
+ return null;
40
+ Atomics.store(control, 0, CLANG_STDIN_WAITING);
41
+ worker.postMessage({ type: 'stdin' });
42
+ while (Atomics.load(control, 0) === CLANG_STDIN_WAITING) {
43
+ Atomics.wait(control, 0, CLANG_STDIN_WAITING);
44
+ }
45
+ const state = Atomics.load(control, 0);
46
+ if (state === CLANG_STDIN_EOF) {
47
+ reachedEof = true;
48
+ return null;
49
+ }
50
+ const length = Atomics.load(control, 1);
51
+ if (state !== CLANG_STDIN_DATA || length < 0 || length > payload.byteLength) {
52
+ throw new Error('Invalid C/C++ stdin buffer response.');
53
+ }
54
+ return payload.slice(0, length);
55
+ },
56
+ stdout: (output) => worker.postMessage({ type: 'output', output }),
57
+ stderr: (output) => worker.postMessage({ type: 'output', output })
58
+ });
59
+ worker.postMessage({ type: 'done', exitCode: result.exitCode });
60
+ }
61
+ catch (error) {
62
+ worker.postMessage({
63
+ type: 'error',
64
+ error: error instanceof Error ? error.message : String(error)
65
+ });
66
+ }
67
+ };
@@ -4,11 +4,7 @@ import { isSharedBufferBackedView } from '../sharedBuffer';
4
4
  import { configureWorkerRuntimeAssets, handleWorkerAssetMessage } from './assets';
5
5
  import { withVerifiedStreaming } from './clangStreaming';
6
6
  import { BUNDLED_CLANG_LANGUAGE_SYSROOT_PROFILES } from '../clangAssetIntegrity';
7
- self.document = {
8
- querySelectorAll() {
9
- return [];
10
- }
11
- };
7
+ // Preserve native Worker globals: a partial document makes dev loaders misdetect a DOM.
12
8
  let stdinBufferClang, debugBufferClang, watchBufferClang, watchResultBufferClang, interruptBufferClang, clang;
13
9
  let hasInitialStdinClang = false;
14
10
  let initialStdinClang = null;
@@ -30,9 +26,7 @@ async function loadClang(path, log, maxAssetBytes, verifiedStreaming = false, la
30
26
  }
31
27
  }
32
28
  : manifest;
33
- const Runtime = verifiedStreaming
34
- ? withVerifiedStreaming(BrowserClangRuntime, path, maxAssetBytes)
35
- : BrowserClangRuntime;
29
+ const Runtime = withVerifiedStreaming(BrowserClangRuntime, path, maxAssetBytes, verifiedStreaming);
36
30
  clang = new Runtime({
37
31
  stdout: (output) => postMessage({ output }),
38
32
  onDebugEvent: (debugEvent) => postMessage({ debugEvent }),
@@ -1,3 +1,3 @@
1
1
  import type { BrowserClangRuntime } from '@wasm-idle/llvm-core/clang';
2
2
  /** Application-only adapter. The host opts in only for the built-in trust profile. */
3
- export declare function withVerifiedStreaming(Runtime: typeof BrowserClangRuntime, baseUrl: string, maxAssetBytes?: number): typeof BrowserClangRuntime;
3
+ export declare function withVerifiedStreaming(Runtime: typeof BrowserClangRuntime, baseUrl: string, maxAssetBytes?: number, verifiedStreaming?: boolean): typeof BrowserClangRuntime;
@@ -1,17 +1,35 @@
1
1
  import { compileVerifiedWasmAsset } from '@wasm-idle/llvm-core/core/verified-wasm';
2
2
  import { BUNDLED_CLANG_ASSET_INTEGRITY } from '../clangAssetIntegrity';
3
+ import { hasWorkerRuntimeModuleBridge } from './assets';
4
+ import { compileWorkerRuntimeAsset } from './runtimeModule';
3
5
  // Capture before configureWorkerRuntimeAssets installs its whole-buffer fetch shim.
4
6
  const nativeFetch = globalThis.fetch?.bind(globalThis);
5
7
  const toolNames = ['bin/clang.wasm.gz', 'bin/lld.wasm.gz'];
6
8
  /** Application-only adapter. The host opts in only for the built-in trust profile. */
7
- export function withVerifiedStreaming(Runtime, baseUrl, maxAssetBytes = 128 * 1024 * 1024) {
8
- if (!nativeFetch || !globalThis.crypto?.subtle || typeof DecompressionStream !== 'function')
9
+ export function withVerifiedStreaming(Runtime, baseUrl, maxAssetBytes = 128 * 1024 * 1024, verifiedStreaming = true) {
10
+ const bridgeModules = hasWorkerRuntimeModuleBridge();
11
+ if (!bridgeModules &&
12
+ (!verifiedStreaming ||
13
+ !nativeFetch ||
14
+ !globalThis.crypto?.subtle ||
15
+ typeof DecompressionStream !== 'function'))
9
16
  return Runtime;
10
17
  const urls = new Map(toolNames.map((name) => [new URL(name, baseUrl).href, name]));
11
18
  // Scope the two immutable tool modules to this one runtime construction, not globals.
12
19
  const loads = new Map();
13
20
  return class extends Runtime {
14
21
  async getModule(url, progress, signal) {
22
+ if (bridgeModules) {
23
+ const root = new URL(baseUrl);
24
+ const target = new URL(url, root);
25
+ if (target.origin === root.origin && target.pathname.startsWith(root.pathname)) {
26
+ signal?.throwIfAborted();
27
+ const module = await compileWorkerRuntimeAsset(`${target.pathname.slice(root.pathname.length)}${target.search}`);
28
+ signal?.throwIfAborted();
29
+ progress?.set?.(1);
30
+ return module;
31
+ }
32
+ }
15
33
  const name = urls.get(url);
16
34
  if (!name)
17
35
  return super.getModule(url, progress, signal);
@@ -1,7 +1,7 @@
1
1
  import { waitForBufferedStdin } from '../stdinBuffer';
2
2
  import { ELIXIR_RUNTIME_ASSET_NAMES, snapshotElixirRuntimeAssetReceipts } from '../elixirAssets';
3
3
  import { fetchRuntimeAssetBytes } from './runtimeAssetFetch';
4
- import { verifyRuntimeAssetIntegrity } from '@wasm-idle/core';
4
+ import { configureRuntimeAssetCache, verifyRuntimeAssetIntegrity } from '@wasm-idle/core';
5
5
  const workerDocument = {
6
6
  querySelectorAll() {
7
7
  return [];
@@ -403,15 +403,16 @@ async function loadRuntime(nextBundleUrl, nextAssetReceipts, nextMaxAssetBytes,
403
403
  const bytes = await fetchRuntimeAssetBytes({
404
404
  url: assetUrl,
405
405
  label: `Elixir runtime asset ${asset}`,
406
+ expected: { sha256: receipt.uncompressedSha256, bytes: receipt.uncompressedBytes },
407
+ verify: (bytes) => verifyRuntimeAssetIntegrity({
408
+ asset,
409
+ bytes,
410
+ expected: receipt,
411
+ stage: 'uncompressed',
412
+ runtimeId: 'ELIXIR'
413
+ }),
406
414
  maxAssetBytes: Math.min(receipt.uncompressedBytes, effectiveMaxAssetBytes)
407
415
  });
408
- await verifyRuntimeAssetIntegrity({
409
- asset,
410
- bytes,
411
- expected: receipt,
412
- stage: 'uncompressed',
413
- runtimeId: 'ELIXIR'
414
- });
415
416
  loadedAssets.push([asset, bytes]);
416
417
  }
417
418
  const assets = Object.freeze(Object.fromEntries(loadedAssets));
@@ -465,6 +466,7 @@ self.onmessage = async (event) => {
465
466
  const evalLanguage = language === 'ERLANG' ? 'ERLANG' : 'ELIXIR';
466
467
  try {
467
468
  if (load) {
469
+ configureRuntimeAssetCache(event.data.persistentCache ?? {});
468
470
  await loadRuntime(nextBundleUrl, nextAssetReceipts, nextMaxAssetBytes, log);
469
471
  const configuredReceipts = snapshotElixirRuntimeAssetReceipts(nextAssetReceipts);
470
472
  bundleUrl = nextBundleUrl;
@@ -4,7 +4,7 @@ import { snapshotFortranExecutionAssetReceipts } from '../fortranAssets';
4
4
  import { configureWorkerRuntimeAssets, handleWorkerAssetMessage } from './assets';
5
5
  import { fetchRuntimeAssetBytes, resolveRuntimeAssetUrl } from './runtimeAssetFetch';
6
6
  import { BrowserClangRuntime, createBrowserWasiHost, executeBrowserClangArtifact, loadRuntimeManifest, resolveRuntimeManifestUrl } from '@wasm-idle/llvm-core/clang';
7
- import { verifyRuntimeAssetIntegrity } from '@wasm-idle/core';
7
+ import { configureRuntimeAssetCache, verifyRuntimeAssetIntegrity } from '@wasm-idle/core';
8
8
  self.document = {
9
9
  querySelectorAll() {
10
10
  return [];
@@ -73,13 +73,9 @@ async function fetchVerifiedBytes(url, asset, receipt) {
73
73
  const bytes = await fetchRuntimeAssetBytes({
74
74
  url,
75
75
  label: asset,
76
- maxAssetBytes: receipt.bytes
77
- });
78
- await verifyRuntimeAssetIntegrity({
79
- asset,
80
- bytes,
81
76
  expected: receipt,
82
- runtimeId: 'FORTRAN'
77
+ verify: (bytes) => verifyRuntimeAssetIntegrity({ asset, bytes, expected: receipt, runtimeId: 'FORTRAN' }),
78
+ maxAssetBytes: receipt.bytes
83
79
  });
84
80
  return bytes;
85
81
  }
@@ -248,6 +244,7 @@ self.onmessage = async (event) => {
248
244
  return;
249
245
  const { code, buffer, load, log, prepare, compileArgs, programArgs, activePath, workspaceFiles, stdin, clangAssets, fortranAssets } = event.data;
250
246
  if (load) {
247
+ configureRuntimeAssetCache(event.data.persistentCache ?? {});
251
248
  try {
252
249
  await loadFortranRuntime(clangAssets, fortranAssets, log);
253
250
  postMessage({ load: true });
@@ -1,5 +1,6 @@
1
1
  import { ConsoleStdout, Directory, File, OpenFile, PreopenDirectory, WASI, wasi } from '@bjorn3/browser_wasi_shim';
2
2
  import { installWasiExtractionQuota } from '@wasm-idle/llvm-core';
3
+ import { configureRuntimeAssetCache } from '@wasm-idle/core';
3
4
  import { loadVerifiedHaskellRuntimeAssets, snapshotHaskellRuntimeAssetConfig } from '../haskellAssets';
4
5
  import { waitForBufferedStdin } from '../stdinBuffer';
5
6
  let moduleUrl = '';
@@ -397,6 +398,7 @@ self.onmessage = async (event) => {
397
398
  const { load, moduleUrl: nextModuleUrl, rootfsUrl: nextRootfsUrl, bsdtarUrl: nextBsdtarUrl, integrity: nextIntegrity, maxAssetBytes: nextMaxAssetBytes, mainSoPath: nextMainSoPath, searchDirs: nextSearchDirs, code, prepare, buffer, stdin, ghcArgs = '', activePath = 'main.hs', workspaceFiles = [], log } = event.data;
398
399
  try {
399
400
  if (load) {
401
+ configureRuntimeAssetCache(event.data.persistentCache ?? {});
400
402
  const nextConfig = snapshotHaskellRuntimeAssetConfig({
401
403
  moduleUrl: nextModuleUrl,
402
404
  rootfsUrl: nextRootfsUrl,
@@ -1,4 +1,5 @@
1
1
  import { waitForBufferedStdin } from '../stdinBuffer';
2
+ import { configureRuntimeAssetCache } from '@wasm-idle/core';
2
3
  import { loadVerifiedLispRuntimeAssets } from '../lispAssets';
3
4
  let stdinBufferLisp = null;
4
5
  let runtimeConfig = null;
@@ -80,6 +81,7 @@ self.onmessage = async (event) => {
80
81
  const { load, runtimeConfig: nextRuntimeConfig, maxAssetBytes: nextMaxAssetBytes, buffer, code, prepare, args = [], stdin, activePath = 'main.scm', workspaceFiles = [], log } = event.data;
81
82
  try {
82
83
  if (load) {
84
+ configureRuntimeAssetCache(event.data.persistentCache ?? {});
83
85
  runtimeConfig = nextRuntimeConfig;
84
86
  runtimeMaxAssetBytes = resolveMaxAssetBytes(nextMaxAssetBytes);
85
87
  if (log) {
@@ -1,5 +1,5 @@
1
1
  import { waitForBufferedStdin } from '../stdinBuffer';
2
- import { verifyRuntimeAssetIntegrity } from '@wasm-idle/core';
2
+ import { configureRuntimeAssetCache, createRuntimeAssetCacheBackend, getRuntimeAssetCacheOptions, resolveRuntimeAssetCacheOptions, verifyRuntimeAssetIntegrity } from '@wasm-idle/core';
3
3
  import { fetchRuntimeAssetBytes } from './runtimeAssetFetch';
4
4
  const MAX_OCAML_MANIFEST_BYTES = 4 * 1024 * 1024;
5
5
  const MAX_OCAML_RUNTIME_ENTRY_BYTES = 32 * 1024 * 1024;
@@ -9,6 +9,7 @@ let moduleUrl = '';
9
9
  let manifestUrl = '';
10
10
  let moduleReceipt = null;
11
11
  let manifestReceipt = null;
12
+ let loadPersistentCache;
12
13
  let maxAssetBytes = DEFAULT_MAX_OCAML_ASSET_BYTES;
13
14
  let loadedModuleIdentity = '';
14
15
  let loadedManifestIdentity = '';
@@ -124,6 +125,8 @@ async function loadCompiler(nextModuleUrl, nextReceipt, nextMaxAssetBytes) {
124
125
  const bytes = await fetchRuntimeAssetBytes({
125
126
  url: nextModuleUrl,
126
127
  label: 'OCaml runtime module',
128
+ expected: nextReceipt,
129
+ integrityContext: { runtimeId: 'OCAML' },
127
130
  cache: 'no-store',
128
131
  maxAssetBytes: nextReceipt.bytes
129
132
  });
@@ -218,6 +221,8 @@ async function loadManifest(nextManifestUrl, nextReceipt, nextMaxAssetBytes) {
218
221
  const bytes = await fetchRuntimeAssetBytes({
219
222
  url: nextManifestUrl,
220
223
  label: 'OCaml manifest',
224
+ expected: nextReceipt,
225
+ integrityContext: { runtimeId: 'OCAML' },
221
226
  cache: 'no-store',
222
227
  maxAssetBytes: nextReceipt.bytes
223
228
  });
@@ -493,6 +498,8 @@ self.onmessage = async (event) => {
493
498
  let log = true;
494
499
  try {
495
500
  if (event.data.load) {
501
+ configureRuntimeAssetCache(event.data.persistentCache ?? {});
502
+ loadPersistentCache = getRuntimeAssetCacheOptions();
496
503
  moduleUrl = event.data.moduleUrl;
497
504
  manifestUrl = event.data.manifestUrl;
498
505
  moduleReceipt = requireOuterAssetReceipt(event.data.moduleReceipt, 'module');
@@ -504,6 +511,10 @@ self.onmessage = async (event) => {
504
511
  return;
505
512
  }
506
513
  const { code, prepare, target = 'wasm', wasmBinaryenMode = 'fast', log: configuredLog = true, buffer, stdin, activePath = 'main.ml', workspaceFiles = [] } = event.data;
514
+ configureRuntimeAssetCache(resolveRuntimeAssetCacheOptions(loadPersistentCache, event.data.persistentCache));
515
+ const persistentCache = getRuntimeAssetCacheOptions().enabled
516
+ ? createRuntimeAssetCacheBackend()
517
+ : undefined;
507
518
  log = configuredLog;
508
519
  stdinBufferOcaml = buffer ? new Int32Array(buffer) : null;
509
520
  if (log) {
@@ -538,6 +549,7 @@ self.onmessage = async (event) => {
538
549
  system: compilerModule.createBrowserWorkerSystemDispatcher({
539
550
  manifest,
540
551
  runtimeAssets: {
552
+ ...(persistentCache ? { persistentCache } : {}),
541
553
  limits: {
542
554
  maxAssetBytes,
543
555
  maxMetadataBytes: Math.min(MAX_OCAML_MANIFEST_BYTES, maxAssetBytes),
@@ -1,6 +1,7 @@
1
1
  import { flushQueuedStdin, readBufferedStdin, waitForBufferedStdin } from '../stdinBuffer';
2
2
  import { isSharedBufferBackedView } from '../sharedBuffer';
3
3
  import { parsePythonPackageLock } from '../pythonPackageLock';
4
+ import { withCachedPyodideModule } from './runtimeModule';
4
5
  import { configureWorkerRuntimeAssetAllowlist, configureWorkerRuntimeAssets, handleWorkerAssetMessage, loadWorkerRuntimeAsset } from './assets';
5
6
  self.document = {
6
7
  querySelector() {
@@ -215,11 +216,11 @@ async function loadPyodide(path) {
215
216
  lockFileContents = parsedLock.lock;
216
217
  }
217
218
  const { loadPyodide } = runtimeModule;
218
- pyodide = await loadPyodide({
219
+ pyodide = await withCachedPyodideModule(runtimeBaseUrl, () => loadPyodide({
219
220
  indexURL: path,
220
221
  packageBaseUrl,
221
222
  ...(lockFileContents ? { lockFileContents } : {})
222
- });
223
+ }));
223
224
  }
224
225
  async function loadPackages(code) {
225
226
  if (!code)
@@ -1,3 +1,4 @@
1
+ import { type RuntimeAssetCacheOptions, type RuntimeAssetIntegrityEntry } from '@wasm-idle/core';
1
2
  export interface RuntimeAssetDownloadProgress {
2
3
  loaded: number;
3
4
  total?: number;
@@ -9,7 +10,16 @@ export interface RuntimeAssetFetchOptions {
9
10
  maxAssetBytes?: number;
10
11
  onProgress?: (progress: RuntimeAssetDownloadProgress) => void;
11
12
  signal?: AbortSignal;
13
+ expected?: RuntimeAssetIntegrityEntry;
14
+ persistentCache?: RuntimeAssetCacheOptions;
15
+ /** Preserve an existing runtime-specific verification boundary, including its diagnostics. */
16
+ verify?: (bytes: Uint8Array) => Promise<unknown>;
17
+ integrityContext?: {
18
+ asset?: string;
19
+ runtimeId?: string;
20
+ profileId?: string;
21
+ };
12
22
  }
13
23
  export declare const DEFAULT_RUNTIME_ASSET_MAX_BYTES: number;
14
24
  export declare function resolveRuntimeAssetUrl(value: string, label: string): URL;
15
- export declare function fetchRuntimeAssetBytes({ url, label, cache, maxAssetBytes, onProgress, signal }: RuntimeAssetFetchOptions): Promise<Uint8Array<ArrayBuffer>>;
25
+ export declare function fetchRuntimeAssetBytes(options: RuntimeAssetFetchOptions): Promise<Uint8Array<ArrayBuffer>>;
@@ -1,3 +1,4 @@
1
+ import { readPersistentRuntimeAsset, resolveRuntimeAssetCacheOptions, verifyRuntimeAssetIntegrity, writePersistentRuntimeAsset } from '@wasm-idle/core';
1
2
  export const DEFAULT_RUNTIME_ASSET_MAX_BYTES = 128 * 1024 * 1024;
2
3
  const DEFAULT_STREAM_BUFFER_BYTES = 64 * 1024;
3
4
  export function resolveRuntimeAssetUrl(value, label) {
@@ -33,7 +34,69 @@ function abortReason(signal) {
33
34
  ? reason
34
35
  : new DOMException('The runtime asset download was aborted', 'AbortError');
35
36
  }
36
- export async function fetchRuntimeAssetBytes({ url, label, cache, maxAssetBytes = DEFAULT_RUNTIME_ASSET_MAX_BYTES, onProgress, signal }) {
37
+ export async function fetchRuntimeAssetBytes(options) {
38
+ // Unpinned custom assets retain their existing network path, with no implicit URL trust.
39
+ if (!options.expected)
40
+ return fetchRuntimeAssetBytesFromNetwork(options);
41
+ const maxAssetBytes = options.maxAssetBytes ?? DEFAULT_RUNTIME_ASSET_MAX_BYTES;
42
+ if (!Number.isSafeInteger(maxAssetBytes) || maxAssetBytes <= 0) {
43
+ throw new TypeError('Runtime asset maxAssetBytes must be a positive safe integer');
44
+ }
45
+ const url = resolveRuntimeAssetUrl(options.url, options.label).href;
46
+ const cache = resolveRuntimeAssetCacheOptions(options.persistentCache);
47
+ const expected = { ...options.expected };
48
+ const candidates = [{ sha256: expected.sha256, bytes: expected.bytes }];
49
+ if (expected.uncompressedSha256 !== undefined && expected.uncompressedBytes !== undefined) {
50
+ candidates.push({ sha256: expected.uncompressedSha256, bytes: expected.uncompressedBytes });
51
+ }
52
+ const validationKey = JSON.stringify([
53
+ 'worker-asset-fetch-v1',
54
+ 'exact-url',
55
+ 'redirect-error',
56
+ 'mime-unchecked',
57
+ maxAssetBytes
58
+ ]);
59
+ for (const receipt of candidates) {
60
+ if (receipt.bytes !== undefined && receipt.bytes > maxAssetBytes)
61
+ continue;
62
+ const hit = await readPersistentRuntimeAsset({
63
+ identity: { url, ...receipt, validationKey },
64
+ cache,
65
+ signal: options.signal
66
+ });
67
+ if (hit) {
68
+ if (hit.byteLength > maxAssetBytes)
69
+ throw new Error(`${options.label} exceeds the ${maxAssetBytes} byte limit`);
70
+ await options.verify?.(hit);
71
+ options.onProgress?.({ loaded: hit.byteLength, total: hit.byteLength });
72
+ if (options.signal?.aborted)
73
+ throw abortReason(options.signal);
74
+ return Uint8Array.from(hit);
75
+ }
76
+ }
77
+ const bytes = await fetchRuntimeAssetBytesFromNetwork(options);
78
+ const isGzip = bytes[0] === 0x1f && bytes[1] === 0x8b;
79
+ const receipt = !isGzip && candidates.length > 1 ? candidates[1] : candidates[0];
80
+ if (options.verify)
81
+ await options.verify(bytes);
82
+ else
83
+ await verifyRuntimeAssetIntegrity({
84
+ asset: url,
85
+ ...options.integrityContext,
86
+ bytes,
87
+ expected: receipt
88
+ });
89
+ await writePersistentRuntimeAsset({
90
+ identity: { url, ...receipt, validationKey },
91
+ cache,
92
+ signal: options.signal,
93
+ bytes
94
+ });
95
+ if (options.signal?.aborted)
96
+ throw abortReason(options.signal);
97
+ return bytes;
98
+ }
99
+ async function fetchRuntimeAssetBytesFromNetwork({ url, label, cache, maxAssetBytes = DEFAULT_RUNTIME_ASSET_MAX_BYTES, onProgress, signal }) {
37
100
  if (!Number.isSafeInteger(maxAssetBytes) || maxAssetBytes <= 0) {
38
101
  throw new TypeError('Runtime asset maxAssetBytes must be a positive safe integer');
39
102
  }
@@ -0,0 +1,8 @@
1
+ /** The host compiles trusted runtime assets; workers never populate the shared module cache. */
2
+ export declare function compileWorkerRuntimeAsset(asset: string): Promise<WebAssembly.Module>;
3
+ /**
4
+ * Pyodide 0.29.3 creates its own instantiateWasm hook, including its sentinel imports.
5
+ * Intercept only the exact bootstrap fetch/Response during initialization so those imports
6
+ * remain intact. Other Wasm (including the sentinel) and all package fetches are untouched.
7
+ */
8
+ export declare function withCachedPyodideModule<T>(baseUrl: string, initialize: () => Promise<T>): Promise<T>;
@@ -0,0 +1,49 @@
1
+ import { decompressGzip } from '@wasm-idle/llvm-core';
2
+ import { hasWorkerRuntimeModuleBridge, loadWorkerRuntimeAsset, loadWorkerRuntimeModule } from './assets';
3
+ /** The host compiles trusted runtime assets; workers never populate the shared module cache. */
4
+ export async function compileWorkerRuntimeAsset(asset) {
5
+ if (hasWorkerRuntimeModuleBridge())
6
+ return await loadWorkerRuntimeModule(asset);
7
+ const { bytes } = await loadWorkerRuntimeAsset(asset);
8
+ const wasm = bytes[0] === 0x1f && bytes[1] === 0x8b ? await decompressGzip(bytes) : bytes;
9
+ return await WebAssembly.compile(wasm.slice().buffer);
10
+ }
11
+ /**
12
+ * Pyodide 0.29.3 creates its own instantiateWasm hook, including its sentinel imports.
13
+ * Intercept only the exact bootstrap fetch/Response during initialization so those imports
14
+ * remain intact. Other Wasm (including the sentinel) and all package fetches are untouched.
15
+ */
16
+ export async function withCachedPyodideModule(baseUrl, initialize) {
17
+ const runtimeUrl = new URL('pyodide.asm.wasm', baseUrl).href;
18
+ // Compile before entering Pyodide, whose bootstrap catches instantiation failures internally.
19
+ const module = await compileWorkerRuntimeAsset('pyodide.asm.wasm');
20
+ const previousFetch = globalThis.fetch;
21
+ const previousInstantiateStreaming = WebAssembly.instantiateStreaming;
22
+ const responses = new WeakSet();
23
+ const fetchRuntime = async (input, init) => {
24
+ const url = input instanceof Request ? input.url : new URL(String(input), baseUrl).href;
25
+ if (url !== runtimeUrl)
26
+ return await previousFetch(input, init);
27
+ const response = new Response(null, { headers: { 'content-type': 'application/wasm' } });
28
+ responses.add(response);
29
+ return response;
30
+ };
31
+ const instantiateRuntime = async (source, imports) => {
32
+ const response = await source;
33
+ if (!responses.has(response))
34
+ return await previousInstantiateStreaming(response, imports);
35
+ return { module, instance: await WebAssembly.instantiate(module, imports) };
36
+ };
37
+ globalThis.fetch = fetchRuntime;
38
+ WebAssembly.instantiateStreaming = instantiateRuntime;
39
+ try {
40
+ return await initialize();
41
+ }
42
+ finally {
43
+ if (globalThis.fetch === fetchRuntime)
44
+ globalThis.fetch = previousFetch;
45
+ if (WebAssembly.instantiateStreaming === instantiateRuntime) {
46
+ WebAssembly.instantiateStreaming = previousInstantiateStreaming;
47
+ }
48
+ }
49
+ }