@modelprofile.com/flexharness 11.0.1 → 11.1.0

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 (69) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/classes.codesandbox.d.ts +12 -4
  3. package/dist_ts/classes.codesandbox.js +119 -287
  4. package/dist_ts/classes.codesandbox.worker.d.ts +52 -0
  5. package/dist_ts/classes.codesandbox.worker.js +310 -0
  6. package/dist_ts/classes.flexharness.callbacks.d.ts +37 -1
  7. package/dist_ts/classes.flexharness.callbacks.js +162 -53
  8. package/dist_ts/classes.flexharness.core.d.ts +2 -1
  9. package/dist_ts/classes.flexharness.core.js +22 -18
  10. package/dist_ts/classes.flexharness.results.js +4 -4
  11. package/dist_ts/classes.flexharness.reversions.d.ts +7 -0
  12. package/dist_ts/classes.flexharness.reversions.js +40 -39
  13. package/dist_ts/classes.flexharness.runs.js +7 -3
  14. package/dist_ts/classes.flexharness.tooldispatch.d.ts +2 -1
  15. package/dist_ts/classes.flexharness.tooldispatch.js +27 -9
  16. package/dist_ts/classes.stores.js +15 -13
  17. package/dist_ts/errors.d.ts +5 -0
  18. package/dist_ts/errors.js +10 -1
  19. package/dist_ts/index.d.ts +2 -1
  20. package/dist_ts/index.js +3 -2
  21. package/dist_ts/interfaces.d.ts +31 -9
  22. package/dist_ts/interfaces.flexharness.d.ts +14 -0
  23. package/dist_ts/interfaces.js +3 -3
  24. package/dist_ts/plugins.d.ts +6 -4
  25. package/dist_ts/plugins.js +10 -11
  26. package/dist_ts/utils.harnessoptions.d.ts +3 -1
  27. package/dist_ts/utils.harnessoptions.js +15 -2
  28. package/dist_ts/utils.json.d.ts +9 -1
  29. package/dist_ts/utils.json.js +92 -33
  30. package/dist_ts/utils.storecodecs.d.ts +1 -1
  31. package/dist_ts/utils.storecodecs.js +4 -4
  32. package/dist_ts/utils.storerules.d.ts +3 -3
  33. package/dist_ts/utils.storerules.js +1 -14
  34. package/dist_ts_mcp/index.d.ts +3 -1
  35. package/dist_ts_mcp/index.js +17 -2
  36. package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +18 -6
  37. package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +27 -16
  38. package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +33 -3
  39. package/dist_ts_stores_nosqldb/classes.recordstore.js +78 -18
  40. package/dist_ts_stores_nosqldb/index.d.ts +2 -2
  41. package/dist_ts_stores_nosqldb/index.js +2 -2
  42. package/package.json +3 -3
  43. package/readme.md +8 -2
  44. package/ts/00_commitinfo_data.ts +1 -1
  45. package/ts/classes.codesandbox.ts +132 -276
  46. package/ts/classes.codesandbox.worker.ts +345 -0
  47. package/ts/classes.flexharness.callbacks.ts +207 -55
  48. package/ts/classes.flexharness.core.ts +22 -16
  49. package/ts/classes.flexharness.results.ts +3 -3
  50. package/ts/classes.flexharness.reversions.ts +43 -32
  51. package/ts/classes.flexharness.runs.ts +6 -2
  52. package/ts/classes.flexharness.tooldispatch.ts +23 -7
  53. package/ts/classes.stores.ts +21 -12
  54. package/ts/errors.ts +14 -0
  55. package/ts/index.ts +7 -1
  56. package/ts/interfaces.flexharness.ts +15 -0
  57. package/ts/interfaces.ts +32 -9
  58. package/ts/plugins.ts +9 -12
  59. package/ts/readme.md +8 -2
  60. package/ts/utils.harnessoptions.ts +22 -0
  61. package/ts/utils.json.ts +112 -35
  62. package/ts/utils.storecodecs.ts +3 -3
  63. package/ts/utils.storerules.ts +3 -24
  64. package/ts_mcp/index.ts +17 -1
  65. package/ts_mcp/readme.md +3 -1
  66. package/ts_stores_nosqldb/classes.nosqldbstores.ts +39 -15
  67. package/ts_stores_nosqldb/classes.recordstore.ts +105 -13
  68. package/ts_stores_nosqldb/index.ts +2 -0
  69. package/ts_stores_nosqldb/readme.md +10 -2
