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
@@ -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(asset) {
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
- await importRuntimeAssetModule('pyodide.asm.js');
192
- const runtimeModule = (await importRuntimeAssetModule('pyodide.mjs'));
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
- const loadedLock = await loadWorkerRuntimeAsset('pyodide-lock.json');
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
- await loadPackages([
284
- code,
285
- ...(workspaceFiles || []).map((file) => file.content)
286
- ].join('\n'));
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
- async function loadRubyModule(runtimePreflight, maxAssetBytes) {
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 payload = await verifyRubyRuntimePreflightPayload(runtimePreflight, {
15
- maxAssetBytes
16
- });
17
- const buffers = [payload.manifestBytes, payload.moduleJavaScriptBytes, payload.wasmBytes];
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 = rewriteVerifiedRubyRuntimeModule(payload);
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 = { module, runtime };
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 = ['load', 'maxAssetBytes', 'runtimePreflight'];
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
- const { module: rubyModule, runtime } = loadedRuntime;
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
- let stdout = '';
177
- let stderr = '';
178
- const printer = runtime.consolePrinter({
179
- stdout(output) {
180
- stdout += output;
181
- if (output)
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
- const { File, OpenFile, PreopenDirectory, WASI } = runtime.wasiShim;
201
- const fds = [
202
- rubyStdin,
203
- new OpenFile(new File([])),
204
- new OpenFile(new File([])),
205
- new PreopenDirectory('/', workspaceContents(runtime, workspaceFiles))
206
- ];
207
- const wasiInstance = new WASI(['ruby.wasm', ...args], ['USER=jungol'], fds, {
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(Boolean(stdout))} stderr=${String(Boolean(stderr))}`);
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.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/debug": "1.0.0",
63
- "@wasm-idle/lsp": "1.0.0",
64
- "@wasm-idle/terminal": "1.0.0"
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/llvm-core": "1.0.0",
70
- "@wasm-idle/core": "1.0.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",