wasm-idle 1.0.0 → 1.0.4

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 (46) hide show
  1. package/README.md +46 -5
  2. package/dist/index.d.ts +1 -0
  3. package/dist/playground/applicationAssets.js +4 -1
  4. package/dist/playground/assetBridge.d.ts +6 -5
  5. package/dist/playground/assetBridge.js +85 -9
  6. package/dist/playground/assets.d.ts +7 -0
  7. package/dist/playground/assets.js +11 -0
  8. package/dist/playground/clang.js +10 -3
  9. package/dist/playground/clangAssetIntegrity.d.ts +26 -0
  10. package/dist/playground/clangAssetIntegrity.js +10 -3
  11. package/dist/playground/clangStreamingPolicy.d.ts +6 -0
  12. package/dist/playground/clangStreamingPolicy.js +7 -0
  13. package/dist/playground/dotnet.js +2 -1
  14. package/dist/playground/index.d.ts +2 -1
  15. package/dist/playground/index.js +2 -2
  16. package/dist/playground/java.js +8 -2
  17. package/dist/playground/loadingProgress.js +2 -1
  18. package/dist/playground/ruby.js +52 -30
  19. package/dist/playground/rubyAssets.d.ts +4 -4
  20. package/dist/playground/rubyAssets.js +10 -3
  21. package/dist/playground/sandbox.d.ts +4 -0
  22. package/dist/playground/staticRuntimeModuleVersion.d.ts +1 -1
  23. package/dist/playground/staticRuntimeModuleVersion.js +1 -1
  24. package/dist/playground/typescript.js +18 -5
  25. package/dist/playground/wasmDotnetVersion.d.ts +1 -1
  26. package/dist/playground/wasmDotnetVersion.js +1 -1
  27. package/dist/playground/wasmGoVersion.d.ts +1 -1
  28. package/dist/playground/wasmGoVersion.js +1 -1
  29. package/dist/playground/wasmRustVersion.d.ts +66 -12
  30. package/dist/playground/wasmRustVersion.js +71 -11
  31. package/dist/playground/wasmTypeScriptVersion.d.ts +7 -3
  32. package/dist/playground/wasmTypeScriptVersion.js +7 -3
  33. package/dist/playground/worker/assets.js +34 -7
  34. package/dist/playground/worker/boundedAssetResponse.d.ts +14 -0
  35. package/dist/playground/worker/boundedAssetResponse.js +148 -0
  36. package/dist/playground/worker/clang.js +21 -4
  37. package/dist/playground/worker/clangStreaming.d.ts +3 -0
  38. package/dist/playground/worker/clangStreaming.js +41 -0
  39. package/dist/playground/worker/dotnet.js +2 -0
  40. package/dist/playground/worker/java.js +55 -20
  41. package/dist/playground/worker/javaStreaming.d.ts +9 -0
  42. package/dist/playground/worker/javaStreaming.js +99 -0
  43. package/dist/playground/worker/php.js +52 -12
  44. package/dist/playground/worker/python.js +37 -9
  45. package/dist/playground/worker/ruby.js +207 -56
  46. package/package.json +9 -7