@@ -1,9 +1,17 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import { FlexHarnessValidationError } from './errors.js';
3
3
  import type { IFlexCodeModeLimits, IFlexCodeRunFailure, TJsonValue } from './interfaces.js';
4
+ import type { TCodeWorkerPhase, TCodeWorkerReport, TCodeWorkerRequest } from './classes.codesandbox.worker.js';
5
+ import { maxTransferMetadataBytes, truncateUtf8 } from './utils.transfer.js';
4
6
 
5
- /** What one catalog call of a program comes to: a JSON value, or the error the program sees. */
6
- export type TFlexCodeCallOutcome = { ok: true; value: TJsonValue } | { ok: false; error: TJsonValue };
7
+ /**
8
+ * What one catalog call of a program comes to: a JSON value, the error the program sees, or the
9
+ * host's `callbacks` limit, which stops the program without the call.
10
+ */
11
+ export type TFlexCodeCallOutcome =
12
+ | { ok: true; value: TJsonValue }
13
+ | { ok: false; error: TJsonValue }
14
+ | { ok: false; limit: 'callbacks'; message: string };
7
15
 
8
16
  export interface IFlexCodeHost {
9
17
  /**
@@ -24,109 +32,28 @@ type TStop =
24
32
  | { kind: 'aborted'; message: string }
25
33
  | { kind: 'host'; error: unknown };
26
34
 
27
- const programFile = 'program.ts';
28
35
  /** How long calls in flight get to settle after their program stopped and their signal aborted. */
29
36
  const stopGraceMs = 5_000;
37
+ /**
38
+ * How long a program may compute past its time before the host ends its worker. QuickJS checks the
39
+ * time only at its interrupt points, and native work between two of them, such as sorting a large
40
+ * array many times over, can run far longer.
41
+ */
42
+ const cpuGraceMs = 250;
30
43
 
31
- /** Evaluating the prelude, the program's text, or running the program. */
32
- type TPhase = 'prelude' | 'parse' | 'program';
33
44
  const programPrefix = 'globalThis.__flexMain = async function () {\n';
34
45
  const programSuffix = '\n};';
35
46
 
36
- /**
37
- * Runs in the context before the program: it takes the host functions off the global object and
38
- * defines `api`, `ApiError` and `console`. Only strings cross the boundary.
39
- */
40
- const prelude = `'use strict';
41
- (() => {
42
- const call = globalThis.__flexCall;
43
- const log = globalThis.__flexLog;
44
- const done = globalThis.__flexDone;
45
- delete globalThis.__flexCall;
46
- delete globalThis.__flexLog;
47
- delete globalThis.__flexDone;
48
- class ApiError extends Error {
49
- constructor(details) {
50
- const message = details && typeof details.message === 'string' ? details.message : JSON.stringify(details);
51
- super(message);
52
- this.name = 'ApiError';
53
- this.details = details;
54
- }
55
- }
56
- const invoke = (path, args) => {
57
- if (!path) return Promise.reject(new TypeError('Call a method as api.<namespace>.<method>(params).'));
58
- let params;
59
- try {
60
- // api.results.get(handle, { page, path }) reads a stored result.
61
- const argument = path === 'results.get' && typeof args[0] === 'string'
62
- ? Object.assign({}, args[1], { handle: args[0] })
63
- : args[0];
64
- params = JSON.stringify(args.length === 0 || argument === undefined ? {} : argument);
65
- } catch (error) {
66
- return Promise.reject(new TypeError('The params of ' + path + ' are not JSON: ' + error.message));
67
- }
68
- return call(path, params === undefined ? 'null' : params).then((text) => {
69
- const outcome = JSON.parse(text);
70
- if (outcome.ok) return outcome.value;
71
- throw new ApiError(outcome.error);
72
- });
73
- };
74
- const target = (path) => new Proxy(function () {}, {
75
- get: (_target, key) => typeof key === 'string' && key !== 'then' ? target(path ? path + '.' + key : key) : undefined,
76
- apply: (_target, _this, args) => invoke(path, args),
77
- set: () => false,
78
- defineProperty: () => false,
79
- deleteProperty: () => false,
80
- });
81
- const format = (value) => {
82
- if (typeof value === 'string') return value;
83
- try {
84
- const text = JSON.stringify(value);
85
- return text === undefined ? String(value) : text;
86
- } catch {
87
- return String(value);
88
- }
89
- };
90
- const writer = (level) => (...values) => { log(level, values.map(format).join(' ')); };
91
- const fixed = (value) => ({ value, enumerable: false, writable: false, configurable: false });
92
- Object.defineProperty(globalThis, 'api', fixed(target('')));
93
- Object.defineProperty(globalThis, 'ApiError', fixed(ApiError));
94
- Object.defineProperty(globalThis, 'console', fixed(Object.freeze({
95
- log: writer('log'), info: writer('info'), warn: writer('warn'), error: writer('error'), debug: writer('debug'),
96
- })));
97
- const settle = (value) => {
98
- try {
99
- const text = JSON.stringify({ ok: true, value: value === undefined ? null : value });
100
- return text === undefined ? JSON.stringify({ ok: true, value: null }) : text;
101
- } catch (error) {
102
- return JSON.stringify({ ok: false, name: 'TypeError', message: 'The return value is not JSON: ' + error.message });
103
- }
104
- };
105
- const fail = (error) => JSON.stringify({
106
- ok: false,
107
- name: error && typeof error.name === 'string' ? error.name : 'Error',
108
- message: error && typeof error.message === 'string' ? error.message : String(error),
109
- stack: error && typeof error.stack === 'string' ? error.stack : '',
110
- ...(error instanceof ApiError ? { details: error.details } : {}),
111
- });
112
- Object.defineProperty(globalThis, '__flexStart', {
113
- configurable: true,
114
- value: () => {
115
- const main = globalThis.__flexMain;
116
- delete globalThis.__flexStart;
117
- delete globalThis.__flexMain;
118
- main().then(settle, fail).then(done);
119
- },
120
- });
121
- })();
122
- `;
47
+ /** The worker module beside this one, compiled or, where the package runs uncompiled, its source. */
48
+ const workerUrl = new URL(`./classes.codesandbox.worker${plugins.path.extname(import.meta.url)}`, import.meta.url);
123
49
 
124
50
  let enginePromise: Promise<plugins.ICodeSandboxEngine> | undefined;
125
51
 
126
52
  /**
127
- * Runs `run_code` programs in QuickJS-ng compiled to WebAssembly: every program in a WebAssembly
128
- * instance of its own, with no globals beyond the language, `api`, `ApiError` and `console`, and
129
- * every limit of `IFlexCodeModeLimits` enforced. Program types are stripped with amaro first.
53
+ * Runs `run_code` programs in QuickJS-ng compiled to WebAssembly: every program in a worker thread and
54
+ * WebAssembly instance of its own, with no globals beyond the language, `api`, `ApiError` and
55
+ * `console`, and every limit of `IFlexCodeModeLimits` enforced. Program types are stripped with amaro
56
+ * first.
130
57
  */
131
58
  export class FlexCodeSandbox {
132
59
  private constructor(private readonly engine: plugins.ICodeSandboxEngine) {}
@@ -155,27 +82,26 @@ export class FlexCodeSandbox {
155
82
  }
156
83
 
157
84
  class CodeRun {
158
- private runtime?: plugins.IQuickJsRuntime;
159
- private vm?: plugins.IQuickJsContext;
160
- private readonly deferreds = new Set<plugins.IQuickJsDeferredPromise>();
85
+ private worker?: plugins.Worker;
86
+ private terminated?: Promise<unknown>;
87
+ private program = '';
161
88
  private readonly calls = new AbortController();
162
89
  private readonly queue: Array<() => void> = [];
163
90
  /** Host calls in flight; the run settles only after every one of them did. */
164
91
  private readonly inFlight = new Set<Promise<void>>();
165
92
  private running = 0;
166
93
  private callCount = 0;
94
+ /** Requests the worker has not finished computing on: while there are any, the watchdog runs. */
95
+ private busy = 0;
96
+ /** What the program computed by the worker's last report. */
167
97
  private cpuMs = 0;
168
- private enteredAt?: number;
98
+ private watchdog?: ReturnType<typeof setTimeout>;
169
99
  private stop?: TStop;
170
100
  private logs = '';
171
- private logBytes = 0;
172
- private logsCut = false;
173
101
  private settle!: (result: TFlexCodeRunResult) => void;
174
102
  private reject!: (error: unknown) => void;
175
103
  /** The outcome is decided; the run waits for its calls in flight, then settles. */
176
104
  private closing = false;
177
- private finished = false;
178
- private broken = false;
179
105
  private wallTimer?: ReturnType<typeof setTimeout>;
180
106
  private readonly startedAt = performance.now();
181
107
 
@@ -212,121 +138,97 @@ class CodeRun {
212
138
  const onAbort = () => this.halt({ kind: 'aborted', message: 'The program was stopped.' });
213
139
  this.signal?.addEventListener('abort', onAbort, { once: true });
214
140
  try {
215
- const module = await this.engine.newModule();
216
- // QuickJS runs on the host's stack too: start it from a fresh, shallow one.
217
- await new Promise<void>((resolve) => setImmediate(resolve));
218
141
  if (this.signal?.aborted) onAbort();
219
- else {
220
- this.armWallTimer();
221
- this.boot(module);
222
- }
142
+ else this.spawn();
223
143
  return await outcome;
224
144
  } finally {
225
145
  this.signal?.removeEventListener('abort', onAbort);
226
- this.dispose();
146
+ await this.dispose();
227
147
  }
228
148
  }
229
149
 
230
- private program = '';
231
-
232
- private boot(module: plugins.IQuickJsModule): void {
233
- const runtime = module.newRuntime();
234
- this.runtime = runtime;
235
- runtime.setMemoryLimit(this.limits.memoryBytes);
236
- runtime.setMaxStackSize(this.limits.stackBytes);
237
- runtime.setInterruptHandler(() => this.interrupted());
238
- const vm = runtime.newContext();
239
- this.vm = vm;
240
- this.define('__flexCall', (pathHandle, paramsHandle) => this.bridgeCall(vm.getString(pathHandle), vm.getString(paramsHandle)));
241
- this.define('__flexLog', (levelHandle, textHandle) => {
242
- this.log(vm.getString(levelHandle), vm.getString(textHandle));
243
- });
244
- this.define('__flexDone', (textHandle) => {
245
- this.complete(vm.getString(textHandle));
150
+ private spawn(): void {
151
+ const worker = new plugins.Worker(workerUrl);
152
+ this.worker = worker;
153
+ worker.on('message', (report: TCodeWorkerReport) => this.receive(report));
154
+ worker.on('error', (error: unknown) => this.halt({ kind: 'host', error }));
155
+ worker.on('exit', (code: number) => {
156
+ this.halt({ kind: 'host', error: new Error(`The code mode worker exited with code ${code}.`) });
246
157
  });
247
- if (!this.evaluate(prelude, 'prelude.js', 'prelude')) return;
248
- if (!this.evaluate(this.program, programFile, 'parse')) return;
249
- if (!this.evaluate('__flexStart();', 'start.js', 'program')) return;
250
- this.pump();
251
158
  }
252
159
 
253
- private define(name: string, implementation: (...args: plugins.IQuickJsHandle[]) => plugins.IQuickJsHandle | void): void {
254
- const handle = this.vm!.newFunction(name, implementation);
255
- this.vm!.setProp(this.vm!.global, name, handle);
256
- handle.dispose();
257
- }
258
-
259
- private evaluate(code: string, file: string, phase: TPhase): boolean {
260
- if (this.closing) return false;
261
- let result: plugins.TQuickJsResult;
262
- this.enter();
263
- try {
264
- result = this.vm!.evalCode(code, file);
265
- } catch (error) {
266
- this.hostThrow(error);
267
- return false;
268
- } finally {
269
- this.leave();
270
- }
271
- if (result.error) {
272
- this.failWith(result.error, phase);
273
- return false;
274
- }
275
- result.value.dispose();
276
- return true;
277
- }
278
-
279
- /** Runs the program's pending jobs after a call settled or the program started. */
280
- private pump(): void {
281
- if (this.closing || !this.runtime?.alive) return;
282
- let result: { error?: plugins.IQuickJsHandle };
283
- this.enter();
284
- try {
285
- result = this.runtime.executePendingJobs();
286
- } catch (error) {
287
- this.hostThrow(error);
288
- return;
289
- } finally {
290
- this.leave();
160
+ private receive(report: TCodeWorkerReport): void {
161
+ if (this.closing) return;
162
+ switch (report.kind) {
163
+ case 'ready':
164
+ this.armWallTimer();
165
+ this.send({
166
+ kind: 'start',
167
+ program: this.program,
168
+ limits: {
169
+ cpuMs: this.limits.cpuMs,
170
+ memoryBytes: this.limits.memoryBytes,
171
+ stackBytes: this.limits.stackBytes,
172
+ maxLogBytes: this.limits.maxLogBytes,
173
+ },
174
+ });
175
+ return;
176
+ case 'call':
177
+ this.call(report.id, report.method, report.params);
178
+ return;
179
+ case 'log':
180
+ this.logs += report.text;
181
+ return;
182
+ case 'idle':
183
+ this.idle(report.cpuMs);
184
+ return;
185
+ case 'done':
186
+ this.complete(report.text, report.unawaited);
187
+ return;
188
+ case 'failed':
189
+ if (report.phase === 'prelude') {
190
+ this.halt({ kind: 'host', error: new Error(`The code mode prelude failed: ${report.dumped}`) });
191
+ return;
192
+ }
193
+ this.done(this.failure(this.classify(report, report.phase)));
194
+ return;
195
+ case 'stopped':
196
+ this.halt(report.limit === 'cpu'
197
+ ? this.cpuLimit()
198
+ : { kind: 'limit', limit: 'stack', message: 'The program exceeded its stack.' });
199
+ return;
291
200
  }
292
- if (result.error) this.failWith(result.error, 'program');
293
201
  }
294
202
 
295
- /**
296
- * An exception out of the WebAssembly instance: the host's stack ran out under deep recursion, or
297
- * the instance failed. Its state is not trusted afterwards; `dispose()` abandons it.
298
- */
299
- private hostThrow(error: unknown): void {
300
- this.broken = true;
301
- if (error instanceof RangeError) {
302
- this.halt({ kind: 'limit', limit: 'stack', message: 'The program exceeded its stack.' });
303
- return;
304
- }
305
- this.halt({ kind: 'host', error });
203
+ /** Has the worker compute: start the program, or go on with an answer. */
204
+ private send(request: TCodeWorkerRequest): void {
205
+ this.busy += 1;
206
+ this.worker!.postMessage(request);
207
+ this.armWatchdog();
306
208
  }
307
209
 
308
- private enter(): void {
309
- this.enteredAt = performance.now();
210
+ private idle(cpuMs: number): void {
211
+ this.cpuMs = cpuMs;
212
+ this.busy -= 1;
213
+ clearTimeout(this.watchdog);
214
+ this.watchdog = undefined;
215
+ this.armWatchdog();
310
216
  }
311
217
 
312
- private leave(): void {
313
- if (this.enteredAt === undefined) return;
314
- this.cpuMs += performance.now() - this.enteredAt;
315
- this.enteredAt = undefined;
218
+ /** While the worker computes, ends it once the program's time and `cpuGraceMs` are up. */
219
+ private armWatchdog(): void {
220
+ if (this.watchdog || this.busy === 0) return;
221
+ this.watchdog = setTimeout(
222
+ () => this.halt(this.cpuLimit()),
223
+ Math.max(0, this.limits.cpuMs - this.cpuMs) + cpuGraceMs,
224
+ );
316
225
  }
317
226
 
318
- private interrupted(): boolean {
319
- if (this.stop) return true;
320
- if (this.enteredAt !== undefined && this.cpuMs + performance.now() - this.enteredAt > this.limits.cpuMs) {
321
- this.stop = { kind: 'limit', limit: 'cpu', message: `The program computed for more than ${this.limits.cpuMs} ms.` };
322
- return true;
323
- }
324
- return false;
227
+ private cpuLimit(): TStop {
228
+ return { kind: 'limit', limit: 'cpu', message: `The program computed for more than ${this.limits.cpuMs} ms.` };
325
229
  }
326
230
 
327
- private bridgeCall(method: string, paramsText: string): plugins.IQuickJsHandle {
328
- const deferred = this.vm!.newPromise();
329
- this.deferreds.add(deferred);
231
+ private call(id: number, method: string, paramsText: string): void {
330
232
  this.callCount += 1;
331
233
  if (this.callCount > this.limits.maxCalls) {
332
234
  this.halt({
@@ -334,7 +236,7 @@ class CodeRun {
334
236
  limit: 'calls',
335
237
  message: `The program made more than ${this.limits.maxCalls} calls.`,
336
238
  });
337
- return deferred.handle;
239
+ return;
338
240
  }
339
241
  let params: unknown;
340
242
  try {
@@ -345,7 +247,7 @@ class CodeRun {
345
247
  const run = (): void => {
346
248
  if (this.closing) return;
347
249
  this.running += 1;
348
- // A settled call stops counting before the program sees its answer, which may end the program.
250
+ // A settled call leaves the calls in flight before its outcome is handled, which may end the run.
349
251
  const settled = (): void => {
350
252
  this.running -= 1;
351
253
  this.inFlight.delete(call);
@@ -353,7 +255,13 @@ class CodeRun {
353
255
  const call: Promise<void> = this.host.call(method, params, this.calls.signal).then(
354
256
  (outcome) => {
355
257
  settled();
356
- this.answer(deferred, outcome);
258
+ if ('limit' in outcome) {
259
+ // The host did not make the call.
260
+ this.callCount -= 1;
261
+ this.halt({ kind: 'limit', limit: outcome.limit, message: outcome.message });
262
+ return;
263
+ }
264
+ this.answer(id, outcome);
357
265
  },
358
266
  (error: unknown) => {
359
267
  settled();
@@ -364,39 +272,20 @@ class CodeRun {
364
272
  };
365
273
  if (this.running < this.limits.maxConcurrentCalls) run();
366
274
  else this.queue.push(run);
367
- return deferred.handle;
368
275
  }
369
276
 
370
- private answer(deferred: plugins.IQuickJsDeferredPromise, outcome: TFlexCodeCallOutcome): void {
371
- if (this.closing || !deferred.alive) return;
277
+ private answer(id: number, outcome: TFlexCodeCallOutcome): void {
278
+ if (this.closing) return;
372
279
  let text: string;
373
280
  try {
374
281
  text = JSON.stringify(outcome);
375
282
  } catch {
376
283
  text = JSON.stringify({ ok: false, error: { error: 'failed', message: 'The result is not JSON.' } });
377
284
  }
378
- const value = this.vm!.newString(text);
379
- deferred.resolve(value);
380
- value.dispose();
381
- this.deferreds.delete(deferred);
382
- this.pump();
383
- }
384
-
385
- private log(level: string, text: string): void {
386
- if (this.logsCut || this.closing) return;
387
- const line = `${level === 'log' ? '' : `[${level}] `}${text}\n`;
388
- const bytes = Buffer.byteLength(line, 'utf8');
389
- if (this.logBytes + bytes > this.limits.maxLogBytes) {
390
- this.logsCut = true;
391
- this.logs += '[console output cut off]\n';
392
- return;
393
- }
394
- this.logBytes += bytes;
395
- this.logs += line;
285
+ this.send({ kind: 'answer', id, text });
396
286
  }
397
287
 
398
- private complete(text: string): void {
399
- if (this.closing) return;
288
+ private complete(text: string, unawaited: number): void {
400
289
  let outcome: { ok: boolean; value?: TJsonValue; name?: string; message?: string; stack?: string };
401
290
  try {
402
291
  outcome = JSON.parse(text);
@@ -404,7 +293,6 @@ class CodeRun {
404
293
  outcome = { ok: false, name: 'Error', message: 'The program ended without a result.' };
405
294
  }
406
295
  if (outcome.ok) {
407
- const unawaited = this.running + this.queue.length;
408
296
  if (unawaited > 0) {
409
297
  // Calls the program did not await are not part of its result: it fails, and proposes nothing.
410
298
  this.done(this.failure({
@@ -424,36 +312,13 @@ class CodeRun {
424
312
  this.done(this.failure(this.classify(outcome, 'program')));
425
313
  }
426
314
 
427
- private failWith(handle: plugins.IQuickJsHandle, phase: TPhase): void {
428
- let dumped: unknown;
429
- // Reading the thrown value can run program code, such as a toJSON: it counts as computing.
430
- this.enter();
431
- try {
432
- dumped = this.vm!.dump(handle);
433
- } catch (error) {
434
- if (!(error instanceof Error) || error instanceof RangeError) this.broken = true;
435
- dumped = undefined;
436
- } finally {
437
- this.leave();
438
- if (!this.broken && handle.alive) handle.dispose();
439
- }
440
- if (phase === 'prelude' && !this.stop) {
441
- this.halt({ kind: 'host', error: new Error(`The code mode prelude failed: ${JSON.stringify(dumped)}`) });
442
- return;
443
- }
444
- const error = (dumped && typeof dumped === 'object' ? dumped : { message: String(dumped) }) as {
445
- name?: string; message?: string; stack?: string;
446
- };
447
- this.done(this.failure(this.classify(error, phase)));
448
- }
449
-
450
315
  private classify(
451
316
  error: { name?: string; message?: string; stack?: string },
452
- phase: TPhase,
317
+ phase: TCodeWorkerPhase,
453
318
  ): Omit<IFlexCodeRunFailure, 'calls'> {
454
- if (this.stop?.kind === 'limit') return { error: 'limit_exceeded', limit: this.stop.limit, message: this.stop.message };
455
- if (this.stop?.kind === 'aborted') return { error: 'aborted', message: this.stop.message };
456
- const message = typeof error.message === 'string' ? error.message : 'The program failed.';
319
+ const message = typeof error.message === 'string'
320
+ ? truncateUtf8(error.message, maxTransferMetadataBytes)
321
+ : 'The program failed.';
457
322
  if (error.name === 'InternalError' && message === 'out of memory') {
458
323
  return { error: 'limit_exceeded', limit: 'memory', message: `The program used more than ${this.limits.memoryBytes} bytes of memory.` };
459
324
  }
@@ -463,13 +328,11 @@ class CodeRun {
463
328
  ) {
464
329
  return { error: 'limit_exceeded', limit: 'stack', message: 'The program exceeded its stack.' };
465
330
  }
466
- if (error.name === 'InternalError' && message === 'interrupted') {
467
- return { error: 'limit_exceeded', limit: 'cpu', message: `The program computed for more than ${this.limits.cpuMs} ms.` };
468
- }
331
+ // The worker evaluates the program's text as program.ts.
469
332
  const line = /program\.ts:(\d+)/.exec(error.stack ?? '')?.[1];
470
333
  return {
471
334
  error: phase === 'parse' && error.name === 'SyntaxError' ? 'syntax_error' : 'program_error',
472
- ...(typeof error.name === 'string' ? { name: error.name } : {}),
335
+ ...(typeof error.name === 'string' ? { name: truncateUtf8(error.name, maxTransferMetadataBytes) } : {}),
473
336
  message,
474
337
  ...(line && Number(line) > 1 ? { line: Number(line) - 1 } : {}),
475
338
  };
@@ -505,16 +368,17 @@ class CodeRun {
505
368
  }
506
369
 
507
370
  /**
508
- * Decides the run's outcome. Calls not started yet never start. Calls in flight are told to stop
509
- * through their signal, and the outcome is delivered once they settled, so no call runs on after
510
- * its program; a host call that ignores its signal gets `stopGraceMs`, and its answer is dropped.
371
+ * Decides the run's outcome and ends the program's worker. Calls not started yet never start. Calls
372
+ * in flight are told to stop through their signal, and the outcome is delivered once they settled,
373
+ * so no call runs on after its program; a host call that ignores its signal gets `stopGraceMs`, and
374
+ * its answer is dropped.
511
375
  */
512
376
  private close(deliver: () => void): void {
513
377
  if (this.closing) return;
514
378
  this.closing = true;
515
379
  this.queue.length = 0;
380
+ void this.terminate();
516
381
  if (this.inFlight.size === 0) {
517
- this.finished = true;
518
382
  deliver();
519
383
  return;
520
384
  }
@@ -527,7 +391,6 @@ class CodeRun {
527
391
  }),
528
392
  ]).then(() => {
529
393
  clearTimeout(grace);
530
- this.finished = true;
531
394
  deliver();
532
395
  });
533
396
  }
@@ -545,25 +408,18 @@ class CodeRun {
545
408
  this.wallTimer = setTimeout(check, Math.max(10, Math.min(250, this.limits.wallMs)));
546
409
  }
547
410
 
548
- /**
549
- * Frees the program's runtime. An instance that threw, or whose runtime cannot be freed after a
550
- * stack overflow left objects behind, is abandoned instead: it is the program's own, and garbage
551
- * collection reclaims its memory.
552
- */
553
- private dispose(): void {
411
+ /** Ends the program's worker, whatever it computes, which frees its WebAssembly instance with it. */
412
+ private terminate(): Promise<unknown> {
413
+ clearTimeout(this.watchdog);
414
+ this.terminated ??= this.worker?.terminate() ?? Promise.resolve();
415
+ return this.terminated;
416
+ }
417
+
418
+ private async dispose(): Promise<void> {
554
419
  this.closing = true;
555
- this.finished = true;
556
420
  if (this.wallTimer) clearTimeout(this.wallTimer);
557
421
  this.queue.length = 0;
558
422
  this.calls.abort();
559
- if (this.broken) return;
560
- try {
561
- for (const deferred of this.deferreds) if (deferred.alive) deferred.dispose();
562
- this.deferreds.clear();
563
- this.vm?.dispose();
564
- if (this.runtime?.alive) this.runtime.dispose();
565
- } catch {
566
- this.broken = true;
567
- }
423
+ await this.terminate();
568
424
  }
569
425
  }