pi-supernova 0.0.15 → 0.2.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/CHANGELOG.md +31 -0
- package/README.md +87 -33
- package/bottleneck.js +98 -97
- package/catalog.js +7 -32
- package/config.js +1 -2
- package/decode.js +61 -0
- package/evidence.js +14 -5
- package/format.js +16 -17
- package/guest-worker.js +29 -150
- package/host-bridge.js +152 -67
- package/index.js +82 -101
- package/ledger.js +73 -101
- package/omp-frame.js +0 -1
- package/package.json +7 -3
- package/parallel.js +42 -34
- package/patch.js +62 -72
- package/render-measure.js +48 -118
- package/render.js +12 -13
- package/repo-index.js +16 -7
- package/runtime.js +204 -210
- package/snap.js +183 -214
- package/vfs.js +114 -70
- package/workspace.js +37 -33
package/runtime.js
CHANGED
|
@@ -1,276 +1,270 @@
|
|
|
1
1
|
import { Worker } from "node:worker_threads";
|
|
2
|
+
import { parse } from "acorn";
|
|
2
3
|
import { performance } from "node:perf_hooks";
|
|
3
4
|
import { packageFinalReturn } from "./bottleneck.js";
|
|
4
|
-
import { isFunction, isObject } from "./decode.js";
|
|
5
|
-
|
|
6
|
-
// Guest code runs in a worker thread (see guest-worker.js). The host thread
|
|
7
|
-
// owns the bridge and answers nova.* RPCs; a hard timeout or abort terminates
|
|
8
|
-
// the worker, which is the only way to stop a synchronous loop.
|
|
5
|
+
import { isFunction, isObject, isString } from "./decode.js";
|
|
9
6
|
|
|
10
7
|
const WORKER_URL = new URL("./guest-worker.js", import.meta.url);
|
|
11
8
|
const ABORT_MESSAGE = "supernova timed out or aborted: pass timeoutMs to allow longer runs, or split the program";
|
|
12
|
-
// Bun ignores worker resourceLimits, so a process-RSS watchdog backs up the V8 heap cap.
|
|
13
9
|
const MEMORY_POLL_MS = 50;
|
|
14
10
|
const MEMORY_SLACK = 1.5;
|
|
15
|
-
|
|
16
11
|
const rssBytes = isFunction(process.memoryUsage?.rss) ? () => process.memoryUsage.rss() : () => process.memoryUsage().rss;
|
|
17
|
-
|
|
18
12
|
let idleWorker = null;
|
|
19
13
|
let runSeq = 0;
|
|
20
14
|
|
|
15
|
+
const PARSE_OPTIONS = { ecmaVersion: "latest", sourceType: "module", allowReturnOutsideFunction: true, allowAwaitOutsideFunction: true };
|
|
16
|
+
const FUNCTION_TYPES = new Set(["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"]);
|
|
17
|
+
|
|
18
|
+
function hasReturn(node) {
|
|
19
|
+
if (!isObject(node)) return false;
|
|
20
|
+
if (node.type === "ReturnStatement") return true;
|
|
21
|
+
if (FUNCTION_TYPES.has(node.type)) return false;
|
|
22
|
+
return Object.values(node).some(value => Array.isArray(value) ? value.some(hasReturn) : hasReturn(value));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function prepareProgram(code) {
|
|
26
|
+
let program;
|
|
27
|
+
let expression;
|
|
28
|
+
let expressionSource;
|
|
29
|
+
try {
|
|
30
|
+
program = parse(code, PARSE_OPTIONS);
|
|
31
|
+
const statements = program.body.filter(node => node.type !== "EmptyStatement");
|
|
32
|
+
const statement = statements.length === 1 ? statements[0] : undefined;
|
|
33
|
+
const candidate = statement?.type === "ExpressionStatement" ? statement.expression : statement;
|
|
34
|
+
if (candidate && FUNCTION_TYPES.has(candidate.type)) {
|
|
35
|
+
expression = candidate;
|
|
36
|
+
expressionSource = code.slice(0, statement.end).replace(/;\s*$/, "");
|
|
37
|
+
}
|
|
38
|
+
} catch (bodyError) {
|
|
39
|
+
expressionSource = code.trimEnd().replace(/;+\s*$/, "");
|
|
40
|
+
try {
|
|
41
|
+
const wrapped = parse("(" + expressionSource + "\n)", PARSE_OPTIONS);
|
|
42
|
+
expression = wrapped.body[0]?.expression;
|
|
43
|
+
if (!expression || !FUNCTION_TYPES.has(expression.type)) throw bodyError;
|
|
44
|
+
} catch { throw bodyError; }
|
|
45
|
+
}
|
|
46
|
+
const body = expression ? "return await (" + expressionSource + "\n)();" : code;
|
|
47
|
+
const returns = expression
|
|
48
|
+
? expression.type === "ArrowFunctionExpression" && expression.body.type !== "BlockStatement" || hasReturn(expression.body)
|
|
49
|
+
: hasReturn(program);
|
|
50
|
+
return { body, hasReturn: returns };
|
|
51
|
+
}
|
|
52
|
+
|
|
21
53
|
function spawnWorker(config) {
|
|
22
|
-
const
|
|
23
|
-
const worker = new Worker(WORKER_URL, {
|
|
24
|
-
|
|
54
|
+
const maxHeapMb = config.maxHeapMb ?? 512;
|
|
55
|
+
const worker = new Worker(WORKER_URL, { resourceLimits: { maxOldGenerationSizeMb: maxHeapMb } });
|
|
56
|
+
const handle = { worker, maxHeapMb, dead: false, ready: null };
|
|
57
|
+
// This listener also owns errors between readiness and a run's listeners.
|
|
58
|
+
worker.on("error", () => { handle.dead = true; });
|
|
59
|
+
worker.on("exit", () => {
|
|
60
|
+
handle.dead = true;
|
|
61
|
+
if (idleWorker === handle) idleWorker = null;
|
|
25
62
|
});
|
|
26
|
-
const handle = { worker, dead: false, ready: null };
|
|
27
63
|
handle.ready = new Promise((resolve, reject) => {
|
|
64
|
+
const cleanup = () => {
|
|
65
|
+
worker.off("message", onMessage);
|
|
66
|
+
worker.off("error", onFail);
|
|
67
|
+
worker.off("exit", onFail);
|
|
68
|
+
};
|
|
28
69
|
const onMessage = (msg) => {
|
|
29
|
-
if (msg?.op
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
70
|
+
if (msg?.op !== "ready") return;
|
|
71
|
+
cleanup();
|
|
72
|
+
resolve();
|
|
33
73
|
};
|
|
34
74
|
const onFail = (err) => {
|
|
35
75
|
cleanup();
|
|
36
|
-
handle.dead = true;
|
|
37
76
|
reject(err instanceof Error ? err : new Error("guest worker exited before ready (code " + err + ")"));
|
|
38
77
|
};
|
|
39
|
-
const cleanup = () => {
|
|
40
|
-
worker.off("message", onMessage);
|
|
41
|
-
worker.off("error", onFail);
|
|
42
|
-
worker.off("exit", onFail);
|
|
43
|
-
};
|
|
44
78
|
worker.on("message", onMessage);
|
|
45
79
|
worker.on("error", onFail);
|
|
46
80
|
worker.on("exit", onFail);
|
|
47
81
|
});
|
|
48
82
|
handle.ready.catch(() => {});
|
|
49
|
-
worker.on("exit", () => {
|
|
50
|
-
handle.dead = true;
|
|
51
|
-
if (idleWorker === handle) idleWorker = null;
|
|
52
|
-
});
|
|
53
83
|
return handle;
|
|
54
84
|
}
|
|
55
85
|
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
if (
|
|
86
|
+
function killWorker(handle) {
|
|
87
|
+
if (!handle) return;
|
|
88
|
+
if (idleWorker === handle) idleWorker = null;
|
|
89
|
+
handle.dead = true;
|
|
90
|
+
return handle.worker.terminate();
|
|
59
91
|
}
|
|
60
92
|
|
|
61
93
|
function acquireWorker(config) {
|
|
62
|
-
const
|
|
94
|
+
const candidate = idleWorker;
|
|
63
95
|
idleWorker = null;
|
|
64
|
-
|
|
96
|
+
const reusable = candidate && !candidate.dead && candidate.maxHeapMb === (config.maxHeapMb ?? 512);
|
|
97
|
+
if (candidate && !reusable) void killWorker(candidate);
|
|
98
|
+
const handle = reusable ? candidate : spawnWorker(config);
|
|
99
|
+
handle.worker.ref?.();
|
|
65
100
|
return handle;
|
|
66
101
|
}
|
|
67
102
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (idleWorker && idleWorker !== handle) {
|
|
71
|
-
void handle.worker.terminate();
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
idleWorker = handle;
|
|
75
|
-
setIdleRef(handle, true);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function killWorker(handle) {
|
|
79
|
-
handle.dead = true;
|
|
80
|
-
if (idleWorker === handle) idleWorker = null;
|
|
81
|
-
void handle.worker.terminate();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Pre-spawn the guest worker so the first program does not pay startup cost. */
|
|
85
|
-
export function warmGuestWorker(config) {
|
|
103
|
+
/** Only pristine workers may be prewarmed. A used worker is never pooled. */
|
|
104
|
+
export function warmGuestWorker(config = {}) {
|
|
86
105
|
if (idleWorker && !idleWorker.dead) return idleWorker.ready;
|
|
87
|
-
const handle = spawnWorker(config
|
|
106
|
+
const handle = spawnWorker(config);
|
|
88
107
|
idleWorker = handle;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
() => {},
|
|
93
|
-
);
|
|
108
|
+
handle.ready.then(() => {
|
|
109
|
+
if (idleWorker === handle) handle.worker.unref?.();
|
|
110
|
+
}, () => {});
|
|
94
111
|
return handle.ready;
|
|
95
112
|
}
|
|
96
113
|
|
|
97
114
|
const RPC_METHODS = {
|
|
98
|
-
call: (nova, args) =>
|
|
99
|
-
if (!isFunction(nova?.call)) throw new Error("nova.call unavailable");
|
|
100
|
-
return nova.call(args[0], args[1]);
|
|
101
|
-
},
|
|
115
|
+
call: (nova, args) => nova.call(args[0], args[1]),
|
|
102
116
|
callMany: async (nova, args) => {
|
|
103
|
-
if (!isFunction(nova?.callMany)) throw new Error("nova.callMany unavailable");
|
|
104
117
|
const wave = await nova.callMany(args[0]);
|
|
105
|
-
|
|
106
|
-
return wave;
|
|
107
|
-
},
|
|
108
|
-
search: (nova, args) => {
|
|
109
|
-
if (!isFunction(nova?.search)) throw new Error("nova.search unavailable");
|
|
110
|
-
return nova.search(args[0], args[1]);
|
|
111
|
-
},
|
|
112
|
-
describe: (nova, args) => {
|
|
113
|
-
if (!isFunction(nova?.describe)) throw new Error("nova.describe unavailable");
|
|
114
|
-
return nova.describe(args[0]);
|
|
115
|
-
},
|
|
116
|
-
speculateBegin: (nova) => nova?.speculateBegin?.(),
|
|
117
|
-
speculateCommit: (nova) => nova?.speculateCommit?.(),
|
|
118
|
-
speculateRollback: (nova) => nova?.speculateRollback?.(),
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
async function dispatchRpc(nova, method, args) {
|
|
122
|
-
const fn = RPC_METHODS[method];
|
|
123
|
-
if (!fn) throw new Error("unknown nova method: " + method);
|
|
124
|
-
return fn(nova, args);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function loadAvailable(nova) {
|
|
128
|
-
if (!isFunction(nova?.names)) return [];
|
|
129
|
-
try {
|
|
130
|
-
return await nova.names();
|
|
131
|
-
} catch {
|
|
132
|
-
return [];
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async function prepareRun(options, fail) {
|
|
137
|
-
const { code, config, signal, nova } = options;
|
|
138
|
-
if (!String(code || "").trim()) return { failed: fail("code must be a non-empty string") };
|
|
139
|
-
const maxCode = config.maxCodeChars ?? 48000;
|
|
140
|
-
if (code.length > maxCode) return { failed: fail("code exceeds " + maxCode + " characters") };
|
|
141
|
-
if (signal?.aborted) return { failed: fail(ABORT_MESSAGE) };
|
|
142
|
-
const handle = acquireWorker(config);
|
|
143
|
-
try {
|
|
144
|
-
await handle.ready;
|
|
145
|
-
} catch (err) {
|
|
146
|
-
return { failed: fail("guest worker failed to start: " + err?.message) };
|
|
147
|
-
}
|
|
148
|
-
const available = await loadAvailable(nova);
|
|
149
|
-
return { handle, worker: handle.worker, available };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function startWatchdogs({ timeoutMs, rssLimit, signal, onAbort, onMemoryExceeded }) {
|
|
153
|
-
const timer = setTimeout(onAbort, timeoutMs);
|
|
154
|
-
if (timer.unref) timer.unref();
|
|
155
|
-
const memTimer = setInterval(() => {
|
|
156
|
-
if (rssBytes() <= rssLimit) return;
|
|
157
|
-
onMemoryExceeded(rssBytes());
|
|
158
|
-
}, MEMORY_POLL_MS);
|
|
159
|
-
if (memTimer.unref) memTimer.unref();
|
|
160
|
-
if (signal) signal.addEventListener("abort", onAbort, { once: true });
|
|
161
|
-
return () => {
|
|
162
|
-
clearTimeout(timer);
|
|
163
|
-
clearInterval(memTimer);
|
|
164
|
-
if (signal) signal.removeEventListener("abort", onAbort);
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const MESSAGE_HANDLERS = {
|
|
169
|
-
log: (msg, ctx) => {
|
|
170
|
-
ctx.logs.push(msg.line);
|
|
171
|
-
},
|
|
172
|
-
rpc: (msg, ctx) => {
|
|
173
|
-
dispatchRpc(ctx.nova, msg.method, msg.args).then(
|
|
174
|
-
(value) => ctx.postResult({ op: "rpc:result", id: msg.id, ok: true, value }),
|
|
175
|
-
(err) => ctx.postResult({ op: "rpc:result", id: msg.id, ok: false, error: err instanceof Error ? err.message : String(err) }),
|
|
176
|
-
);
|
|
177
|
-
},
|
|
178
|
-
done: (msg, ctx) => {
|
|
179
|
-
const packaged = packageFinalReturn(msg.value, ctx.logs, ctx.config);
|
|
180
|
-
ctx.finish(
|
|
181
|
-
{
|
|
182
|
-
ok: true,
|
|
183
|
-
result: packaged.returnValue,
|
|
184
|
-
resultText: packaged.returnText,
|
|
185
|
-
returnTruncated: packaged.returnTruncated,
|
|
186
|
-
undefinedReturn: msg.undefinedReturn === true && msg.hasReturn === false,
|
|
187
|
-
logs: packaged.logs,
|
|
188
|
-
logTruncated: packaged.logTruncated,
|
|
189
|
-
wallMs: ctx.wall(),
|
|
190
|
-
},
|
|
191
|
-
true,
|
|
192
|
-
);
|
|
193
|
-
},
|
|
194
|
-
error: (msg, ctx) => {
|
|
195
|
-
const where = msg.location ? " (line " + msg.location.line + ":" + msg.location.col + ")" : "";
|
|
196
|
-
ctx.finish(ctx.fail(msg.message + where, ctx.logs), true);
|
|
118
|
+
return Array.isArray(wave) ? { results: [...wave], mode: wave.mode, reason: wave.reason } : wave;
|
|
197
119
|
},
|
|
120
|
+
search: (nova, args) => nova.search(args[0], args[1]),
|
|
121
|
+
describe: (nova, args) => nova.describe(args[0]),
|
|
122
|
+
speculateBegin: (nova) => nova.speculateBegin(),
|
|
123
|
+
speculateCommit: (nova) => nova.speculateCommit(),
|
|
124
|
+
speculateRollback: (nova) => nova.speculateRollback(),
|
|
198
125
|
};
|
|
199
126
|
|
|
200
|
-
export async function runGuestProgram(
|
|
201
|
-
const { code, nova, config, signal, onTimeout } = options;
|
|
127
|
+
export async function runGuestProgram({ code, nova = {}, config = {}, signal, onTimeout }) {
|
|
202
128
|
const started = performance.now();
|
|
203
129
|
const wall = () => Math.round(performance.now() - started);
|
|
204
|
-
const fail = (error, logs = []) => ({ ok: false, error, logs, wallMs: wall() });
|
|
205
|
-
const prepared = await prepareRun(options, fail);
|
|
206
|
-
if (prepared.failed) return prepared.failed;
|
|
207
|
-
const { handle, worker, available } = prepared;
|
|
208
130
|
const logs = [];
|
|
131
|
+
const fail = (error) => ({ ok: false, error, logs, logTruncated, wallMs: wall() });
|
|
132
|
+
let logTruncated = false;
|
|
133
|
+
if (!isString(code) || !code.trim()) return fail("code must be a non-empty string");
|
|
134
|
+
if (code.length > (config.maxCodeChars ?? 48000)) return fail("code exceeds " + (config.maxCodeChars ?? 48000) + " characters");
|
|
135
|
+
if (signal?.aborted) return fail(ABORT_MESSAGE);
|
|
209
136
|
const runId = ++runSeq;
|
|
210
|
-
const
|
|
211
|
-
const rssLimit = rssBytes() + maxHeapMb * MEMORY_SLACK * 1048576;
|
|
212
|
-
|
|
137
|
+
const timeoutMs = config.timeoutMs ?? 60000;
|
|
138
|
+
const rssLimit = rssBytes() + (config.maxHeapMb ?? 512) * MEMORY_SLACK * 1048576;
|
|
139
|
+
|
|
140
|
+
return new Promise((resolve) => {
|
|
141
|
+
let handle;
|
|
213
142
|
let finished = false;
|
|
214
|
-
let
|
|
215
|
-
|
|
143
|
+
let accepting = true;
|
|
144
|
+
let completing = false;
|
|
145
|
+
let hostError;
|
|
146
|
+
let notifyingHost = false;
|
|
147
|
+
const pending = new Set();
|
|
148
|
+
const cleanup = () => {
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
clearInterval(memTimer);
|
|
151
|
+
signal?.removeEventListener("abort", signalAbort);
|
|
152
|
+
handle?.worker.off("message", onMessage);
|
|
153
|
+
handle?.worker.off("error", onError);
|
|
154
|
+
handle?.worker.off("exit", onExit);
|
|
155
|
+
};
|
|
156
|
+
const finish = (outcome) => {
|
|
216
157
|
if (finished) return;
|
|
217
158
|
finished = true;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (keepWorker) releaseWorker(handle);
|
|
223
|
-
else killWorker(handle);
|
|
224
|
-
resolve(outcome);
|
|
159
|
+
accepting = false;
|
|
160
|
+
cleanup();
|
|
161
|
+
void killWorker(handle);
|
|
162
|
+
resolve({ ...outcome, wallMs: wall() });
|
|
225
163
|
};
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
} catch {}
|
|
230
|
-
finish(fail(ABORT_MESSAGE, logs), false);
|
|
164
|
+
const cancelHost = () => {
|
|
165
|
+
notifyingHost = true;
|
|
166
|
+
try { nova.cancel?.(); } catch {} finally { notifyingHost = false; }
|
|
231
167
|
};
|
|
232
|
-
const
|
|
168
|
+
const abort = () => {
|
|
233
169
|
if (finished) return;
|
|
170
|
+
cancelHost();
|
|
171
|
+
try { onTimeout?.(); } catch {}
|
|
172
|
+
finish(fail(ABORT_MESSAGE));
|
|
173
|
+
};
|
|
174
|
+
const signalAbort = () => { if (!notifyingHost) abort(); };
|
|
175
|
+
const timer = setTimeout(abort, Math.min(timeoutMs, 2147483647));
|
|
176
|
+
const memTimer = setInterval(() => {
|
|
177
|
+
if (rssBytes() <= rssLimit) return;
|
|
178
|
+
cancelHost();
|
|
179
|
+
finish(fail("guest exceeded memory limit (maxHeapMb=" + (config.maxHeapMb ?? 512) + ")"));
|
|
180
|
+
}, MEMORY_POLL_MS);
|
|
181
|
+
signal?.addEventListener("abort", signalAbort, { once: true });
|
|
182
|
+
|
|
183
|
+
const postResult = (message) => {
|
|
184
|
+
if (!accepting || finished) return;
|
|
234
185
|
try {
|
|
235
|
-
worker.postMessage(
|
|
186
|
+
handle.worker.postMessage({ ...message, runId });
|
|
236
187
|
} catch (err) {
|
|
237
|
-
|
|
188
|
+
try {
|
|
189
|
+
handle.worker.postMessage({ op: "rpc:result", id: message.id, runId, ok: false, error: "result not transferable: " + err.message });
|
|
190
|
+
} catch (error) {
|
|
191
|
+
onError(error);
|
|
192
|
+
}
|
|
238
193
|
}
|
|
239
194
|
};
|
|
240
|
-
const
|
|
195
|
+
const complete = async (outcome) => {
|
|
196
|
+
if (finished || completing) return;
|
|
197
|
+
completing = true;
|
|
198
|
+
accepting = false;
|
|
199
|
+
// Stop timers and detached guest continuations before settling host work.
|
|
200
|
+
handle?.worker.off("error", onError);
|
|
201
|
+
handle?.worker.off("exit", onExit);
|
|
202
|
+
void killWorker(handle);
|
|
203
|
+
if (!outcome.ok) cancelHost();
|
|
204
|
+
await Promise.allSettled(pending);
|
|
205
|
+
if (finished) return;
|
|
206
|
+
finish(outcome.ok && hostError ? fail(hostError) : outcome);
|
|
207
|
+
};
|
|
208
|
+
const onError = (err) => { void complete(fail("guest crashed: " + err.message)); };
|
|
209
|
+
const onExit = (exitCode) => { void complete(fail("guest exited (code " + exitCode + ")")); };
|
|
241
210
|
const onMessage = (msg) => {
|
|
242
|
-
if (!isObject(msg)) return;
|
|
243
|
-
if (msg.
|
|
244
|
-
if (
|
|
245
|
-
|
|
211
|
+
if (finished || !accepting || !isObject(msg) || msg.runId !== runId) return;
|
|
212
|
+
if (msg.op === "log") {
|
|
213
|
+
if (logs.length < (config.maxLogLines ?? 100)) logs.push(msg.line);
|
|
214
|
+
else logTruncated = true;
|
|
215
|
+
logTruncated ||= msg.truncated === true;
|
|
216
|
+
} else if (msg.op === "logTruncated") {
|
|
217
|
+
logTruncated = true;
|
|
218
|
+
} else if (msg.op === "rpc") {
|
|
219
|
+
const method = Object.hasOwn(RPC_METHODS, msg.method) && RPC_METHODS[msg.method];
|
|
220
|
+
const work = Promise.resolve().then(() => {
|
|
221
|
+
if (!method) throw new Error("unknown nova method: " + msg.method);
|
|
222
|
+
return method(nova, msg.args);
|
|
223
|
+
});
|
|
224
|
+
pending.add(work);
|
|
225
|
+
work.then(
|
|
226
|
+
value => postResult({ op: "rpc:result", id: msg.id, ok: true, value }),
|
|
227
|
+
err => {
|
|
228
|
+
// An awaited, handled host error must not poison the whole program.
|
|
229
|
+
if (!accepting) hostError ??= err instanceof Error ? err.message : String(err);
|
|
230
|
+
postResult({ op: "rpc:result", id: msg.id, ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
231
|
+
},
|
|
232
|
+
).finally(() => pending.delete(work));
|
|
233
|
+
} else if (msg.op === "done") {
|
|
234
|
+
try {
|
|
235
|
+
const packed = packageFinalReturn(msg.value, logs, config);
|
|
236
|
+
void complete({ ok: true, result: packed.returnValue, resultText: packed.returnText,
|
|
237
|
+
returnTruncated: packed.returnTruncated, undefinedReturn: msg.undefinedReturn === true,
|
|
238
|
+
logs: packed.logs, logTruncated: logTruncated || packed.logTruncated });
|
|
239
|
+
} catch (err) {
|
|
240
|
+
void complete(fail(err.message));
|
|
241
|
+
}
|
|
242
|
+
} else if (msg.op === "error") {
|
|
243
|
+
const where = msg.location ? " (line " + msg.location.line + ":" + msg.location.col + ")" : "";
|
|
244
|
+
void complete(fail(msg.message + where));
|
|
246
245
|
}
|
|
247
|
-
const handler = MESSAGE_HANDLERS[msg.op];
|
|
248
|
-
if (handler) handler(msg, ctx);
|
|
249
246
|
};
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
limits: { maxLogLines, maxLogLineChars },
|
|
273
|
-
available,
|
|
274
|
-
});
|
|
247
|
+
|
|
248
|
+
void (async () => {
|
|
249
|
+
try {
|
|
250
|
+
if (signal?.aborted) return abort();
|
|
251
|
+
handle = acquireWorker(config);
|
|
252
|
+
await handle.ready;
|
|
253
|
+
if (finished || signal?.aborted) return abort();
|
|
254
|
+
const available = isFunction(nova.names) ? await nova.names() : [];
|
|
255
|
+
if (finished || signal?.aborted) return abort();
|
|
256
|
+
handle.worker.on("message", onMessage);
|
|
257
|
+
handle.worker.on("error", onError);
|
|
258
|
+
handle.worker.on("exit", onExit);
|
|
259
|
+
const prepared = prepareProgram(code);
|
|
260
|
+
if (wall() >= timeoutMs) return abort();
|
|
261
|
+
handle.worker.postMessage({ op: "run", runId, prepared, available,
|
|
262
|
+
batchRead: nova.batchRead !== false,
|
|
263
|
+
limits: { maxLogLines: config.maxLogLines ?? 100, maxLogLineChars: config.maxLogLineChars ?? 4096 } });
|
|
264
|
+
} catch (err) {
|
|
265
|
+
cancelHost();
|
|
266
|
+
finish(fail("guest worker failed to start: " + err.message));
|
|
267
|
+
}
|
|
268
|
+
})();
|
|
275
269
|
});
|
|
276
270
|
}
|