@@ -0,0 +1,14 @@
1
+ interface BoundedAssetResponseOptions {
2
+ asset: string;
3
+ maxAssetBytes: number;
4
+ total?: number;
5
+ signal?: AbortSignal;
6
+ onProgress?: (loaded: number, total?: number) => void;
7
+ }
8
+ /**
9
+ * Wrap an already-authorized response without assembling the entire Wasm file in JS.
10
+ * URL, HTTP status and integrity checks belong to the caller. Count browser-decoded
11
+ * bytes before forwarding each chunk; transport Content-Length is not a size bound.
12
+ */
13
+ export declare function createBoundedAssetResponse(response: Response, { asset, maxAssetBytes, total, signal, onProgress }: BoundedAssetResponseOptions): Promise<Response>;
14
+ export {};
@@ -0,0 +1,148 @@
1
+ const abortReason = (signal) => signal.reason ?? new DOMException('Runtime asset load aborted', 'AbortError');
2
+ const bestEffortCancel = (source, reason) => {
3
+ try {
4
+ void Promise.resolve(source.cancel(reason)).catch(() => undefined);
5
+ }
6
+ catch {
7
+ // Cancellation must not replace the original failure or hold up a consumer.
8
+ }
9
+ };
10
+ /**
11
+ * Wrap an already-authorized response without assembling the entire Wasm file in JS.
12
+ * URL, HTTP status and integrity checks belong to the caller. Count browser-decoded
13
+ * bytes before forwarding each chunk; transport Content-Length is not a size bound.
14
+ */
15
+ export async function createBoundedAssetResponse(response, { asset, maxAssetBytes, total, signal, onProgress }) {
16
+ const sizeError = () => new Error(`Runtime asset ${asset} exceeds the ${maxAssetBytes} byte limit`);
17
+ try {
18
+ if (!Number.isSafeInteger(maxAssetBytes) || maxAssetBytes <= 0) {
19
+ throw new TypeError('Runtime asset maxAssetBytes must be a positive safe integer');
20
+ }
21
+ if (total !== undefined && (!Number.isSafeInteger(total) || total < 0)) {
22
+ throw new TypeError('Runtime asset total must be a non-negative safe integer');
23
+ }
24
+ if (total !== undefined && total > maxAssetBytes)
25
+ throw sizeError();
26
+ if (signal?.aborted)
27
+ throw abortReason(signal);
28
+ }
29
+ catch (error) {
30
+ if (response.body)
31
+ bestEffortCancel(response.body, error);
32
+ throw error;
33
+ }
34
+ // Preserve the old synthesized-response headers. In particular, do not propagate
35
+ // transport Content-Encoding or a compressed Content-Length onto decoded bytes.
36
+ const mimeType = response.headers.get('content-type');
37
+ const init = { status: 200, headers: mimeType ? { 'Content-Type': mimeType } : undefined };
38
+ if (!response.body) {
39
+ let cancelOnAbort;
40
+ const aborted = signal
41
+ ? new Promise((_resolve, reject) => {
42
+ cancelOnAbort = () => reject(abortReason(signal));
43
+ signal.addEventListener('abort', cancelOnAbort, { once: true });
44
+ })
45
+ : undefined;
46
+ try {
47
+ const pending = response.arrayBuffer();
48
+ const bytes = aborted ? await Promise.race([pending, aborted]) : await pending;
49
+ if (signal?.aborted)
50
+ throw abortReason(signal);
51
+ if (bytes.byteLength > maxAssetBytes)
52
+ throw sizeError();
53
+ onProgress?.(bytes.byteLength, total ?? bytes.byteLength);
54
+ if (signal?.aborted)
55
+ throw abortReason(signal);
56
+ return new Response(bytes, init);
57
+ }
58
+ finally {
59
+ if (cancelOnAbort)
60
+ signal?.removeEventListener('abort', cancelOnAbort);
61
+ }
62
+ }
63
+ const reader = response.body.getReader();
64
+ let settled = false;
65
+ let released = false;
66
+ let receivedLength = 0;
67
+ let outputController;
68
+ const release = () => {
69
+ if (released)
70
+ return;
71
+ released = true;
72
+ reader.releaseLock();
73
+ };
74
+ const detach = () => signal?.removeEventListener('abort', onAbort);
75
+ const stop = (reason) => {
76
+ if (settled)
77
+ return;
78
+ settled = true;
79
+ detach();
80
+ bestEffortCancel(reader, reason);
81
+ try {
82
+ release();
83
+ }
84
+ catch {
85
+ // Keep the cancellation, stream failure, or size violation authoritative.
86
+ }
87
+ };
88
+ const fail = (reason) => {
89
+ if (settled)
90
+ return;
91
+ stop(reason);
92
+ outputController.error(reason);
93
+ };
94
+ const onAbort = () => {
95
+ if (signal)
96
+ fail(abortReason(signal));
97
+ };
98
+ const body = new ReadableStream({
99
+ start(controller) {
100
+ outputController = controller;
101
+ signal?.addEventListener('abort', onAbort, { once: true });
102
+ if (signal?.aborted)
103
+ onAbort();
104
+ },
105
+ async pull(controller) {
106
+ if (settled)
107
+ return;
108
+ try {
109
+ const { done, value } = await reader.read();
110
+ // A cancelled reader may settle an outstanding read later. It must not
111
+ // enqueue bytes or emit successful completion after cancellation.
112
+ if (settled)
113
+ return;
114
+ if (signal?.aborted)
115
+ throw abortReason(signal);
116
+ if (done) {
117
+ release();
118
+ onProgress?.(receivedLength, total ?? receivedLength);
119
+ if (settled)
120
+ return;
121
+ settled = true;
122
+ detach();
123
+ controller.close();
124
+ return;
125
+ }
126
+ if (!value)
127
+ return;
128
+ const nextLength = receivedLength + value.byteLength;
129
+ if (!Number.isSafeInteger(nextLength) || nextLength > maxAssetBytes) {
130
+ throw sizeError();
131
+ }
132
+ receivedLength = nextLength;
133
+ onProgress?.(receivedLength, total);
134
+ if (!settled)
135
+ controller.enqueue(value);
136
+ }
137
+ catch (error) {
138
+ fail(signal?.aborted ? abortReason(signal) : error);
139
+ }
140
+ },
141
+ cancel(reason) {
142
+ stop(reason);
143
+ }
144
+ },
145
+ // Pull only when the Wasm consumer asks for data; no eager read-ahead or tee.
146
+ { highWaterMark: 0 });
147
+ return new Response(body, init);
148
+ }
@@ -2,6 +2,8 @@ import { normalizeDwarfWorkspacePath } from '@wasm-idle/llvm-core/clang';
2
2
  import { waitForBufferedStdin } from '../stdinBuffer';
