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.
- package/README.md +46 -5
- package/dist/index.d.ts +1 -0
- package/dist/playground/applicationAssets.js +4 -1
- package/dist/playground/assetBridge.d.ts +6 -5
- package/dist/playground/assetBridge.js +85 -9
- package/dist/playground/assets.d.ts +7 -0
- package/dist/playground/assets.js +11 -0
- package/dist/playground/clang.js +10 -3
- package/dist/playground/clangAssetIntegrity.d.ts +26 -0
- package/dist/playground/clangAssetIntegrity.js +10 -3
- package/dist/playground/clangStreamingPolicy.d.ts +6 -0
- package/dist/playground/clangStreamingPolicy.js +7 -0
- package/dist/playground/dotnet.js +2 -1
- package/dist/playground/index.d.ts +2 -1
- package/dist/playground/index.js +2 -2
- package/dist/playground/java.js +8 -2
- package/dist/playground/loadingProgress.js +2 -1
- package/dist/playground/ruby.js +52 -30
- package/dist/playground/rubyAssets.d.ts +4 -4
- package/dist/playground/rubyAssets.js +10 -3
- package/dist/playground/sandbox.d.ts +4 -0
- package/dist/playground/staticRuntimeModuleVersion.d.ts +1 -1
- package/dist/playground/staticRuntimeModuleVersion.js +1 -1
- package/dist/playground/typescript.js +18 -5
- package/dist/playground/wasmDotnetVersion.d.ts +1 -1
- package/dist/playground/wasmDotnetVersion.js +1 -1
- package/dist/playground/wasmGoVersion.d.ts +1 -1
- package/dist/playground/wasmGoVersion.js +1 -1
- package/dist/playground/wasmRustVersion.d.ts +66 -12
- package/dist/playground/wasmRustVersion.js +71 -11
- package/dist/playground/wasmTypeScriptVersion.d.ts +7 -3
- package/dist/playground/wasmTypeScriptVersion.js +7 -3
- package/dist/playground/worker/assets.js +34 -7
- package/dist/playground/worker/boundedAssetResponse.d.ts +14 -0
- package/dist/playground/worker/boundedAssetResponse.js +148 -0
- package/dist/playground/worker/clang.js +21 -4
- package/dist/playground/worker/clangStreaming.d.ts +3 -0
- package/dist/playground/worker/clangStreaming.js +41 -0
- package/dist/playground/worker/dotnet.js +2 -0
- package/dist/playground/worker/java.js +55 -20
- package/dist/playground/worker/javaStreaming.d.ts +9 -0
- package/dist/playground/worker/javaStreaming.js +99 -0
- package/dist/playground/worker/php.js +52 -12
- package/dist/playground/worker/python.js +37 -9
- package/dist/playground/worker/ruby.js +207 -56
- 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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
73
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
}
|