@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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.codesandbox.d.ts +12 -4
- package/dist_ts/classes.codesandbox.js +119 -287
- package/dist_ts/classes.codesandbox.worker.d.ts +52 -0
- package/dist_ts/classes.codesandbox.worker.js +310 -0
- package/dist_ts/classes.flexharness.callbacks.d.ts +37 -1
- package/dist_ts/classes.flexharness.callbacks.js +162 -53
- package/dist_ts/classes.flexharness.core.d.ts +2 -1
- package/dist_ts/classes.flexharness.core.js +22 -18
- package/dist_ts/classes.flexharness.results.js +4 -4
- package/dist_ts/classes.flexharness.reversions.d.ts +7 -0
- package/dist_ts/classes.flexharness.reversions.js +40 -39
- package/dist_ts/classes.flexharness.runs.js +7 -3
- package/dist_ts/classes.flexharness.tooldispatch.d.ts +2 -1
- package/dist_ts/classes.flexharness.tooldispatch.js +27 -9
- package/dist_ts/classes.stores.js +15 -13
- package/dist_ts/errors.d.ts +5 -0
- package/dist_ts/errors.js +10 -1
- package/dist_ts/index.d.ts +2 -1
- package/dist_ts/index.js +3 -2
- package/dist_ts/interfaces.d.ts +31 -9
- package/dist_ts/interfaces.flexharness.d.ts +14 -0
- package/dist_ts/interfaces.js +3 -3
- package/dist_ts/plugins.d.ts +6 -4
- package/dist_ts/plugins.js +10 -11
- package/dist_ts/utils.harnessoptions.d.ts +3 -1
- package/dist_ts/utils.harnessoptions.js +15 -2
- package/dist_ts/utils.json.d.ts +9 -1
- package/dist_ts/utils.json.js +92 -33
- package/dist_ts/utils.storecodecs.d.ts +1 -1
- package/dist_ts/utils.storecodecs.js +4 -4
- package/dist_ts/utils.storerules.d.ts +3 -3
- package/dist_ts/utils.storerules.js +1 -14
- package/dist_ts_mcp/index.d.ts +3 -1
- package/dist_ts_mcp/index.js +17 -2
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +18 -6
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +27 -16
- package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +33 -3
- package/dist_ts_stores_nosqldb/classes.recordstore.js +78 -18
- package/dist_ts_stores_nosqldb/index.d.ts +2 -2
- package/dist_ts_stores_nosqldb/index.js +2 -2
- package/package.json +3 -3
- package/readme.md +8 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.codesandbox.ts +132 -276
- package/ts/classes.codesandbox.worker.ts +345 -0
- package/ts/classes.flexharness.callbacks.ts +207 -55
- package/ts/classes.flexharness.core.ts +22 -16
- package/ts/classes.flexharness.results.ts +3 -3
- package/ts/classes.flexharness.reversions.ts +43 -32
- package/ts/classes.flexharness.runs.ts +6 -2
- package/ts/classes.flexharness.tooldispatch.ts +23 -7
- package/ts/classes.stores.ts +21 -12
- package/ts/errors.ts +14 -0
- package/ts/index.ts +7 -1
- package/ts/interfaces.flexharness.ts +15 -0
- package/ts/interfaces.ts +32 -9
- package/ts/plugins.ts +9 -12
- package/ts/readme.md +8 -2
- package/ts/utils.harnessoptions.ts +22 -0
- package/ts/utils.json.ts +112 -35
- package/ts/utils.storecodecs.ts +3 -3
- package/ts/utils.storerules.ts +3 -24
- package/ts_mcp/index.ts +17 -1
- package/ts_mcp/readme.md +3 -1
- package/ts_stores_nosqldb/classes.nosqldbstores.ts +39 -15
- package/ts_stores_nosqldb/classes.recordstore.ts +105 -13
- package/ts_stores_nosqldb/index.ts +2 -0
- 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
|
-
/**
|
|
6
|
-
|
|
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
|
-
|
|
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
|
|
128
|
-
* instance of its own, with no globals beyond the language, `api`, `ApiError` and
|
|
129
|
-
* every limit of `IFlexCodeModeLimits` enforced. Program types are stripped with amaro
|
|
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
|
|
159
|
-
private
|
|
160
|
-
private
|
|
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
|
|
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
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
this.
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
|
309
|
-
this.
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
this.
|
|
315
|
-
this.
|
|
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
|
|
319
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
371
|
-
if (this.closing
|
|
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
|
-
|
|
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:
|
|
317
|
+
phase: TCodeWorkerPhase,
|
|
453
318
|
): Omit<IFlexCodeRunFailure, 'calls'> {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
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
|
|
509
|
-
* through their signal, and the outcome is delivered once they settled,
|
|
510
|
-
* its program; a host call that ignores its signal gets `stopGraceMs`, and
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
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
|
}
|