wasm-idle 1.0.11 → 1.0.12

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.
@@ -5,11 +5,14 @@ import { bufferedSequence, flushBufferedEof, flushQueuedStdin, resetBufferedStdi
5
5
  import { createWasmIdleSharedBuffer, requireSharedArrayBuffer } from './sharedBuffer';
6
6
  import { WorkerSession } from './workerSession';
7
7
  import { reportWorkerProgress } from './workerProgress';
8
- import { BusyError, TimeoutError, resolveExecutionLimits } from '@wasm-idle/core';
8
+ import { BusyError, DEFAULT_WORKSPACE_LIMITS, OutputLimitError, RuntimeConfigurationError, TimeoutError, resolveExecutionLimits, validateExecutionWorkspace } from '@wasm-idle/core';
9
9
  const debugBreakpointBufferInts = 1028;
10
- const abortReason = (signal, phase) => signal.reason !== undefined
10
+ const outputEncoder = new TextEncoder();
11
+ const abortReason = (signal, phase, label = 'Python') => signal.reason !== undefined
11
12
  ? signal.reason
12
- : new DOMException(phase === 'startup' ? 'Python runtime startup aborted' : 'Python execution aborted', 'AbortError');
13
+ : new DOMException(phase === 'startup'
14
+ ? `${label} runtime startup aborted`
15
+ : `${label} execution aborted`, 'AbortError');
13
16
  class Python {
14
17
  ts = Date.now();
15
18
  output = null;
@@ -37,6 +40,25 @@ class Python {
37
40
  workerExtensionKey;
38
41
  /** Language executed by the shared Pyodide worker; subclasses run other Python-hosted languages. */
39
42
  workerLanguage = 'python';
43
+ get usesInterpreterStdio() {
44
+ return this.workerLanguage === 'aheui' || this.workerLanguage === 'apecode';
45
+ }
46
+ get runtimeId() {
47
+ if (this.workerLanguage === 'apecode')
48
+ return 'APECODE';
49
+ return this.workerLanguage === 'aheui' ? 'AHEUI' : 'PYTHON3';
50
+ }
51
+ get runtimeLabel() {
52
+ if (this.workerLanguage === 'apecode')
53
+ return 'APECode';
54
+ return this.workerLanguage === 'aheui' ? 'Aheui' : 'Python';
55
+ }
56
+ resetInterpreterInput() {
57
+ if (!this.usesInterpreterStdio)
58
+ return;
59
+ this.pendingInput = [];
60
+ resetBufferedStdin(this.buffer);
61
+ }
40
62
  /** Extra worker load configuration for Python-hosted languages, keyed for worker reuse. */
41
63
  workerLoadExtension(_runtimeAssets, _currentUrl) {
42
64
  return undefined;
@@ -57,10 +79,10 @@ class Python {
57
79
  });
58
80
  beginOperation(phase) {
59
81
  if (this.disposed)
60
- throw new Error('Python runtime has been disposed');
82
+ throw new Error(`${this.runtimeLabel} runtime has been disposed`);
61
83
  if (this.activeOperation) {
62
- throw new BusyError('Python runtime already has an active operation', {
63
- runtimeId: 'PYTHON3',
84
+ throw new BusyError(`${this.runtimeLabel} runtime already has an active operation`, {
85
+ runtimeId: this.runtimeId,
64
86
  phase: this.activeOperation.phase
65
87
  });
66
88
  }
@@ -110,6 +132,7 @@ class Python {
110
132
  this.activeOperation = null;
111
133
  this.waitingForInput = false;
112
134
  this.pendingEof = false;
135
+ this.resetInterpreterInput();
113
136
  this.uid += 1;
114
137
  this.exit = true;
115
138
  this.workerSession.terminate(reason);
@@ -124,7 +147,7 @@ class Python {
124
147
  return;
125
148
  let reason;
126
149
  try {
127
- reason = abortReason(signal, operation.phase);
150
+ reason = abortReason(signal, operation.phase, this.runtimeLabel);
128
151
  }
129
152
  catch (error) {
130
153
  reason = error;
@@ -145,9 +168,10 @@ class Python {
145
168
  this.cancelOperation(operation, error);
146
169
  }
147
170
  }
148
- bindStartupTimeout(operation, timeoutMs) {
171
+ bindOperationTimeout(operation, timeoutMs) {
149
172
  if (!this.isOperationActive(operation))
150
173
  return;
174
+ timeoutMs = Math.min(2_147_483_647, timeoutMs);
151
175
  let timeout;
152
176
  operation.cleanups.push(() => {
153
177
  if (timeout !== undefined)
@@ -157,9 +181,9 @@ class Python {
157
181
  timeout = setTimeout(() => {
158
182
  if (!this.isOperationActive(operation))
159
183
  return;
160
- this.cancelOperation(operation, new TimeoutError(`Python startup timed out after ${timeoutMs} ms`, {
161
- phase: 'startup',
162
- runtimeId: 'PYTHON3',
184
+ this.cancelOperation(operation, new TimeoutError(`${this.runtimeLabel} ${operation.phase === 'startup' ? 'startup' : 'execution'} timed out after ${timeoutMs} ms`, {
185
+ phase: operation.phase,
186
+ runtimeId: this.runtimeId,
163
187
  timeoutMs
164
188
  }));
165
189
  }, timeoutMs);
@@ -187,7 +211,7 @@ class Python {
187
211
  return Promise.reject(this.releaseBeforeSession(operation, 'Python runtime startup cancelled'));
188
212
  }
189
213
  if (signal?.aborted) {
190
- return Promise.reject(this.releaseBeforeSession(operation, abortReason(signal, 'startup')));
214
+ return Promise.reject(this.releaseBeforeSession(operation, abortReason(signal, 'startup', this.runtimeLabel)));
191
215
  }
192
216
  }
193
217
  catch (error) {
@@ -300,7 +324,7 @@ class Python {
300
324
  }
301
325
  });
302
326
  const timeoutMs = Math.min(2_147_483_647, limits.assetTimeoutMs + limits.startupTimeoutMs);
303
- this.bindStartupTimeout(operation, timeoutMs);
327
+ this.bindOperationTimeout(operation, timeoutMs);
304
328
  this.bindAbortSignal(operation, signal);
305
329
  return loading.finally(() => {
306
330
  this.releaseOperation(operation);
@@ -346,6 +370,7 @@ class Python {
346
370
  let activePath;
347
371
  let debugPath;
348
372
  let workspaceFiles;
373
+ let limits;
349
374
  try {
350
375
  signal = options.signal;
351
376
  debug = !!options.debug;
@@ -355,6 +380,24 @@ class Python {
355
380
  activePath = options.activePath;
356
381
  debugPath = options.debugPath;
357
382
  workspaceFiles = options.workspaceFiles;
383
+ if (this.usesInterpreterStdio) {
384
+ limits = resolveExecutionLimits(options.limits);
385
+ if (_args.length || options.programArgs?.length) {
386
+ throw new RuntimeConfigurationError(`${this.runtimeLabel} does not accept program arguments`, { runtimeId: this.runtimeId });
387
+ }
388
+ if (debug || (options.debugMode && options.debugMode !== 'none')) {
389
+ throw new RuntimeConfigurationError(`${this.runtimeLabel} debugging is not supported`, { runtimeId: this.runtimeId });
390
+ }
391
+ const workspace = validateExecutionWorkspace(code, workspaceFiles ?? [], activePath ?? (this.workerLanguage === 'apecode' ? 'main.ape' : 'main.aheui'), {
392
+ ...options.workspaceLimits,
393
+ maxFileBytes: Math.min(options.workspaceLimits?.maxFileBytes ??
394
+ DEFAULT_WORKSPACE_LIMITS.maxFileBytes, limits.maxWorkspaceBytes),
395
+ maxTotalBytes: Math.min(options.workspaceLimits?.maxTotalBytes ??
396
+ DEFAULT_WORKSPACE_LIMITS.maxTotalBytes, limits.maxWorkspaceBytes)
397
+ });
398
+ activePath = workspace.activePath;
399
+ workspaceFiles = workspace.workspaceFiles;
400
+ }
358
401
  const persistentCache = options.persistentCache;
359
402
  if (!this.isOperationActive(activeOperation)) {
360
403
  return Promise.reject(this.releaseBeforeSession(activeOperation, 'Python execution cancelled'));
@@ -370,14 +413,22 @@ class Python {
370
413
  if (!worker || this.worker !== worker) {
371
414
  return Promise.reject(this.releaseBeforeSession(activeOperation, 'Worker not loaded'));
372
415
  }
416
+ this.resetInterpreterInput();
417
+ if (this.usesInterpreterStdio) {
418
+ this.waitingForInput = false;
419
+ this.pendingEof = false;
420
+ }
373
421
  this.exit = false;
374
422
  const running = new Promise((resolve, reject) => {
375
423
  const workerOperation = this.workerSession.beginRun(worker, reject);
424
+ if (limits)
425
+ this.bindOperationTimeout(activeOperation, prepare ? limits.compileTimeoutMs : limits.runTimeoutMs);
376
426
  this.bindAbortSignal(activeOperation, signal);
377
427
  if (!this.isOperationActive(activeOperation))
378
428
  return;
379
429
  const interrupt = new Uint8Array(this.interruptBuffer), runUid = ++this.uid;
380
430
  let handler;
431
+ let outputBytes = 0;
381
432
  const ownsRun = () => this.isOperationActive(activeOperation) &&
382
433
  this.worker === worker &&
383
434
  worker.onmessage === handler &&
@@ -387,6 +438,7 @@ class Python {
387
438
  this.exit = true;
388
439
  this.waitingForInput = false;
389
440
  this.pendingEof = false;
441
+ this.resetInterpreterInput();
390
442
  };
391
443
  const claimRun = () => {
392
444
  if (!ownsRun())
@@ -431,7 +483,7 @@ class Python {
431
483
  kind: 'ready',
432
484
  state: 'waiting-input',
433
485
  reason: 'stdin-request',
434
- label: 'Python program is waiting for input'
486
+ label: `${this.runtimeLabel} program is waiting for input`
435
487
  });
436
488
  }
437
489
  this.waitingForInput = true;
@@ -443,8 +495,21 @@ class Python {
443
495
  this.image?.(payload);
444
496
  if (!ownsRun())
445
497
  return;
446
- if (output)
498
+ if (output) {
499
+ if (limits) {
500
+ outputBytes += outputEncoder.encode(output).byteLength;
501
+ if (outputBytes > limits.maxOutputBytes) {
502
+ failRun(new OutputLimitError(`${this.runtimeLabel} output exceeded the configured byte limit`, {
503
+ runtimeId: this.runtimeId,
504
+ phase: 'execute',
505
+ actual: outputBytes,
506
+ limit: limits.maxOutputBytes
507
+ }), true);
508
+ return;
509
+ }
510
+ }
447
511
  this.output?.(output);
512
+ }
448
513
  if (!ownsRun())
449
514
  return;
450
515
  if (debugEvent)
@@ -502,6 +567,7 @@ class Python {
502
567
  this.exit = true;
503
568
  this.waitingForInput = false;
504
569
  this.pendingEof = false;
570
+ this.resetInterpreterInput();
505
571
  }
506
572
  this.cleanupOperation(activeOperation);
507
573
  });
@@ -2,6 +2,7 @@ import { type PlaygroundRuntimeAssets } from './assets';
2
2
  import { type CompilerDiagnostic, type SandboxExecutionOptions } from './options';
3
3
  import type { Sandbox, SandboxProgress } from './sandbox';
4
4
  declare class Ruby implements Sandbox {
5
+ private readonly workerLanguage;
5
6
  output: any;
6
7
  worker?: Worker;
7
8
  buffer: import("./sharedBuffer").WasmIdleSharedBuffer;
@@ -21,6 +22,11 @@ declare class Ruby implements Sandbox {
21
22
  private disposePromise;
22
23
  private readonly disposeCancellation;
23
24
  private readonly workerSession;
25
+ constructor(workerLanguage?: 'ruby' | 'golfscript');
26
+ private get runtimeId();
27
+ private get runtimeLabel();
28
+ private get defaultSourcePath();
29
+ private validateRuntimeNamespace;
24
30
  private beginOperation;
25
31
  private isOperationActive;
26
32
  private releaseOperation;
@@ -1,16 +1,20 @@
1
- import { resolveRubyRuntimeAssetConfig } from './assets';
1
+ import { resolveRubyRuntimeAssetConfig, resolveGolfscriptRuntimeAssetConfig } from './assets';
2
+ import { GOLFSCRIPT_RUNTIME_DIRECTORY, WASM_GOLFSCRIPT_INTERPRETER_RECEIPT } from './wasmGolfscriptVersion';
2
3
  import { createRubyRuntimeOwnedPreflightDelivery, preflightVerifiedRubyRuntimeAssets } from './rubyAssets';
3
4
  import { resolveSandboxExecutionArgs } from './options';
4
5
  import { flushBufferedEof, flushQueuedStdin, resetBufferedStdin } from './stdinBuffer';
5
6
  import { createWasmIdleSharedBuffer } from './sharedBuffer';
6
7
  import { WorkerSession } from './workerSession';
7
8
  import { reportWorkerInputReady, reportWorkerProgress } from './workerProgress';
8
- import { BusyError, AssetTooLargeError, CancelledError, DEFAULT_WORKSPACE_LIMITS, DiagnosticLimitError, OutputLimitError, RUBY_MAX_ASSET_BYTES, RUBY_MAX_MANIFEST_BYTES, RUBY_MAX_MODULE_BYTES, RuntimeConfigurationError, TimeoutError, resolveExecutionLimits, validateExecutionWorkspace } from '@wasm-idle/core';
9
+ import { BusyError, AssetTooLargeError, CancelledError, DEFAULT_WORKSPACE_LIMITS, DiagnosticLimitError, OutputLimitError, RUBY_MAX_ASSET_BYTES, RUBY_MAX_MANIFEST_BYTES, RUBY_MAX_MODULE_BYTES, RuntimeConfigurationError, TimeoutError, fetchPinnedRuntimeAsset, resolveExecutionLimits, validateExecutionWorkspace } from '@wasm-idle/core';
9
10
  const OUTPUT_ENCODER = new TextEncoder();
10
- const abortReason = (signal, phase) => signal.reason !== undefined
11
+ const abortReason = (signal, phase, label) => signal.reason !== undefined
11
12
  ? signal.reason
12
- : new DOMException(phase === 'startup' ? 'Ruby runtime startup aborted' : 'Ruby execution aborted', 'AbortError');
13
+ : new DOMException(phase === 'startup'
14
+ ? `${label} runtime startup aborted`
15
+ : `${label} execution aborted`, 'AbortError');
13
16
  class Ruby {
17
+ workerLanguage;
14
18
  output = null;
15
19
  worker;
16
20
  buffer = createWasmIdleSharedBuffer(1024);
@@ -28,32 +32,57 @@ class Ruby {
28
32
  activeOperation = null;
29
33
  disposed = false;
30
34
  disposePromise = null;
31
- disposeCancellation = new CancelledError('Ruby sandbox disposed', {
32
- phase: 'dispose',
33
- runtimeId: 'RUBY',
34
- recoverable: false
35
- });
36
- workerSession = new WorkerSession({
37
- label: 'Ruby',
38
- onDispose: (worker) => {
39
- if (this.worker !== worker)
40
- return;
41
- delete this.worker;
42
- this.exit = true;
43
- this.waitingForInput = false;
44
- this.pendingEof = false;
35
+ disposeCancellation;
36
+ workerSession;
37
+ constructor(workerLanguage = 'ruby') {
38
+ this.workerLanguage = workerLanguage;
39
+ this.disposeCancellation = new CancelledError(`${this.runtimeLabel} sandbox disposed`, {
40
+ phase: 'dispose',
41
+ runtimeId: this.runtimeId,
42
+ recoverable: false
43
+ });
44
+ this.workerSession = new WorkerSession({
45
+ label: this.runtimeLabel,
46
+ onDispose: (worker) => {
47
+ if (this.worker !== worker)
48
+ return;
49
+ delete this.worker;
50
+ this.exit = true;
51
+ this.waitingForInput = false;
52
+ this.pendingEof = false;
53
+ }
54
+ });
55
+ }
56
+ get runtimeId() {
57
+ return this.workerLanguage === 'golfscript' ? 'GOLFSCRIPT' : 'RUBY';
58
+ }
59
+ get runtimeLabel() {
60
+ return this.workerLanguage === 'golfscript' ? 'GolfScript' : 'Ruby';
61
+ }
62
+ get defaultSourcePath() {
63
+ return this.workerLanguage === 'golfscript' ? 'main.gs' : 'main.rb';
64
+ }
65
+ validateRuntimeNamespace(activePath, files) {
66
+ if (this.workerLanguage !== 'golfscript')
67
+ return;
68
+ if ([activePath ?? this.defaultSourcePath, ...files.map((file) => file.path)].some((path) => path === GOLFSCRIPT_RUNTIME_DIRECTORY ||
69
+ path.startsWith(`${GOLFSCRIPT_RUNTIME_DIRECTORY}/`))) {
70
+ throw new RuntimeConfigurationError('GolfScript workspace conflicts with its private runtime mount', {
71
+ phase: 'configuration',
72
+ runtimeId: this.runtimeId
73
+ });
45
74
  }
46
- });
75
+ }
47
76
  beginOperation(phase) {
48
77
  if (this.disposed) {
49
- throw new RuntimeConfigurationError('Ruby sandbox is disposed', {
78
+ throw new RuntimeConfigurationError(`${this.runtimeLabel} sandbox is disposed`, {
50
79
  phase: 'dispose',
51
- runtimeId: 'RUBY'
80
+ runtimeId: this.runtimeId
52
81
  });
53
82
  }
54
83
  if (this.activeOperation) {
55
- throw new BusyError('Ruby runtime already has an active operation', {
56
- runtimeId: 'RUBY',
84
+ throw new BusyError(`${this.runtimeLabel} runtime already has an active operation`, {
85
+ runtimeId: this.runtimeId,
57
86
  phase: this.activeOperation.phase
58
87
  });
59
88
  }
@@ -115,7 +144,7 @@ class Ruby {
115
144
  return;
116
145
  let reason;
117
146
  try {
118
- reason = abortReason(signal, operation.phase);
147
+ reason = abortReason(signal, operation.phase, this.runtimeLabel);
119
148
  }
120
149
  catch (error) {
121
150
  reason = error;
@@ -151,7 +180,7 @@ class Ruby {
151
180
  return;
152
181
  let reason;
153
182
  try {
154
- reason = abortReason(signal, operation.phase);
183
+ reason = abortReason(signal, operation.phase, this.runtimeLabel);
155
184
  }
156
185
  catch (error) {
157
186
  reason = error;
@@ -185,9 +214,9 @@ class Ruby {
185
214
  if (!this.isOperationActive(operation))
186
215
  return;
187
216
  const label = operation.phase === 'startup' ? 'startup' : 'execution';
188
- this.cancelOperation(operation, new TimeoutError(`Ruby ${label} timed out after ${timeoutMs} ms`, {
217
+ this.cancelOperation(operation, new TimeoutError(`${this.runtimeLabel} ${label} timed out after ${timeoutMs} ms`, {
189
218
  phase: operation.phase,
190
- runtimeId: 'RUBY',
219
+ runtimeId: this.runtimeId,
191
220
  timeoutMs
192
221
  }));
193
222
  }, timeoutMs);
@@ -252,6 +281,7 @@ class Ruby {
252
281
  }
253
282
  let limits;
254
283
  let nextConfig;
284
+ let golfscriptConfig;
255
285
  let startupContext;
256
286
  let nextAssetKey;
257
287
  let signal;
@@ -260,32 +290,37 @@ class Ruby {
260
290
  signal = options.signal;
261
291
  unbindPreSessionAbort = this.bindPreSessionAbort(activeOperation, signal);
262
292
  if (!this.isOperationActive(activeOperation)) {
263
- return Promise.reject(this.releaseBeforeSession(activeOperation, 'Ruby runtime startup cancelled'));
293
+ return Promise.reject(this.releaseBeforeSession(activeOperation, `${this.runtimeLabel} runtime startup cancelled`));
264
294
  }
265
295
  limits = resolveExecutionLimits(options.limits);
266
296
  if (!this.isOperationActive(activeOperation) || signal?.aborted) {
267
- return Promise.reject(this.releaseBeforeSession(activeOperation, 'Ruby runtime startup cancelled'));
297
+ return Promise.reject(this.releaseBeforeSession(activeOperation, `${this.runtimeLabel} runtime startup cancelled`));
268
298
  }
269
- const workspace = validateExecutionWorkspace(_code, options.workspaceFiles ?? [], options.activePath ?? 'main.rb', {
299
+ const workspace = validateExecutionWorkspace(_code, options.workspaceFiles ?? [], options.activePath ?? this.defaultSourcePath, {
270
300
  ...options.workspaceLimits,
271
301
  maxFileBytes: Math.min(options.workspaceLimits?.maxFileBytes ??
272
302
  DEFAULT_WORKSPACE_LIMITS.maxFileBytes, limits.maxWorkspaceBytes),
273
303
  maxTotalBytes: Math.min(options.workspaceLimits?.maxTotalBytes ??
274
304
  DEFAULT_WORKSPACE_LIMITS.maxTotalBytes, limits.maxWorkspaceBytes)
275
305
  });
306
+ this.validateRuntimeNamespace(workspace.activePath, workspace.workspaceFiles);
276
307
  startupContext = {
277
- args: [...resolveSandboxExecutionArgs('RUBY', _args, options).programArgs],
278
- activePath: workspace.activePath ?? 'main.rb',
308
+ args: [...resolveSandboxExecutionArgs(this.runtimeId, _args, options).programArgs],
309
+ activePath: workspace.activePath ?? this.defaultSourcePath,
279
310
  workspaceFiles: workspace.workspaceFiles.map((file) => ({
280
311
  path: file.path,
281
312
  content: file.content
282
313
  }))
283
314
  };
284
315
  if (!this.isOperationActive(activeOperation) || signal?.aborted) {
285
- return Promise.reject(this.releaseBeforeSession(activeOperation, 'Ruby runtime startup cancelled'));
316
+ return Promise.reject(this.releaseBeforeSession(activeOperation, `${this.runtimeLabel} runtime startup cancelled`));
286
317
  }
287
318
  const currentUrl = typeof window !== 'undefined' ? window.location.href : '';
288
319
  nextConfig = resolveRubyRuntimeAssetConfig(runtimeAssets, currentUrl);
320
+ golfscriptConfig =
321
+ this.workerLanguage === 'golfscript'
322
+ ? resolveGolfscriptRuntimeAssetConfig(runtimeAssets, currentUrl)
323
+ : undefined;
289
324
  const effectiveMaxAssetBytes = Math.min(limits.maxAssetBytes, RUBY_MAX_ASSET_BYTES);
290
325
  if (!nextConfig.splitStdlib) {
291
326
  for (const [label, bytes, limit] of [
@@ -311,26 +346,30 @@ class Ruby {
311
346
  ]
312
347
  ]) {
313
348
  if ((bytes ?? 0) > limit) {
314
- throw new AssetTooLargeError(`Ruby runtime ${label} exceeds the ${limit} byte limit`, {
349
+ throw new AssetTooLargeError(`${this.runtimeLabel} runtime ${label} exceeds the ${limit} byte limit`, {
315
350
  actual: bytes,
316
351
  limit,
317
352
  phase: 'asset',
318
353
  profileId: nextConfig.preflightProfile.profileId,
319
- runtimeId: 'RUBY'
354
+ runtimeId: this.runtimeId
320
355
  });
321
356
  }
322
357
  }
323
358
  }
324
- nextAssetKey = JSON.stringify([nextConfig.preflightKey, effectiveMaxAssetBytes]);
359
+ nextAssetKey = JSON.stringify([
360
+ nextConfig.preflightKey,
361
+ effectiveMaxAssetBytes,
362
+ ...(golfscriptConfig ? [golfscriptConfig.assetKey] : [])
363
+ ]);
325
364
  if (!this.isOperationActive(activeOperation) || signal?.aborted) {
326
- return Promise.reject(this.releaseBeforeSession(activeOperation, 'Ruby runtime startup cancelled'));
365
+ return Promise.reject(this.releaseBeforeSession(activeOperation, `${this.runtimeLabel} runtime startup cancelled`));
327
366
  }
328
367
  }
329
368
  catch (error) {
330
369
  return Promise.reject(this.releaseBeforeSession(activeOperation, error));
331
370
  }
332
371
  if (!this.isOperationActive(activeOperation)) {
333
- return Promise.reject(this.releaseBeforeSession(activeOperation, 'Ruby runtime startup cancelled'));
372
+ return Promise.reject(this.releaseBeforeSession(activeOperation, `${this.runtimeLabel} runtime startup cancelled`));
334
373
  }
335
374
  unbindPreSessionAbort();
336
375
  const loading = this.workerSession.load(async (resolve, reject) => {
@@ -364,7 +403,7 @@ class Ruby {
364
403
  return;
365
404
  preflightController.abort(activeOperation.cancellationReason ??
366
405
  signal?.reason ??
367
- new DOMException('Ruby runtime preflight retired', 'AbortError'));
406
+ new DOMException(`${this.runtimeLabel} runtime preflight retired`, 'AbortError'));
368
407
  });
369
408
  const payload = await preflightVerifiedRubyRuntimeAssets(nextConfig, {
370
409
  limits,
@@ -373,6 +412,15 @@ class Ruby {
373
412
  progress?.set?.(totalBytes > 0 ? Math.min(0.9, (loadedBytes / totalBytes) * 0.9) : 0);
374
413
  }
375
414
  });
415
+ const interpreterBytes = golfscriptConfig
416
+ ? Uint8Array.from(await fetchPinnedRuntimeAsset({
417
+ url: golfscriptConfig.interpreterUrl,
418
+ receipt: WASM_GOLFSCRIPT_INTERPRETER_RECEIPT,
419
+ maxAssetBytes: limits.maxAssetBytes,
420
+ persistentCache: options.persistentCache,
421
+ signal: preflightController.signal
422
+ }))
423
+ : undefined;
376
424
  if (!this.isOperationActive(activeOperation) || signal?.aborted)
377
425
  return;
378
426
  const delivery = createRubyRuntimeOwnedPreflightDelivery(payload);
@@ -431,17 +479,20 @@ class Ruby {
431
479
  const location = event.filename && event.lineno
432
480
  ? ` (${event.filename}:${event.lineno}:${event.colno})`
433
481
  : '';
434
- rejectOperation(`Ruby worker script error: ${event.message || 'unknown error'}${location}`);
482
+ rejectOperation(`${this.runtimeLabel} worker script error: ${event.message || 'unknown error'}${location}`);
435
483
  };
436
- worker.onmessageerror = () => rejectOperation('Ruby worker message deserialization failed');
484
+ worker.onmessageerror = () => rejectOperation(`${this.runtimeLabel} worker message deserialization failed`);
437
485
  const transferables = delivery.consume();
438
486
  try {
439
487
  worker.postMessage({
440
488
  load: true,
441
489
  runtimePreflight: delivery.payload,
442
490
  startupContext,
443
- maxAssetBytes: Math.min(limits.maxAssetBytes, RUBY_MAX_ASSET_BYTES)
444
- }, [...transferables]);
491
+ maxAssetBytes: Math.min(limits.maxAssetBytes, RUBY_MAX_ASSET_BYTES),
492
+ ...(interpreterBytes
493
+ ? { language: 'golfscript', interpreterBytes }
494
+ : {})
495
+ }, [...transferables, ...(interpreterBytes ? [interpreterBytes.buffer] : [])]);
445
496
  }
446
497
  finally {
447
498
  delivery.retire();
@@ -501,32 +552,33 @@ class Ruby {
501
552
  signal = options.signal;
502
553
  unbindPreSessionAbort = this.bindPreSessionAbort(activeOperation, signal);
503
554
  if (!this.isOperationActive(activeOperation)) {
504
- return Promise.reject(this.releaseBeforeSession(activeOperation, 'Ruby execution cancelled'));
555
+ return Promise.reject(this.releaseBeforeSession(activeOperation, `${this.runtimeLabel} execution cancelled`));
505
556
  }
506
557
  limits = resolveExecutionLimits(options.limits);
507
- const workspace = validateExecutionWorkspace(code, options.workspaceFiles ?? [], options.activePath ?? 'main.rb', {
558
+ const workspace = validateExecutionWorkspace(code, options.workspaceFiles ?? [], options.activePath ?? this.defaultSourcePath, {
508
559
  ...options.workspaceLimits,
509
560
  maxFileBytes: Math.min(options.workspaceLimits?.maxFileBytes ??
510
561
  DEFAULT_WORKSPACE_LIMITS.maxFileBytes, limits.maxWorkspaceBytes),
511
562
  maxTotalBytes: Math.min(options.workspaceLimits?.maxTotalBytes ??
512
563
  DEFAULT_WORKSPACE_LIMITS.maxTotalBytes, limits.maxWorkspaceBytes)
513
564
  });
514
- const programArgs = resolveSandboxExecutionArgs('RUBY', args, options).programArgs;
565
+ this.validateRuntimeNamespace(workspace.activePath, workspace.workspaceFiles);
566
+ const programArgs = resolveSandboxExecutionArgs(this.runtimeId, args, options).programArgs;
515
567
  const stdin = options.stdin;
516
568
  if (stdin !== undefined && typeof stdin !== 'string') {
517
- throw new RuntimeConfigurationError('Ruby stdin must be a string', {
569
+ throw new RuntimeConfigurationError(`${this.runtimeLabel} stdin must be a string`, {
518
570
  phase: 'execute',
519
- runtimeId: 'RUBY'
571
+ runtimeId: this.runtimeId
520
572
  });
521
573
  }
522
574
  request = {
523
575
  programArgs,
524
576
  stdin,
525
- activePath: workspace.activePath ?? 'main.rb',
577
+ activePath: workspace.activePath ?? this.defaultSourcePath,
526
578
  workspaceFiles: workspace.workspaceFiles
527
579
  };
528
580
  if (!this.isOperationActive(activeOperation) || signal?.aborted) {
529
- return Promise.reject(this.releaseBeforeSession(activeOperation, 'Ruby execution cancelled'));
581
+ return Promise.reject(this.releaseBeforeSession(activeOperation, `${this.runtimeLabel} execution cancelled`));
530
582
  }
531
583
  unbindPreSessionAbort();
532
584
  }
@@ -534,7 +586,7 @@ class Ruby {
534
586
  return Promise.reject(this.releaseBeforeSession(activeOperation, error));
535
587
  }
536
588
  if (!this.isOperationActive(activeOperation)) {
537
- return Promise.reject(this.releaseBeforeSession(activeOperation, 'Ruby execution cancelled'));
589
+ return Promise.reject(this.releaseBeforeSession(activeOperation, `${this.runtimeLabel} execution cancelled`));
538
590
  }
539
591
  const worker = this.worker;
540
592
  if (!worker) {
@@ -603,7 +655,7 @@ class Ruby {
603
655
  if (buffer && !hasExplicitStdin) {
604
656
  this.waitingForInput = true;
605
657
  if (!prepare) {
606
- reportWorkerInputReady(_prog, 'Ruby runtime ready for input');
658
+ reportWorkerInputReady(_prog, `${this.runtimeLabel} runtime ready for input`);
607
659
  if (!ownsRun())
608
660
  return;
609
661
  }
@@ -617,11 +669,11 @@ class Ruby {
617
669
  if (typeof output === 'string' && output.length > 0) {
618
670
  const actual = outputBytes + OUTPUT_ENCODER.encode(output).byteLength;
619
671
  if (actual > limits.maxOutputBytes) {
620
- failRun(new OutputLimitError(`Ruby output exceeded ${limits.maxOutputBytes} bytes`, {
672
+ failRun(new OutputLimitError(`${this.runtimeLabel} output exceeded ${limits.maxOutputBytes} bytes`, {
621
673
  actual,
622
674
  limit: limits.maxOutputBytes,
623
675
  phase: 'execute',
624
- runtimeId: 'RUBY'
676
+ runtimeId: this.runtimeId
625
677
  }), true);
626
678
  return;
627
679
  }
@@ -633,11 +685,11 @@ class Ruby {
633
685
  if (diagnostic !== undefined) {
634
686
  const actual = diagnosticCount + 1;
635
687
  if (actual > limits.maxDiagnostics) {
636
- failRun(new DiagnosticLimitError(`Ruby diagnostics exceeded ${limits.maxDiagnostics} messages`, {
688
+ failRun(new DiagnosticLimitError(`${this.runtimeLabel} diagnostics exceeded ${limits.maxDiagnostics} messages`, {
637
689
  actual,
638
690
  limit: limits.maxDiagnostics,
639
691
  phase: 'execute',
640
- runtimeId: 'RUBY'
692
+ runtimeId: this.runtimeId
641
693
  }), true);
642
694
  return;
643
695
  }
@@ -0,0 +1,15 @@
1
+ export type WasiInterpreterProfile = {
2
+ readonly id: string;
3
+ readonly folder: string;
4
+ readonly fileName: string;
5
+ readonly sourcePath: string;
6
+ readonly sourcePathPrefix?: string;
7
+ readonly maxSourcePathBytes?: number;
8
+ readonly maxSourceBytes?: number;
9
+ readonly minSourceCharacters?: number;
10
+ readonly command: string;
11
+ readonly args: readonly string[];
12
+ readonly sha256: string;
13
+ readonly bytes: number;
14
+ };
15
+ export declare const WASI_INTERPRETERS: Readonly<Record<string, WasiInterpreterProfile>>;