3
3
  import { isSharedBufferBackedView } from '../sharedBuffer';
4
4
  import { configureWorkerRuntimeAssets, handleWorkerAssetMessage } from './assets';
5
+ import { withVerifiedStreaming } from './clangStreaming';
6
+ import { BUNDLED_CLANG_LANGUAGE_SYSROOT_PROFILES } from '../clangAssetIntegrity';
5
7
  self.document = {
6
8
  querySelectorAll() {
7
9
  return [];
@@ -13,10 +15,25 @@ let initialStdinClang = null;
13
15
  function postProgress(percent, stage) {
14
16
  postMessage({ progress: { percent, stage } });
15
17
  }
16
- async function loadClang(path, log, maxAssetBytes) {
18
+ async function loadClang(path, log, maxAssetBytes, verifiedStreaming = false, languageSysrootProfiles = false) {
17
19
  const { BrowserClangRuntime, loadRuntimeManifest, resolveRuntimeManifestUrl } = await import('@wasm-idle/llvm-core/clang');
18
20
  const manifest = await loadRuntimeManifest(resolveRuntimeManifestUrl(path), fetch, undefined, maxAssetBytes);
19
- clang = new BrowserClangRuntime({
21
+ const runtimeManifest = languageSysrootProfiles
22
+ ? {
23
+ ...manifest,
24
+ compiler: {
25
+ ...manifest.compiler,
26
+ sysroot: {
27
+ ...manifest.compiler.sysroot,
28
+ profiles: BUNDLED_CLANG_LANGUAGE_SYSROOT_PROFILES
29
+ }
30
+ }
31
+ }
32
+ : manifest;
33
+ const Runtime = verifiedStreaming
34
+ ? withVerifiedStreaming(BrowserClangRuntime, path, maxAssetBytes)
35
+ : BrowserClangRuntime;
36
+ clang = new Runtime({
20
37
  stdout: (output) => postMessage({ output }),
21
38
  onDebugEvent: (debugEvent) => postMessage({ debugEvent }),
22
39
  stdin: () => {
@@ -31,7 +48,7 @@ async function loadClang(path, log, maxAssetBytes) {
31
48
  log,
32
49
  maxAssetBytes,
33
50
  runtimeBaseUrl: path,
34
- manifest
51
+ manifest: runtimeManifest
35
52
  });
36
53
  await clang.ready;
37
54
  }
@@ -44,7 +61,7 @@ self.onmessage = async (event) => {
44
61
  try {
45
62
  const runtimeAssets = assets;
46
63
  configureWorkerRuntimeAssets(runtimeAssets || null);
47
- await loadClang(runtimeAssets?.baseUrl || path || '', log, maxAssetBytes);
64
+ await loadClang(runtimeAssets?.baseUrl || path || '', log, maxAssetBytes, event.data.verifiedStreaming === true, event.data.languageSysrootProfiles === true);
48
65
  postMessage({ load: true });
49
66
  }
50
67
  catch (error) {
@@ -0,0 +1,3 @@
1
+ import type { BrowserClangRuntime } from '@wasm-idle/llvm-core/clang';
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;
@@ -0,0 +1,41 @@
1
+ import { compileVerifiedWasmAsset } from '@wasm-idle/llvm-core/core/verified-wasm';
2
+ import { BUNDLED_CLANG_ASSET_INTEGRITY } from '../clangAssetIntegrity';
3
+ // Capture before configureWorkerRuntimeAssets installs its whole-buffer fetch shim.
4
+ const nativeFetch = globalThis.fetch?.bind(globalThis);
5
+ const toolNames = ['bin/clang.wasm.gz', 'bin/lld.wasm.gz'];
6
+ /** 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
+ return Runtime;
10
+ const urls = new Map(toolNames.map((name) => [new URL(name, baseUrl).href, name]));
11
+ // Scope the two immutable tool modules to this one runtime construction, not globals.
12
+ const loads = new Map();
13
+ return class extends Runtime {
14
+ async getModule(url, progress, signal) {
15
+ const name = urls.get(url);
16
+ if (!name)
17
+ return super.getModule(url, progress, signal);
18
+ signal?.throwIfAborted();
19
+ const cached = signal ? undefined : loads.get(url);
20
+ if (cached)
21
+ return cached;
22
+ const operation = compileVerifiedWasmAsset(url, BUNDLED_CLANG_ASSET_INTEGRITY[name], {
23
+ fetch: nativeFetch,
24
+ maxAssetBytes,
25
+ signal,
26
+ onProgress(loaded, total) {
27
+ progress?.set?.(loaded / total);
28
+ globalThis.postMessage?.({ assetProgress: { asset: name, loaded, total } });
29
+ }
30
+ });
31
+ if (!signal) {
32
+ loads.set(url, operation);
33
+ void operation.catch(() => {
34
+ if (loads.get(url) === operation)
35
+ loads.delete(url);
36
+ });
37
+ }
38
+ return operation;
39
+ }
40
+ };
41
+ }
@@ -56,6 +56,8 @@ workerSelf.onmessage = async (event) => {
56
56
  console.log(`[wasm-idle:dotnet-worker] load moduleUrl=${moduleUrl}`);
57
57
  }
58
58
  await loadRuntime(moduleUrl);
59
+ // Older/custom runtime modules remain compatible without this optional API.
60
+ await compiler?.prepare?.({ language });
59
61
  postMessage({ load: true });
60
62
  return;
61
63
  }
@@ -1,3 +1,4 @@
1
+ import { acceptCompiledTeaVmModule, loadStreamingJavaCompiler } from './javaStreaming';
1
2
  import { prepareJavaStdinInjection } from '../javaStdin';
2
3
  import { resolveJavaSourceIdentity } from '../javaSource';
3
4
  import { waitForBufferedStdin } from '../stdinBuffer';
@@ -15,6 +16,7 @@ let compiledCode = '';
15
16
  let compiledStdin = '';
16
17
  let compiledMainClass = '';
17
18
  let compiledWasm = null;
19
+ let compiledDiagnostics = [];
18
20
  let compiledActivePath = '';
19
21
  let compiledWorkspaceFiles = [];
20
22
  let currentSourcePaths = new Set();
@@ -51,30 +53,52 @@ self.addEventListener('message', async (event) => {
51
53
  const runtimeAssets = assets;
52
54
  configureWorkerRuntimeAssets(runtimeAssets || null);
53
55
  const baseUrl = runtimeAssets?.baseUrl || '';
56
+ const streamCompiler = runtimeAssets?.streamCompiler === true;
54
57
  if (!compiler || loadedBaseUrl !== baseUrl) {
55
- loadedBaseUrl = baseUrl;
56
- const runtimeSource = decoder.decode((await loadWorkerRuntimeAsset('compiler.wasm-runtime.js')).bytes);
57
- const runtimeUrl = URL.createObjectURL(new Blob([runtimeSource], { type: 'text/javascript;charset=utf-8' }));
58
- const runtimeModule = await import(/* @vite-ignore */ runtimeUrl);
59
- URL.revokeObjectURL(runtimeUrl);
60
- const runtimeLoadFn = runtimeModule.load;
61
- runtimeLoad = runtimeLoadFn;
62
- const compilerWasm = (await loadWorkerRuntimeAsset('compiler.wasm')).bytes;
63
- const compilerModule = await runtimeLoadFn(compilerWasm, {
64
- stackDeobfuscator: { enabled: false }
65
- });
66
- compilerLib = compilerModule.exports;
67
- compiler = compilerLib.createCompiler();
68
- const [sdk, runtimeClasslib] = await Promise.all([
58
+ // Observe every branch immediately: failed assets must not leave an
59
+ // unhandled rejection while another branch is still initializing Wasm.
60
+ const [nextRuntime, sdk, runtimeClasslib] = await Promise.all([
61
+ (async () => {
62
+ const [runtimeModule, compilerAsset] = await Promise.all([
63
+ (async () => {
64
+ let runtimeSource = decoder.decode((await loadWorkerRuntimeAsset('compiler.wasm-runtime.js')).bytes);
65
+ if (streamCompiler)
66
+ runtimeSource = acceptCompiledTeaVmModule(runtimeSource);
67
+ const runtimeUrl = URL.createObjectURL(new Blob([runtimeSource], { type: 'text/javascript;charset=utf-8' }));
68
+ try {
69
+ return await import(/* @vite-ignore */ runtimeUrl);
70
+ }
71
+ finally {
72
+ URL.revokeObjectURL(runtimeUrl);
73
+ }
74
+ })(),
75
+ (async () => {
76
+ const module = streamCompiler ? await loadStreamingJavaCompiler(baseUrl, runtimeAssets?.maxAssetBytes ?? 128 * 1024 * 1024) : undefined;
77
+ return module ? { bytes: module } : loadWorkerRuntimeAsset('compiler.wasm');
78
+ })()
79
+ ]);
80
+ const load = runtimeModule.load;
81
+ const module = await load(compilerAsset.bytes, {
82
+ stackDeobfuscator: { enabled: false }
83
+ });
84
+ return { load, lib: module.exports, compiler: module.exports.createCompiler() };
85
+ })(),
69
86
  loadWorkerRuntimeAsset('compile-classlib-teavm.bin').then(({ bytes }) => toInt8Array(bytes)),
70
87
  loadWorkerRuntimeAsset('runtime-classlib-teavm.bin').then(({ bytes }) => toInt8Array(bytes))
71
88
  ]);
72
- compiler.setSdk(sdk);
73
- compiler.setTeaVMClasslib(runtimeClasslib);
89
+ // Publish only a fully initialized compiler; a failed first attempt
90
+ // must retry instead of acknowledging a partially initialized runtime.
91
+ nextRuntime.compiler.setSdk(sdk);
92
+ nextRuntime.compiler.setTeaVMClasslib(runtimeClasslib);
93
+ compilerLib = nextRuntime.lib;
94
+ compiler = nextRuntime.compiler;
95
+ runtimeLoad = nextRuntime.load;
96
+ loadedBaseUrl = baseUrl;
74
97
  compiledCode = '';
75
98
  compiledStdin = '';
76
99
  compiledMainClass = '';
77
100
  compiledWasm = null;
101
+ compiledDiagnostics = [];
78
102
  compiledActivePath = '';
79
103
  compiledWorkspaceFiles = [];
80
104
  currentSourcePaths = new Set();
@@ -106,17 +130,20 @@ self.addEventListener('message', async (event) => {
106
130
  sourceFiles.some((file) => file.path === stdinInjection.helperSourcePath))) {
107
131
  throw new Error(`Java workspace conflicts with generated stdin helper: ${stdinInjection.helperSourcePath}`);
108
132
  }
109
- if (prepare ||
110
- compiledCode !== code ||
133
+ if (compiledCode !== code ||
111
134
  compiledStdin !== stdinInjection.stdinCacheKey ||
112
135
  workspaceChanged ||
113
136
  !compiledWasm) {
137
+ // A failed rebuild must never leave a previous artifact eligible for reuse.
138
+ compiledWasm = null;
139
+ compiledDiagnostics = [];
114
140
  const mainClass = sourceIdentity.mainClass;
115
141
  currentSourcePaths = new Set([sourcePath, ...sourceFiles.map((file) => file.path)].flatMap((path) => [
116
142
  path,
117
143
  path.split('/').pop() || path
118
144
  ]));
119
145
  const diagnosticLines = [];
146
+ const diagnosticMessages = [];
120
147
  const diagnosticRegistration = compiler.onDiagnostic((diagnostic) => {
121
148
  const severity = diagnostic.severity
122
149
  ? String(diagnostic.severity).toLowerCase()
@@ -129,7 +156,7 @@ self.addEventListener('message', async (event) => {
129
156
  if (fileName && !currentSourcePaths.has(fileName)) {
130
157
  return;
131
158
  }
132
- self.postMessage({
159
+ const diagnosticMessage = {
133
160
  diagnostic: {
134
161
  fileName,
135
162
  lineNumber: Number(diagnostic.lineNumber) || 1,
@@ -141,7 +168,9 @@ self.addEventListener('message', async (event) => {
141
168
  : 'error',
142
169
  message: String(diagnostic.message || '')
143
170
  }
144
- });
171
+ };
172
+ diagnosticMessages.push(diagnosticMessage);
173
+ self.postMessage(diagnosticMessage);
145
174
  });
146
175
  const disposeDiagnosticRegistration = () => {
147
176
  if (typeof diagnosticRegistration === 'function') {
@@ -193,6 +222,12 @@ self.addEventListener('message', async (event) => {
193
222
  compiledWasm = new Uint8Array(compiler.getWebAssemblyOutputFile('app.wasm'));
194
223
  compiledActivePath = sourcePath;
195
224
  compiledWorkspaceFiles = sourceFiles.map((file) => ({ ...file }));
225
+ compiledDiagnostics = diagnosticMessages;
226
+ }
227
+ else {
228
+ // Every operation has its own diagnostic sink, even when compilation is reused.
229
+ for (const diagnostic of compiledDiagnostics)
230
+ self.postMessage(diagnostic);
196
231
  }
197
232
  if (prepare) {
198
233
  self.postMessage({ results: true });
@@ -0,0 +1,9 @@
1
+ /** No instance is created until EOF, exact length and SHA-256 have all passed. */
2
+ export declare function compileVerifiedWasmResponse(response: Response, receipt: {
3
+ bytes: number;
4
+ sha256: string;
5
+ }, maxAssetBytes: number, onProgress?: (loaded: number, total: number) => void): Promise<WebAssembly.Module>;
6
+ /** Only enabled by the host for the pinned, non-custom-loader TeaVM profile. */
7
+ export declare function loadStreamingJavaCompiler(baseUrl: string, maxAssetBytes: number): Promise<WebAssembly.Module | undefined>;
8
+ /** Transform only the known verified loader, preserving TeaVM imports and exports. */
9
+ export declare function acceptCompiledTeaVmModule(source: string): string;
@@ -0,0 +1,99 @@
1
+ import { TEAVM_RUNTIME_ASSET_RECEIPTS } from '@wasm-idle/core';
2
+ // Capture before the worker asset shim installs its buffered fetch adapter.
3
+ const nativeFetch = globalThis.fetch?.bind(globalThis);
4
+ const compilerReceipt = TEAVM_RUNTIME_ASSET_RECEIPTS['compiler.wasm'];
5
+ /** No instance is created until EOF, exact length and SHA-256 have all passed. */
6
+ export async function compileVerifiedWasmResponse(response, receipt, maxAssetBytes, onProgress) {
7
+ if (!Number.isSafeInteger(maxAssetBytes) || maxAssetBytes < 1 || receipt.bytes > maxAssetBytes ||
8
+ !Number.isSafeInteger(receipt.bytes) || receipt.bytes < 1 || !/^[a-f0-9]{64}$/.test(receipt.sha256)) {
9
+ void response.body?.cancel().catch(() => { });
10
+ throw new Error('Java compiler exceeds its asset limit or has an invalid receipt');
11
+ }
12
+ if (!response.ok || !response.body || !globalThis.crypto?.subtle) {
13
+ void response.body?.cancel().catch(() => { });
14
+ throw new Error('Java compiler streaming requires a successful body and Web Crypto');
15
+ }
16
+ const reader = response.body.getReader();
17
+ const snapshot = new Uint8Array(receipt.bytes);
18
+ let received = 0;
19
+ let released = false;
20
+ const release = () => { if (!released) {
21
+ released = true;
22
+ reader.releaseLock();
23
+ } };
24
+ const cancel = async (reason) => {
25
+ if (released)
26
+ return;
27
+ try {
28
+ await reader.cancel(reason);
29
+ }
30
+ finally {
31
+ release();
32
+ }
33
+ };
34
+ const body = new ReadableStream({
35
+ async pull(controller) {
36
+ try {
37
+ const { done, value } = await reader.read();
38
+ if (done) {
39
+ if (received !== receipt.bytes)
40
+ throw new Error('Java compiler length mismatch');
41
+ const digest = await crypto.subtle.digest('SHA-256', snapshot);
42
+ const actual = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('');
43
+ if (actual !== receipt.sha256)
44
+ throw new Error('Java compiler SHA-256 mismatch');
45
+ release();
46
+ controller.close();
47
+ return;
48
+ }
49
+ if (received + value.byteLength > receipt.bytes)
50
+ throw new Error('Java compiler exceeds its receipt byte limit');
51
+ snapshot.set(value, received);
52
+ received += value.byteLength;
53
+ // Own every chunk: mutable producer views cannot change after hashing.
54
+ controller.enqueue(Uint8Array.from(value));
55
+ onProgress?.(received, receipt.bytes);
56
+ }
57
+ catch (error) {
58
+ controller.error(error);
59
+ await cancel(error).catch(() => { });
60
+ }
61
+ },
62
+ cancel
63
+ });
64
+ try {
65
+ return await WebAssembly.compileStreaming(new Response(body, { headers: { 'Content-Type': 'application/wasm' } }), { builtins: ['js-string'] });
66
+ }
67
+ catch (error) {
68
+ await cancel(error).catch(() => { });
69
+ throw error;
70
+ }
71
+ }
72
+ /** Only enabled by the host for the pinned, non-custom-loader TeaVM profile. */
73
+ export async function loadStreamingJavaCompiler(baseUrl, maxAssetBytes) {
74
+ if (typeof WebAssembly.compileStreaming !== 'function' || !globalThis.crypto?.subtle || !nativeFetch)
75
+ return undefined;
76
+ const url = new URL('compiler.wasm', baseUrl);
77
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash)
78
+ throw new Error('Invalid Java compiler URL');
79
+ const response = await nativeFetch(url.href, { credentials: 'omit', redirect: 'error', referrerPolicy: 'no-referrer' });
80
+ if (response.url && new URL(response.url).href !== url.href) {
81
+ void response.body?.cancel().catch(() => { });
82
+ throw new Error('Java compiler response URL mismatch');
83
+ }
84
+ // Static hosts without logical .wasm delivery retain the verified buffered path.
85
+ if (response.status === 404) {
86
+ void response.body?.cancel().catch(() => { });
87
+ return undefined;
88
+ }
89
+ return await compileVerifiedWasmResponse(response, compilerReceipt, maxAssetBytes, (loaded, total) => {
90
+ globalThis.postMessage?.({ assetProgress: { asset: 'compiler.wasm', loaded, total } });
91
+ });
92
+ }
93
+ /** Transform only the known verified loader, preserving TeaVM imports and exports. */
94
+ export function acceptCompiledTeaVmModule(source) {
95
+ const anchor = 'async function b(e,t){if(typeof e!=="string")';
96
+ if (source.split(anchor).length !== 2)
97
+ throw new Error('Unsupported TeaVM loader for compiled-module input');
98
+ return source.replace(anchor, 'async function b(e,t){if(e instanceof WebAssembly.Module)return e;if(typeof e!=="string")');
99
+ }
@@ -5,6 +5,17 @@ let stdinBufferPhp = null;
5
5
  let runtimeModuleUrl = '';
6
6
  let phpPromise = null;
7
7
  let workspaceDirty = false;
8
+ // A superseded attempt must not send errors to a newer load or run's listener.
9
+ class SupersededPhpStartup extends Error {
10
+ }
11
+ function disposePhp(php) {
12
+ try {
13
+ php.exit?.();
14
+ }
15
+ catch {
16
+ // Preserve the startup failure or replacement that triggered cleanup.
17
+ }
18
+ }
8
19
  function postProgress(percent) {
9
20
  postMessage({ progress: { percent: Math.max(0, Math.min(100, percent)) } });
10
21
  }
@@ -12,25 +23,52 @@ async function loadPhp(moduleUrl, log = true) {
12
23
  if (!moduleUrl)
13
24
  throw new Error('PHP runtime module URL is not configured.');
14
25
  if (runtimeModuleUrl !== moduleUrl) {
26
+ const previous = phpPromise;
15
27
  runtimeModuleUrl = moduleUrl;
16
28
  phpPromise = null;
17
29
  workspaceDirty = false;
30
+ void previous?.then(disposePhp, () => { });
18
31
  }
19
32
  if (phpPromise)
20
33
  return await phpPromise;
21
- phpPromise = (async () => {
22
- postProgress(5);
23
- const runtime = await importRuntimeModule(moduleUrl);
24
- const php = await runtime.createPhp84();
25
- postProgress(95);
26
- php.mkdir('/workspace');
27
- if (log) {
28
- console.log('[wasm-idle:php-worker] PHP 8.4 ready');
34
+ const operation = Promise.resolve().then(async () => {
35
+ let php;
36
+ try {
37
+ postProgress(5);
38
+ const runtime = await importRuntimeModule(moduleUrl);
39
+ if (phpPromise !== operation || runtimeModuleUrl !== moduleUrl) {
40
+ throw new SupersededPhpStartup('PHP runtime startup was superseded.');
41
+ }
42
+ php = await runtime.createPhp84();
43
+ if (phpPromise !== operation || runtimeModuleUrl !== moduleUrl) {
44
+ throw new SupersededPhpStartup('PHP runtime startup was superseded.');
45
+ }
46
+ postProgress(95);
47
+ php.mkdir('/workspace');
48
+ if (log) {
49
+ console.log('[wasm-idle:php-worker] PHP 8.4 ready');
50
+ }
51
+ postProgress(100);
52
+ return php;
53
+ }
54
+ catch (error) {
55
+ if (php)
56
+ disposePhp(php);
57
+ throw error;
29
58
  }
30
- postProgress(100);
31
- return php;
32
- })();
33
- return await phpPromise;
59
+ });
60
+ phpPromise = operation;
61
+ try {
62
+ return await operation;
63
+ }
64
+ catch (error) {
65
+ // Failed speculative startup must not poison the later explicit Run.
66
+ // A stale attempt must never clear a newer configuration's runtime.
67
+ if (phpPromise !== operation)
68
+ throw new SupersededPhpStartup('PHP runtime startup was superseded.');
69
+ phpPromise = null;
70
+ throw error;
71
+ }
34
72
  }
35
73
  function prepareWorkspace(php) {
36
74
  if (workspaceDirty) {
@@ -253,6 +291,8 @@ self.onmessage = async (event) => {
253
291
  postMessage({ results: true });
254
292
  }
255
293
  catch (error) {
294
+ if (error instanceof SupersededPhpStartup)
295
+ return;
256
296
  if (log) {
257
297
  console.error('[wasm-idle:php-worker] failed', error);
258
298
  }