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
|
@@ -11,6 +11,8 @@ self.document = {
|
|
|
11
11
|
}
|
|
12
12
|
};
|
|
13
13
|
let stdinBufferPyodide, debugBufferPyodide, watchBufferPyodide, watchResultBufferPyodide, interruptBufferPyodide, pyodide, baseUrl = '', useAssetBridge = false;
|
|
14
|
+
// Only the next matching run may reuse a successful package preparation.
|
|
15
|
+
let preparedPackagesKey;
|
|
14
16
|
const imageHook = `
|
|
15
17
|
if not globals().get("__wasm_idle_img_inited__", False):
|
|
16
18
|
globals()["__wasm_idle_img_inited__"] = True
|
|
@@ -172,8 +174,7 @@ const resolvePinnedPackageBaseUrl = (version) => {
|
|
|
172
174
|
function postProgress(percent, stage) {
|
|
173
175
|
self.postMessage({ progress: { percent, stage } });
|
|
174
176
|
}
|
|
175
|
-
async function importRuntimeAssetModule(
|
|
176
|
-
const loaded = await loadWorkerRuntimeAsset(asset);
|
|
177
|
+
async function importRuntimeAssetModule(loaded) {
|
|
177
178
|
const moduleUrl = URL.createObjectURL(new Blob([loaded.bytes.slice().buffer], {
|
|
178
179
|
type: loaded.mimeType || 'text/javascript'
|
|
179
180
|
}));
|
|
@@ -188,13 +189,21 @@ async function loadPyodide(path) {
|
|
|
188
189
|
if (pyodide)
|
|
189
190
|
return;
|
|
190
191
|
const runtimeBaseUrl = path.endsWith('/') ? path : `${path}/`;
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
// Download independent bootstrap files together, but preserve module evaluation order.
|
|
193
|
+
// The existing bounded loader (and host bridge, when configured) still owns every byte.
|
|
194
|
+
const [asmAsset, runtimeAsset, loadedLock] = await Promise.all([
|
|
195
|
+
loadWorkerRuntimeAsset('pyodide.asm.js'),
|
|
196
|
+
loadWorkerRuntimeAsset('pyodide.mjs'),
|
|
197
|
+
useAssetBridge ? undefined : loadWorkerRuntimeAsset('pyodide-lock.json')
|
|
198
|
+
]);
|
|
199
|
+
await importRuntimeAssetModule(asmAsset);
|
|
200
|
+
const runtimeModule = (await importRuntimeAssetModule(runtimeAsset));
|
|
193
201
|
let packageBaseUrl = runtimeBaseUrl;
|
|
194
202
|
let lockFileContents;
|
|
195
203
|
if (!useAssetBridge) {
|
|
196
204
|
packageBaseUrl = resolvePinnedPackageBaseUrl(runtimeModule.version);
|
|
197
|
-
|
|
205
|
+
if (!loadedLock)
|
|
206
|
+
throw new Error('Python runtime lock file is unavailable');
|
|
198
207
|
const parsedLock = parsePythonPackageLock(loadedLock.bytes);
|
|
199
208
|
// The interceptor caps downloaded (including browser-decoded transport) archive bytes.
|
|
200
209
|
// Pyodide owns package extraction after receiving that bounded archive.
|
|
@@ -217,6 +226,14 @@ async function loadPackages(code) {
|
|
|
217
226
|
return;
|
|
218
227
|
await pyodide.loadPackagesFromImports(code);
|
|
219
228
|
}
|
|
229
|
+
function packagePreparationKey(code, activePath, files = []) {
|
|
230
|
+
// Keep source boundaries and paths: joining contents alone can alias different workspaces.
|
|
231
|
+
return JSON.stringify([
|
|
232
|
+
code,
|
|
233
|
+
activePath ?? null,
|
|
234
|
+
files.map(({ path, content }) => [path, content])
|
|
235
|
+
]);
|
|
236
|
+
}
|
|
220
237
|
function normalizeWorkspacePath(path) {
|
|
221
238
|
return path
|
|
222
239
|
.replaceAll('\\', '/')
|
|
@@ -241,6 +258,7 @@ self.onmessage = async (event) => {
|
|
|
241
258
|
return;
|
|
242
259
|
const { code, buffer, debugBuffer, watchBuffer, watchResultBuffer, load, interrupt, assets, prepare, stdin, debug = false, breakpoints = [], pauseOnEntry = false, activePath, debugPath, workspaceFiles } = event.data;
|
|
243
260
|
if (load) {
|
|
261
|
+
preparedPackagesKey = undefined;
|
|
244
262
|
try {
|
|
245
263
|
const runtimeAssets = assets;
|
|
246
264
|
baseUrl = runtimeAssets?.baseUrl || baseUrl;
|
|
@@ -258,9 +276,11 @@ self.onmessage = async (event) => {
|
|
|
258
276
|
}
|
|
259
277
|
}
|
|
260
278
|
else if (prepare) {
|
|
279
|
+
preparedPackagesKey = undefined;
|
|
261
280
|
postMessage({ output: 'Loading packages...' });
|
|
262
281
|
try {
|
|
263
282
|
postProgress(5, 'Preparing Python workspace');
|
|
283
|
+
const preparationKey = packagePreparationKey(code, activePath, workspaceFiles);
|
|
264
284
|
await loadPyodide(baseUrl);
|
|
265
285
|
writeWorkspaceFiles(workspaceFiles);
|
|
266
286
|
postProgress(15, 'Resolving Python imports');
|
|
@@ -268,22 +288,30 @@ self.onmessage = async (event) => {
|
|
|
268
288
|
code,
|
|
269
289
|
...(workspaceFiles || []).map((file) => file.content)
|
|
270
290
|
].join('\n'));
|
|
291
|
+
preparedPackagesKey = preparationKey;
|
|
271
292
|
postProgress(100, 'Python packages ready');
|
|
272
293
|
postMessage({ output: ' Done.\n\r' });
|
|
273
294
|
self.postMessage({ results: true });
|
|
274
295
|
}
|
|
275
296
|
catch (e) {
|
|
297
|
+
preparedPackagesKey = undefined;
|
|
276
298
|
self.postMessage({ error: e.message || 'Unknown error' });
|
|
277
299
|
}
|
|
278
300
|
}
|
|
279
301
|
else if (typeof code === 'string') {
|
|
302
|
+
const preparedKey = preparedPackagesKey;
|
|
303
|
+
// Consume before any await or user code, including executions that fail. Python can
|
|
304
|
+
// mutate imports and the filesystem, so this is not a cross-execution package cache.
|
|
305
|
+
preparedPackagesKey = undefined;
|
|
280
306
|
try {
|
|
281
307
|
await loadPyodide(baseUrl);
|
|
282
308
|
writeWorkspaceFiles(workspaceFiles);
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
309
|
+
if (preparedKey !== packagePreparationKey(code, activePath, workspaceFiles)) {
|
|
310
|
+
await loadPackages([
|
|
311
|
+
code,
|
|
312
|
+
...(workspaceFiles || []).map((file) => file.content)
|
|
313
|
+
].join('\n'));
|
|
314
|
+
}
|
|
287
315
|
}
|
|
288
316
|
catch (e) {
|
|
289
317
|
self.postMessage({ error: e.message || 'Unknown error' });
|
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
import { waitForBufferedStdin } from '../stdinBuffer';
|
|
2
2
|
import { importRuntimeModule } from '../runtimeModule';
|
|
3
|
-
import { RUBY_RUNTIME_VERIFIED_WASM_URL, rewriteVerifiedRubyRuntimeModule, verifyRubyRuntimePreflightPayload } from '@wasm-idle/core';
|
|
3
|
+
import { RUBY_RUNTIME_VERIFIED_WASM_URL, normalizeWorkspacePath, RUBY_SPLIT_PROTOCOL, verifyRubySplitPayload, rewriteRubySplitRuntimeModule, parseRubyStdlibPack, createRubyStdlibPreopens, rewriteVerifiedRubyRuntimeModule, verifyRubyRuntimePreflightPayload } from '@wasm-idle/core';
|
|
4
4
|
const encoder = new TextEncoder();
|
|
5
5
|
let stdinBufferRuby = null;
|
|
6
6
|
let runtimeState = 'uninitialized';
|
|
7
7
|
let loadedRuntime = null;
|
|
8
|
-
|
|
8
|
+
let preparedExecution = null;
|
|
9
|
+
async function loadRubyModule(runtimePreflight, maxAssetBytes, context) {
|
|
9
10
|
if (runtimeState !== 'uninitialized') {
|
|
10
11
|
throw new Error('Ruby runtime is already loaded.');
|
|
11
12
|
}
|
|
12
13
|
runtimeState = 'loading';
|
|
13
14
|
try {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
const split = !!runtimePreflight &&
|
|
16
|
+
typeof runtimePreflight === 'object' &&
|
|
17
|
+
Object.getOwnPropertyDescriptor(runtimePreflight, 'protocol')?.value ===
|
|
18
|
+
RUBY_SPLIT_PROTOCOL;
|
|
19
|
+
const payload = split
|
|
20
|
+
? await verifyRubySplitPayload(runtimePreflight, { maxAssetBytes })
|
|
21
|
+
: await verifyRubyRuntimePreflightPayload(runtimePreflight, {
|
|
22
|
+
maxAssetBytes
|
|
23
|
+
});
|
|
24
|
+
const buffers = [
|
|
25
|
+
payload.manifestBytes,
|
|
26
|
+
payload.moduleJavaScriptBytes,
|
|
27
|
+
payload.wasmBytes,
|
|
28
|
+
...('stdlibBytes' in payload ? [payload.stdlibBytes] : [])
|
|
29
|
+
];
|
|
18
30
|
if (buffers.some((bytes) => !ArrayBuffer.isView(bytes) ||
|
|
19
31
|
Object.prototype.toString.call(bytes) !== '[object Uint8Array]' ||
|
|
20
32
|
!(bytes.buffer instanceof ArrayBuffer) ||
|
|
@@ -24,7 +36,9 @@ async function loadRubyModule(runtimePreflight, maxAssetBytes) {
|
|
|
24
36
|
throw new TypeError('Ruby execution worker requires unique owned runtime preflight bytes');
|
|
25
37
|
}
|
|
26
38
|
const wasmBytes = payload.wasmBytes;
|
|
27
|
-
const moduleSource =
|
|
39
|
+
const moduleSource = 'stdlibBytes' in payload
|
|
40
|
+
? rewriteRubySplitRuntimeModule(payload)
|
|
41
|
+
: rewriteVerifiedRubyRuntimeModule(payload);
|
|
28
42
|
if (typeof Blob !== 'function' ||
|
|
29
43
|
typeof URL.createObjectURL !== 'function' ||
|
|
30
44
|
typeof URL.revokeObjectURL !== 'function') {
|
|
@@ -53,23 +67,44 @@ async function loadRubyModule(runtimePreflight, maxAssetBytes) {
|
|
|
53
67
|
runtime.rubyStdlibWasmUrl !== RUBY_RUNTIME_VERIFIED_WASM_URL) {
|
|
54
68
|
throw new Error('Ruby runtime module is missing required verified Ruby or WASI exports.');
|
|
55
69
|
}
|
|
56
|
-
loadedRuntime = {
|
|
70
|
+
loadedRuntime = {
|
|
71
|
+
module,
|
|
72
|
+
runtime,
|
|
73
|
+
...('stdlibBytes' in payload
|
|
74
|
+
? { stdlib: parseRubyStdlibPack(payload.stdlibBytes) }
|
|
75
|
+
: {})
|
|
76
|
+
};
|
|
77
|
+
// Initialize only the interpreter; never evaluate user source during prewarm.
|
|
78
|
+
const candidate = await initializeRubyExecution(loadedRuntime, context, true);
|
|
79
|
+
preparedExecution = candidate.stdin.wasReadBeforeActivation ? null : candidate;
|
|
57
80
|
runtimeState = 'ready';
|
|
58
81
|
return loadedRuntime;
|
|
59
82
|
}
|
|
60
83
|
catch (error) {
|
|
61
84
|
loadedRuntime = null;
|
|
85
|
+
preparedExecution = null;
|
|
62
86
|
runtimeState = 'uninitialized';
|
|
63
87
|
throw error;
|
|
64
88
|
}
|
|
65
89
|
}
|
|
66
|
-
function createRubyStdin(runtime, initialStdin, requestInput) {
|
|
90
|
+
function createRubyStdin(runtime, initialStdin, requestInput, deferred = false) {
|
|
67
91
|
const { Fd, Inode, wasi } = runtime.wasiShim;
|
|
68
92
|
return new (class extends Fd {
|
|
69
93
|
ino = Inode.issue_ino();
|
|
70
94
|
currentStdin = initialStdin;
|
|
71
95
|
pendingBytes = null;
|
|
72
96
|
fixedInitialStdin = initialStdin != null;
|
|
97
|
+
active = !deferred;
|
|
98
|
+
wasReadBeforeActivation = false;
|
|
99
|
+
activate(initial, provider) {
|
|
100
|
+
if (this.active)
|
|
101
|
+
throw new Error('Ruby stdin is already active.');
|
|
102
|
+
this.currentStdin = initial;
|
|
103
|
+
this.fixedInitialStdin = initial != null;
|
|
104
|
+
this.pendingBytes = null;
|
|
105
|
+
requestInput = provider;
|
|
106
|
+
this.active = true;
|
|
107
|
+
}
|
|
73
108
|
fd_fdstat_get() {
|
|
74
109
|
const fdstat = new wasi.Fdstat(wasi.FILETYPE_CHARACTER_DEVICE, 0);
|
|
75
110
|
fdstat.fs_rights_base = BigInt(wasi.RIGHTS_FD_READ);
|
|
@@ -82,6 +117,12 @@ function createRubyStdin(runtime, initialStdin, requestInput) {
|
|
|
82
117
|
};
|
|
83
118
|
}
|
|
84
119
|
fd_read(size) {
|
|
120
|
+
if (!this.active) {
|
|
121
|
+
// Do not request user input or block during speculative initialization.
|
|
122
|
+
// Such an initialization may have cached EOF in libc; never reuse it.
|
|
123
|
+
this.wasReadBeforeActivation = true;
|
|
124
|
+
return { ret: wasi.ERRNO_SUCCESS, data: new Uint8Array() };
|
|
125
|
+
}
|
|
85
126
|
let bytes = this.pendingBytes;
|
|
86
127
|
if (!bytes?.length) {
|
|
87
128
|
const chunk = this.readChunk();
|
|
@@ -142,13 +183,142 @@ function workspaceContents(runtime, workspaceFiles) {
|
|
|
142
183
|
}
|
|
143
184
|
return materializeWorkspaceTree(runtime, tree);
|
|
144
185
|
}
|
|
186
|
+
const defaultContext = () => ({
|
|
187
|
+
args: [],
|
|
188
|
+
workspaceFiles: [],
|
|
189
|
+
activePath: 'main.rb'
|
|
190
|
+
});
|
|
191
|
+
// This private bootstrap envelope contains data only, never source to evaluate or URLs.
|
|
192
|
+
function decodeStartupContext(value) {
|
|
193
|
+
if (value === undefined)
|
|
194
|
+
return defaultContext();
|
|
195
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
196
|
+
throw new TypeError('Invalid Ruby startup context.');
|
|
197
|
+
const input = value;
|
|
198
|
+
if (Object.keys(input).sort().join(',') !== 'activePath,args,workspaceFiles' ||
|
|
199
|
+
!Array.isArray(input.args) ||
|
|
200
|
+
input.args.some((arg) => typeof arg !== 'string') ||
|
|
201
|
+
!Array.isArray(input.workspaceFiles) ||
|
|
202
|
+
typeof input.activePath !== 'string') {
|
|
203
|
+
throw new TypeError('Invalid Ruby startup context.');
|
|
204
|
+
}
|
|
205
|
+
const workspaceFiles = input.workspaceFiles.map((file) => {
|
|
206
|
+
if (!file ||
|
|
207
|
+
typeof file !== 'object' ||
|
|
208
|
+
Object.keys(file).sort().join(',') !== 'content,path' ||
|
|
209
|
+
typeof file.path !== 'string' ||
|
|
210
|
+
typeof file.content !== 'string')
|
|
211
|
+
throw new TypeError('Invalid Ruby startup workspace.');
|
|
212
|
+
return { path: normalizeWorkspacePath(file.path), content: file.content };
|
|
213
|
+
});
|
|
214
|
+
return {
|
|
215
|
+
args: [...input.args],
|
|
216
|
+
workspaceFiles,
|
|
217
|
+
activePath: normalizeWorkspacePath(input.activePath)
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const sameContext = (a, b) => a.activePath === b.activePath &&
|
|
221
|
+
a.args.length === b.args.length &&
|
|
222
|
+
a.args.every((value, index) => value === b.args[index]) &&
|
|
223
|
+
a.workspaceFiles.length === b.workspaceFiles.length &&
|
|
224
|
+
a.workspaceFiles.every((file, index) => file.path === b.workspaceFiles[index].path &&
|
|
225
|
+
file.content === b.workspaceFiles[index].content);
|
|
226
|
+
async function initializeRubyExecution(loaded, context, deferred, initial = null, provider = () => null) {
|
|
227
|
+
const { runtime, module } = loaded;
|
|
228
|
+
// Snapshot the immutable startup context before awaiting VM initialization.
|
|
229
|
+
const snapshot = {
|
|
230
|
+
args: [...context.args],
|
|
231
|
+
activePath: context.activePath,
|
|
232
|
+
workspaceFiles: context.workspaceFiles.map((file) => ({
|
|
233
|
+
path: file.path,
|
|
234
|
+
content: file.content
|
|
235
|
+
}))
|
|
236
|
+
};
|
|
237
|
+
let active = !deferred;
|
|
238
|
+
let hasStdout = false, hasStderr = false;
|
|
239
|
+
let pendingBytes = 0;
|
|
240
|
+
const pending = [];
|
|
241
|
+
const emit = (output, error) => {
|
|
242
|
+
if (!output)
|
|
243
|
+
return;
|
|
244
|
+
if (error)
|
|
245
|
+
hasStderr = true;
|
|
246
|
+
else
|
|
247
|
+
hasStdout = true;
|
|
248
|
+
if (active)
|
|
249
|
+
postMessage({ output });
|
|
250
|
+
else {
|
|
251
|
+
pendingBytes += encoder.encode(output).byteLength;
|
|
252
|
+
if (pendingBytes > 64 * 1024)
|
|
253
|
+
throw new Error('Ruby initialization output exceeded 65536 bytes.');
|
|
254
|
+
pending.push(output);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
const printer = runtime.consolePrinter({
|
|
258
|
+
stdout: (output) => emit(output, false),
|
|
259
|
+
stderr: (output) => emit(output, true)
|
|
260
|
+
});
|
|
261
|
+
const rubyStdin = createRubyStdin(runtime, initial, provider, deferred);
|
|
262
|
+
const { File, OpenFile, PreopenDirectory, WASI } = runtime.wasiShim;
|
|
263
|
+
const root = workspaceContents(runtime, snapshot.workspaceFiles);
|
|
264
|
+
// Preserve the complete split standard library and ancestor directories for realpath.
|
|
265
|
+
const preopens = loaded.stdlib
|
|
266
|
+
? createRubyStdlibPreopens(loaded.stdlib, runtime.wasiShim, root)
|
|
267
|
+
: [new PreopenDirectory('/', root)];
|
|
268
|
+
// @ruby/wasm-wasi automatically requires /bundle/setup.rb while initializing.
|
|
269
|
+
// The split profile owns and verifies that mount. For the embedded profile, hide a
|
|
270
|
+
// same-named user directory until initialization completes so prewarm cannot run
|
|
271
|
+
// workspace source; the live root map is restored before the VM can be claimed.
|
|
272
|
+
const deferredUserBundle = loaded.stdlib ? undefined : root.get('bundle');
|
|
273
|
+
if (deferredUserBundle)
|
|
274
|
+
root.delete('bundle');
|
|
275
|
+
const wasiInstance = new WASI(['ruby.wasm', ...snapshot.args], ['USER=jungol'], [rubyStdin, new OpenFile(new File([])), new OpenFile(new File([])), ...preopens], { debug: false });
|
|
276
|
+
let vm;
|
|
277
|
+
try {
|
|
278
|
+
({ vm } = await runtime.RubyVM.instantiateModule({
|
|
279
|
+
module,
|
|
280
|
+
wasip1: wasiInstance,
|
|
281
|
+
args: ['ruby.wasm', '-EUTF-8', '-e_=0', '--', ...snapshot.args],
|
|
282
|
+
addToImports(imports) {
|
|
283
|
+
printer.addToImports(imports);
|
|
284
|
+
},
|
|
285
|
+
setMemory(memory) {
|
|
286
|
+
printer.setMemory(memory);
|
|
287
|
+
}
|
|
288
|
+
}));
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
if (deferredUserBundle)
|
|
292
|
+
root.set('bundle', deferredUserBundle);
|
|
293
|
+
}
|
|
294
|
+
if (!vm || typeof vm.eval !== 'function')
|
|
295
|
+
throw new Error('Ruby VM initialization did not return an evaluator.');
|
|
296
|
+
return {
|
|
297
|
+
context: snapshot,
|
|
298
|
+
vm,
|
|
299
|
+
stdin: rubyStdin,
|
|
300
|
+
activate(initial, provider) {
|
|
301
|
+
if (active)
|
|
302
|
+
throw new Error('Ruby execution is already active.');
|
|
303
|
+
rubyStdin.activate(initial, provider);
|
|
304
|
+
active = true;
|
|
305
|
+
for (const output of pending.splice(0))
|
|
306
|
+
postMessage({ output });
|
|
307
|
+
pendingBytes = 0;
|
|
308
|
+
},
|
|
309
|
+
hasOutput: () => ({ stdout: hasStdout, stderr: hasStderr })
|
|
310
|
+
};
|
|
311
|
+
}
|
|
145
312
|
self.onmessage = async (event) => {
|
|
146
313
|
const message = event.data;
|
|
147
314
|
const { load, runtimePreflight, maxAssetBytes, buffer, code, prepare, args = [], stdin, activePath = 'main.rb', workspaceFiles = [], log } = message;
|
|
315
|
+
let ownsRuntimeState = false;
|
|
148
316
|
try {
|
|
149
317
|
if (Object.prototype.hasOwnProperty.call(message, 'load')) {
|
|
150
318
|
const actualKeys = Object.keys(message).sort();
|
|
151
|
-
const expectedKeys =
|
|
319
|
+
const expectedKeys = Object.prototype.hasOwnProperty.call(message, 'startupContext')
|
|
320
|
+
? ['load', 'maxAssetBytes', 'runtimePreflight', 'startupContext']
|
|
321
|
+
: ['load', 'maxAssetBytes', 'runtimePreflight'];
|
|
152
322
|
if (load !== true ||
|
|
153
323
|
actualKeys.length !== expectedKeys.length ||
|
|
154
324
|
actualKeys.some((key, index) => key !== expectedKeys[index]) ||
|
|
@@ -156,8 +326,9 @@ self.onmessage = async (event) => {
|
|
|
156
326
|
maxAssetBytes <= 0) {
|
|
157
327
|
throw new Error('Ruby runtime load message has an invalid shape.');
|
|
158
328
|
}
|
|
329
|
+
const context = decodeStartupContext(message.startupContext);
|
|
159
330
|
postMessage({ progress: { percent: 5, stage: 'Loading Ruby runtime' } });
|
|
160
|
-
await loadRubyModule(runtimePreflight, maxAssetBytes);
|
|
331
|
+
await loadRubyModule(runtimePreflight, maxAssetBytes, context);
|
|
161
332
|
postMessage({ progress: { percent: 100, stage: 'Ruby runtime ready' } });
|
|
162
333
|
postMessage({ load: true });
|
|
163
334
|
return;
|
|
@@ -167,57 +338,37 @@ self.onmessage = async (event) => {
|
|
|
167
338
|
throw new Error('Ruby runtime is not ready.');
|
|
168
339
|
}
|
|
169
340
|
runtimeState = 'running';
|
|
170
|
-
|
|
341
|
+
ownsRuntimeState = true;
|
|
342
|
+
const context = { args, workspaceFiles, activePath };
|
|
343
|
+
if (preparedExecution && !sameContext(preparedExecution.context, context))
|
|
344
|
+
preparedExecution = null;
|
|
171
345
|
if (prepare) {
|
|
346
|
+
if (!preparedExecution) {
|
|
347
|
+
const candidate = await initializeRubyExecution(loadedRuntime, context, true);
|
|
348
|
+
preparedExecution = candidate.stdin.wasReadBeforeActivation ? null : candidate;
|
|
349
|
+
}
|
|
172
350
|
postMessage({ results: true });
|
|
173
|
-
runtimeState = 'ready';
|
|
174
351
|
return;
|
|
175
352
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
postMessage({ output });
|
|
183
|
-
},
|
|
184
|
-
stderr(output) {
|
|
185
|
-
stderr += output;
|
|
186
|
-
if (output)
|
|
187
|
-
postMessage({ output });
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
const hasInitialStdin = typeof stdin === 'string';
|
|
191
|
-
const rubyStdin = createRubyStdin(runtime, hasInitialStdin ? stdin : null, () => {
|
|
192
|
-
const chunk = waitForBufferedStdin(stdinBufferRuby, () => postMessage({ buffer: true }));
|
|
193
|
-
if (log) {
|
|
353
|
+
const initialStdin = typeof stdin === 'string' ? stdin : null;
|
|
354
|
+
// Capture this run's buffer; no retained VM may read a previous caller's input.
|
|
355
|
+
const inputBuffer = stdinBufferRuby;
|
|
356
|
+
const requestInput = () => {
|
|
357
|
+
const chunk = waitForBufferedStdin(inputBuffer, () => postMessage({ buffer: true }));
|
|
358
|
+
if (log)
|
|
194
359
|
console.log(chunk == null
|
|
195
360
|
? '[wasm-idle:ruby-stdin] read(bytes=0, eof=true)'
|
|
196
361
|
: `[wasm-idle:ruby-stdin] read(bytes=${encoder.encode(chunk).byteLength}, text=${JSON.stringify(chunk)})`);
|
|
197
|
-
}
|
|
198
362
|
return chunk;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
debug: false
|
|
209
|
-
});
|
|
210
|
-
const { vm } = await runtime.RubyVM.instantiateModule({
|
|
211
|
-
module: rubyModule,
|
|
212
|
-
wasip1: wasiInstance,
|
|
213
|
-
args: ['ruby.wasm', '-EUTF-8', '-e_=0', ...args],
|
|
214
|
-
addToImports(imports) {
|
|
215
|
-
printer.addToImports(imports);
|
|
216
|
-
},
|
|
217
|
-
setMemory(memory) {
|
|
218
|
-
printer.setMemory(memory);
|
|
219
|
-
}
|
|
220
|
-
});
|
|
363
|
+
};
|
|
364
|
+
// Claim before evaluation. A VM which has run user code is never cached.
|
|
365
|
+
let execution = preparedExecution;
|
|
366
|
+
preparedExecution = null;
|
|
367
|
+
if (execution)
|
|
368
|
+
execution.activate(initialStdin, requestInput);
|
|
369
|
+
else
|
|
370
|
+
execution = await initializeRubyExecution(loadedRuntime, context, false, initialStdin, requestInput);
|
|
371
|
+
const { vm } = execution;
|
|
221
372
|
if (log) {
|
|
222
373
|
console.log(`[wasm-idle:ruby-worker] eval start bytes=${code.length} activePath=${activePath}`);
|
|
223
374
|
}
|
|
@@ -231,7 +382,7 @@ self.onmessage = async (event) => {
|
|
|
231
382
|
});
|
|
232
383
|
vm.eval(code);
|
|
233
384
|
if (log) {
|
|
234
|
-
console.log(`[wasm-idle:ruby-worker] eval settled stdout=${String(
|
|
385
|
+
console.log(`[wasm-idle:ruby-worker] eval settled stdout=${String(execution.hasOutput().stdout)} stderr=${String(execution.hasOutput().stderr)}`);
|
|
235
386
|
}
|
|
236
387
|
postMessage({ results: true });
|
|
237
388
|
}
|
|
@@ -242,7 +393,7 @@ self.onmessage = async (event) => {
|
|
|
242
393
|
postMessage({ error: error?.message || String(error) });
|
|
243
394
|
}
|
|
244
395
|
finally {
|
|
245
|
-
if (runtimeState === 'running')
|
|
396
|
+
if (ownsRuntimeState && runtimeState === 'running')
|
|
246
397
|
runtimeState = 'ready';
|
|
247
398
|
}
|
|
248
399
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wasm-idle",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "A simple web app to run code in the browser",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"apps/*"
|
|
15
15
|
],
|
|
16
16
|
"devDependencies": {
|
|
17
|
+
"acorn": "8.16.0",
|
|
17
18
|
"@eslint/js": "^10.0.1",
|
|
18
19
|
"@sveltejs/adapter-auto": "^7.0.1",
|
|
19
20
|
"@sveltejs/adapter-static": "^3.0.10",
|
|
@@ -35,6 +36,7 @@
|
|
|
35
36
|
"@typescript-eslint/parser": "^8.58.2",
|
|
36
37
|
"@wasmer/sdk": "0.9.0",
|
|
37
38
|
"assemblyscript": "0.28.17",
|
|
39
|
+
"esbuild": "0.28.0",
|
|
38
40
|
"eslint": "^10.2.0",
|
|
39
41
|
"eslint-config-prettier": "^10.1.8",
|
|
40
42
|
"eslint-plugin-svelte": "^3.17.0",
|
|
@@ -59,15 +61,15 @@
|
|
|
59
61
|
"vitest": "^4.1.4",
|
|
60
62
|
"waforth": "0.20.1",
|
|
61
63
|
"webr": "0.6.0",
|
|
62
|
-
"@wasm-idle/
|
|
63
|
-
"@wasm-idle/
|
|
64
|
-
"@wasm-idle/
|
|
64
|
+
"@wasm-idle/terminal": "1.0.4",
|
|
65
|
+
"@wasm-idle/debug": "1.0.4",
|
|
66
|
+
"@wasm-idle/lsp": "1.0.4"
|
|
65
67
|
},
|
|
66
68
|
"dependencies": {
|
|
67
69
|
"@bjorn3/browser_wasi_shim": "^0.4.2",
|
|
68
70
|
"fflate": "0.8.3",
|
|
69
|
-
"@wasm-idle/
|
|
70
|
-
"@wasm-idle/core": "1.0.
|
|
71
|
+
"@wasm-idle/core": "1.0.4",
|
|
72
|
+
"@wasm-idle/llvm-core": "1.0.4"
|
|
71
73
|
},
|
|
72
74
|
"files": [
|
|
73
75
|
"dist",
|
|
@@ -199,7 +201,7 @@
|
|
|
199
201
|
"support:matrix": "node scripts/support-matrix.mjs write",
|
|
200
202
|
"check:support-matrix": "node scripts/support-matrix.mjs check",
|
|
201
203
|
"sync:page-runtimes": "pnpm run sync:pyodide && pnpm run sync:webr && pnpm run sync:wasm-of-js-of-ocaml && pnpm run sync:wasm-typescript && pnpm run sync:wasm-octave && pnpm run sync:wasm-prolog && pnpm run sync:wasm-gleam && pnpm run sync:wasm-perl && pnpm run sync:wasm-tcl && pnpm run sync:wasm-awk && pnpm run sync:wasm-pascal && pnpm run sync:wasm-forth && pnpm run sync:wasm-j && pnpm run sync:wasm-bqn && pnpm run sync:wasm-janet && pnpm run sync:wasm-julia && pnpm run sync:wasm-nim && pnpm run sync:wasm-ruby && pnpm run sync:wasm-bash && pnpm run sync:wasm-clojurescript",
|
|
202
|
-
"page:build": "pnpm run verify:page-c3 && pnpm run verify:page-lfortran && pnpm run build:page-runtimes && pnpm run sync:page-runtimes && pnpm run build:static-runtime-modules && pnpm run prepare:wasm-debug-release && pnpm run layer:static-runtimes && pnpm run compress:static-runtimes && pnpm run build && pnpm run compress:build-runtimes && pnpm run verify:page-c3 build && pnpm run verify:page-lfortran build",
|
|
204
|
+
"page:build": "pnpm run verify:page-c3 && pnpm run verify:page-lfortran && pnpm run prepare:test-assets -- clang && pnpm run build:page-runtimes && pnpm run sync:page-runtimes && pnpm run build:static-runtime-modules && pnpm run prepare:wasm-debug-release && pnpm run layer:static-runtimes && pnpm run compress:static-runtimes && pnpm run build && pnpm run compress:build-runtimes && pnpm run verify:page-c3 build && pnpm run verify:page-lfortran build",
|
|
203
205
|
"package:root": "svelte-kit sync && node scripts/clean-package-output.mjs && svelte-package --tsconfig ./tsconfig.package.json && publint",
|
|
204
206
|
"package": "pnpm run build:publish-deps && pnpm run package:root",
|
|
205
207
|
"verify:package": "node scripts/verify-package.mjs",
|