@oh-my-pi/pi-coding-agent 14.9.5 → 14.9.7
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 +52 -0
- package/package.json +7 -7
- package/src/cli/setup-cli.ts +14 -161
- package/src/cli/stats-cli.ts +56 -2
- package/src/cli.ts +0 -1
- package/src/config/settings-schema.ts +0 -10
- package/src/eval/eval.lark +30 -10
- package/src/eval/js/context-manager.ts +334 -564
- package/src/eval/js/shared/helpers.ts +237 -0
- package/src/eval/js/shared/indirect-eval.ts +30 -0
- package/src/eval/js/shared/rewrite-imports.ts +211 -0
- package/src/eval/js/shared/runtime.ts +168 -0
- package/src/eval/js/shared/types.ts +18 -0
- package/src/eval/js/tool-bridge.ts +2 -4
- package/src/eval/js/worker-core.ts +146 -0
- package/src/eval/js/worker-entry.ts +24 -0
- package/src/eval/js/worker-protocol.ts +41 -0
- package/src/eval/parse.ts +218 -49
- package/src/eval/py/display.ts +71 -0
- package/src/eval/py/executor.ts +74 -89
- package/src/eval/py/index.ts +1 -2
- package/src/eval/py/kernel.ts +472 -900
- package/src/eval/py/prelude.py +95 -7
- package/src/eval/py/runner.py +879 -0
- package/src/eval/py/runtime.ts +3 -16
- package/src/eval/py/tool-bridge.ts +137 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +93 -5
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/modes/controllers/command-controller.ts +0 -23
- package/src/prompts/tools/eval.md +14 -27
- package/src/session/agent-session.ts +0 -1
- package/src/session/history-storage.ts +77 -19
- package/src/tools/browser/tab-protocol.ts +4 -0
- package/src/tools/browser/tab-supervisor.ts +86 -5
- package/src/tools/browser/tab-worker.ts +104 -58
- package/src/tools/eval.ts +1 -1
- package/src/web/search/index.ts +6 -4
- package/src/cli/jupyter-cli.ts +0 -106
- package/src/commands/jupyter.ts +0 -32
- package/src/eval/py/cancellation.ts +0 -28
- package/src/eval/py/gateway-coordinator.ts +0 -424
- /package/src/eval/js/{prelude.ts → shared/prelude.ts} +0 -0
- /package/src/eval/js/{prelude.txt → shared/prelude.txt} +0 -0
|
@@ -1,21 +1,25 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { pathToFileURL } from "node:url";
|
|
5
|
-
import * as util from "node:util";
|
|
6
|
-
import * as vm from "node:vm";
|
|
7
|
-
|
|
8
|
-
import { parse as babelParse } from "@babel/parser";
|
|
9
|
-
import * as Diff from "diff";
|
|
1
|
+
import { logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
10
2
|
import type { ToolSession } from "../../tools";
|
|
11
|
-
import { ToolError } from "../../tools/tool-errors";
|
|
12
|
-
import { JAVASCRIPT_PRELUDE_SOURCE } from "./prelude";
|
|
3
|
+
import { ToolAbortError, ToolError } from "../../tools/tool-errors";
|
|
13
4
|
import { callSessionTool, type JsStatusEvent } from "./tool-bridge";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
5
|
+
import { WorkerCore } from "./worker-core";
|
|
6
|
+
// Imported with `type: "file"` so Bun's bundler statically discovers the worker entry and
|
|
7
|
+
// embeds it inside `bun build --compile` single-file binaries. Mirrors the browser tab
|
|
8
|
+
// worker setup; see packages/coding-agent/src/tools/browser/tab-supervisor.ts for the
|
|
9
|
+
// rationale.
|
|
10
|
+
// @ts-expect-error -- Bun file-URL import attribute is not modeled by tsgo.
|
|
11
|
+
import jsWorkerEntryUrl from "./worker-entry.ts" with { type: "file" };
|
|
12
|
+
import type {
|
|
13
|
+
JsDisplayOutput,
|
|
14
|
+
RunErrorPayload,
|
|
15
|
+
SessionSnapshot,
|
|
16
|
+
Transport,
|
|
17
|
+
WorkerInbound,
|
|
18
|
+
WorkerOutbound,
|
|
19
|
+
} from "./worker-protocol";
|
|
20
|
+
|
|
21
|
+
export { rewriteStaticImports } from "./shared/rewrite-imports";
|
|
22
|
+
export type { JsDisplayOutput } from "./worker-protocol";
|
|
19
23
|
|
|
20
24
|
export interface VmRunState {
|
|
21
25
|
signal?: AbortSignal;
|
|
@@ -23,617 +27,383 @@ export interface VmRunState {
|
|
|
23
27
|
onDisplay?: (output: JsDisplayOutput) => void;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
interface
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
interface WorkerHandle {
|
|
31
|
+
mode: "worker" | "inline";
|
|
32
|
+
send(msg: WorkerInbound): void;
|
|
33
|
+
onMessage(handler: (msg: WorkerOutbound) => void): () => void;
|
|
34
|
+
terminate(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface PendingRun {
|
|
38
|
+
runId: string;
|
|
39
|
+
runState: VmRunState;
|
|
40
|
+
toolSession: ToolSession;
|
|
41
|
+
resolve(value: { value: unknown }): void;
|
|
42
|
+
reject(error: Error): void;
|
|
43
|
+
toolCalls: Map<string, AbortController>;
|
|
44
|
+
settled: boolean;
|
|
35
45
|
}
|
|
36
46
|
|
|
37
|
-
interface
|
|
47
|
+
interface JsSession {
|
|
38
48
|
sessionKey: string;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
context: vm.Context;
|
|
43
|
-
env: Map<string, string>;
|
|
44
|
-
timers: Set<NodeJS.Timeout>;
|
|
45
|
-
intervals: Set<NodeJS.Timeout>;
|
|
46
|
-
currentRun?: VmRunState;
|
|
49
|
+
worker: WorkerHandle;
|
|
50
|
+
state: "alive" | "dead";
|
|
51
|
+
pending: Map<string, PendingRun>;
|
|
47
52
|
queue: Promise<void>;
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
const
|
|
51
|
-
const
|
|
55
|
+
const sessions = new Map<string, JsSession>();
|
|
56
|
+
const READY_TIMEOUT_MS = 5_000;
|
|
52
57
|
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
export async function executeInVmContext(options: {
|
|
59
|
+
sessionKey: string;
|
|
60
|
+
sessionId: string;
|
|
61
|
+
cwd: string;
|
|
62
|
+
session: ToolSession;
|
|
63
|
+
reset?: boolean;
|
|
64
|
+
code: string;
|
|
65
|
+
filename: string;
|
|
66
|
+
timeoutMs?: number;
|
|
67
|
+
runState: VmRunState;
|
|
68
|
+
}): Promise<{ value: unknown }> {
|
|
69
|
+
if (options.reset) {
|
|
70
|
+
await resetVmContext(options.sessionKey);
|
|
62
71
|
}
|
|
63
|
-
|
|
72
|
+
const session = await acquireSession(options.sessionKey, {
|
|
73
|
+
cwd: options.cwd,
|
|
74
|
+
sessionId: options.sessionId,
|
|
75
|
+
});
|
|
76
|
+
return await runQueued(session, () => runOnce(session, options));
|
|
64
77
|
}
|
|
65
78
|
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
79
|
+
export async function resetVmContext(sessionKey: string): Promise<void> {
|
|
80
|
+
const session = sessions.get(sessionKey);
|
|
81
|
+
if (!session) return;
|
|
82
|
+
sessions.delete(sessionKey);
|
|
83
|
+
await killSession(session, new ToolError("JS context reset"));
|
|
71
84
|
}
|
|
72
85
|
|
|
73
|
-
async function
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const filePath = resolvePath(state, rawPath);
|
|
78
|
-
const file = Bun.file(filePath);
|
|
79
|
-
const info = await file.stat().catch(() => undefined);
|
|
80
|
-
if (!info) {
|
|
81
|
-
throw new ToolError(`File not found: ${filePath}`);
|
|
82
|
-
}
|
|
83
|
-
if (info.isDirectory()) {
|
|
84
|
-
throw new ToolError(`Directory paths are not supported by this helper: ${filePath}`);
|
|
85
|
-
}
|
|
86
|
-
return { filePath, file, size: info.size };
|
|
86
|
+
export async function disposeAllVmContexts(): Promise<void> {
|
|
87
|
+
const all = [...sessions.values()];
|
|
88
|
+
sessions.clear();
|
|
89
|
+
await Promise.all(all.map(session => killSession(session, new ToolError("JS context disposed"))));
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
function
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return data.byteLength;
|
|
92
|
+
async function runQueued<T>(session: JsSession, work: () => Promise<T>): Promise<T> {
|
|
93
|
+
const previous = session.queue;
|
|
94
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
95
|
+
session.queue = promise;
|
|
96
|
+
try {
|
|
97
|
+
await previous;
|
|
98
|
+
} catch {
|
|
99
|
+
// Previous run's failure must not poison this one.
|
|
98
100
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return (
|
|
104
|
-
typeof value === "string" || value instanceof Blob || value instanceof ArrayBuffer || ArrayBuffer.isView(value)
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function emitText(state: VmContextState, text: string): void {
|
|
109
|
-
if (!text) return;
|
|
110
|
-
state.currentRun?.onText?.(text.endsWith("\n") ? text : `${text}\n`);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function emitStatus(state: VmContextState, event: JsStatusEvent): void {
|
|
114
|
-
state.currentRun?.onDisplay?.({ type: "status", event });
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function displayValue(state: VmContextState, value: unknown): void {
|
|
118
|
-
if (value === undefined) return;
|
|
119
|
-
if (value && typeof value === "object") {
|
|
120
|
-
const record = value as Record<string, unknown>;
|
|
121
|
-
if (record.type === "image" && typeof record.data === "string" && typeof record.mimeType === "string") {
|
|
122
|
-
state.currentRun?.onDisplay?.({
|
|
123
|
-
type: "image",
|
|
124
|
-
data: record.data,
|
|
125
|
-
mimeType: record.mimeType,
|
|
126
|
-
});
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
state.currentRun?.onDisplay?.({
|
|
130
|
-
type: "json",
|
|
131
|
-
data: structuredClone(value),
|
|
132
|
-
});
|
|
133
|
-
return;
|
|
101
|
+
try {
|
|
102
|
+
return await work();
|
|
103
|
+
} finally {
|
|
104
|
+
resolve();
|
|
134
105
|
}
|
|
135
|
-
emitText(state, String(value));
|
|
136
106
|
}
|
|
137
107
|
|
|
138
|
-
function
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
108
|
+
async function runOnce(
|
|
109
|
+
session: JsSession,
|
|
110
|
+
options: {
|
|
111
|
+
sessionId: string;
|
|
112
|
+
cwd: string;
|
|
113
|
+
session: ToolSession;
|
|
114
|
+
code: string;
|
|
115
|
+
filename: string;
|
|
116
|
+
runState: VmRunState;
|
|
117
|
+
},
|
|
118
|
+
): Promise<{ value: unknown }> {
|
|
119
|
+
const runId = `r-${Snowflake.next()}`;
|
|
120
|
+
const { promise, resolve, reject } = Promise.withResolvers<{ value: unknown }>();
|
|
121
|
+
const pending: PendingRun = {
|
|
122
|
+
runId,
|
|
123
|
+
runState: options.runState,
|
|
124
|
+
toolSession: options.session,
|
|
125
|
+
resolve,
|
|
126
|
+
reject,
|
|
127
|
+
toolCalls: new Map(),
|
|
128
|
+
settled: false,
|
|
129
|
+
};
|
|
130
|
+
session.pending.set(runId, pending);
|
|
131
|
+
|
|
132
|
+
const onAbort = (): void => {
|
|
133
|
+
const reason = options.runState.signal?.reason;
|
|
134
|
+
const abortError = reasonToError(reason, "Execution aborted");
|
|
135
|
+
// Cancel any in-flight tool calls first.
|
|
136
|
+
for (const ctrl of pending.toolCalls.values()) ctrl.abort(abortError);
|
|
137
|
+
// Hard-kill the worker — only way to interrupt synchronous user code.
|
|
138
|
+
void killSessionFor(session, abortError);
|
|
154
139
|
};
|
|
155
|
-
}
|
|
156
140
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
state.intervals.delete(timer);
|
|
162
|
-
return;
|
|
141
|
+
if (options.runState.signal?.aborted) {
|
|
142
|
+
queueMicrotask(onAbort);
|
|
143
|
+
} else {
|
|
144
|
+
options.runState.signal?.addEventListener("abort", onAbort, { once: true });
|
|
163
145
|
}
|
|
164
|
-
clearTimeout(timer);
|
|
165
|
-
state.timers.delete(timer);
|
|
166
|
-
}
|
|
167
146
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
emitStatus(state, { op: "read", path: filePath, bytes: size, chars: text.length });
|
|
182
|
-
return text;
|
|
183
|
-
},
|
|
184
|
-
writeFile: async (rawPath: string, data: unknown): Promise<string> => {
|
|
185
|
-
if (!isWriteData(data)) {
|
|
186
|
-
throw new ToolError("write() expects string, Blob, ArrayBuffer, or TypedArray data");
|
|
187
|
-
}
|
|
188
|
-
const filePath = resolvePath(state, rawPath);
|
|
189
|
-
if (typeof data === "string" || data instanceof Blob || data instanceof ArrayBuffer) {
|
|
190
|
-
await Bun.write(filePath, data);
|
|
191
|
-
} else {
|
|
192
|
-
await Bun.write(filePath, new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
193
|
-
}
|
|
194
|
-
emitStatus(state, { op: "write", path: filePath, bytes: getDataSize(data) });
|
|
195
|
-
return filePath;
|
|
196
|
-
},
|
|
197
|
-
append: async (rawPath: string, content: string): Promise<string> => {
|
|
198
|
-
const target = resolvePath(state, rawPath);
|
|
199
|
-
await Bun.write(
|
|
200
|
-
target,
|
|
201
|
-
`${await Bun.file(target)
|
|
202
|
-
.text()
|
|
203
|
-
.catch(() => "")}${content}`,
|
|
204
|
-
);
|
|
205
|
-
emitStatus(state, {
|
|
206
|
-
op: "append",
|
|
207
|
-
path: target,
|
|
208
|
-
chars: content.length,
|
|
209
|
-
bytes: utf8Encoder.encode(content).byteLength,
|
|
210
|
-
});
|
|
211
|
-
return target;
|
|
212
|
-
},
|
|
213
|
-
sortText: (text: string, options: VmHelperOptions = {}): string => {
|
|
214
|
-
const lines = String(text).split(/\r?\n/);
|
|
215
|
-
const deduped = options.unique ? Array.from(new Set(lines)) : lines;
|
|
216
|
-
const sorted = deduped.sort((a, b) => a.localeCompare(b));
|
|
217
|
-
if (options.reverse) {
|
|
218
|
-
sorted.reverse();
|
|
219
|
-
}
|
|
220
|
-
const result = sorted.join("\n");
|
|
221
|
-
emitStatus(state, {
|
|
222
|
-
op: "sort",
|
|
223
|
-
lines: sorted.length,
|
|
224
|
-
reverse: options.reverse === true,
|
|
225
|
-
unique: options.unique === true,
|
|
226
|
-
});
|
|
227
|
-
return result;
|
|
228
|
-
},
|
|
229
|
-
uniqText: (text: string, options: VmHelperOptions = {}): string | Array<[number, string]> => {
|
|
230
|
-
const lines = String(text)
|
|
231
|
-
.split(/\r?\n/)
|
|
232
|
-
.filter(line => line.length > 0);
|
|
233
|
-
const groups: Array<[number, string]> = [];
|
|
234
|
-
for (const line of lines) {
|
|
235
|
-
const last = groups.at(-1);
|
|
236
|
-
if (last && last[1] === line) {
|
|
237
|
-
last[0] += 1;
|
|
238
|
-
continue;
|
|
239
|
-
}
|
|
240
|
-
groups.push([1, line]);
|
|
241
|
-
}
|
|
242
|
-
emitStatus(state, { op: "uniq", groups: groups.length, count_mode: options.count === true });
|
|
243
|
-
if (options.count) {
|
|
244
|
-
return groups;
|
|
245
|
-
}
|
|
246
|
-
return groups.map(([, line]) => line).join("\n");
|
|
247
|
-
},
|
|
248
|
-
counter: (items: string | string[], options: VmHelperOptions = {}): Array<[number, string]> => {
|
|
249
|
-
const values = Array.isArray(items) ? items : String(items).split(/\r?\n/).filter(Boolean);
|
|
250
|
-
const counts = new Map<string, number>();
|
|
251
|
-
for (const item of values) {
|
|
252
|
-
counts.set(item, (counts.get(item) ?? 0) + 1);
|
|
253
|
-
}
|
|
254
|
-
const entries = Array.from(counts.entries())
|
|
255
|
-
.map(([item, count]) => [count, item] as [number, string])
|
|
256
|
-
.sort((a, b) => (options.reverse === false ? a[0] - b[0] : b[0] - a[0]) || a[1].localeCompare(b[1]));
|
|
257
|
-
const limited = entries.slice(0, options.limit ?? entries.length);
|
|
258
|
-
emitStatus(state, { op: "counter", unique: counts.size, total: values.length, top: limited.slice(0, 10) });
|
|
259
|
-
return limited;
|
|
260
|
-
},
|
|
261
|
-
diff: async (rawA: string, rawB: string): Promise<string> => {
|
|
262
|
-
const fileA = resolvePath(state, rawA);
|
|
263
|
-
const fileB = resolvePath(state, rawB);
|
|
264
|
-
const [a, b] = await Promise.all([Bun.file(fileA).text(), Bun.file(fileB).text()]);
|
|
265
|
-
const result = Diff.createTwoFilesPatch(fileA, fileB, a, b, "", "", { context: 3 });
|
|
266
|
-
emitStatus(state, {
|
|
267
|
-
op: "diff",
|
|
268
|
-
file_a: fileA,
|
|
269
|
-
file_b: fileB,
|
|
270
|
-
identical: a === b,
|
|
271
|
-
preview: result.slice(0, 500),
|
|
272
|
-
});
|
|
273
|
-
return result;
|
|
274
|
-
},
|
|
275
|
-
tree: async (searchPath = ".", options: VmHelperOptions = {}): Promise<string> => {
|
|
276
|
-
const root = resolvePath(state, searchPath);
|
|
277
|
-
const maxDepth = options.maxDepth ?? 3;
|
|
278
|
-
const showHidden = options.hidden ?? false;
|
|
279
|
-
const lines: string[] = [`${root}/`];
|
|
280
|
-
let entryCount = 0;
|
|
281
|
-
const walk = async (dir: string, prefix: string, depth: number): Promise<void> => {
|
|
282
|
-
if (depth > maxDepth) return;
|
|
283
|
-
const entries = (await fs.promises.readdir(dir, { withFileTypes: true }))
|
|
284
|
-
.filter(entry => showHidden || !entry.name.startsWith("."))
|
|
285
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
286
|
-
for (let index = 0; index < entries.length; index++) {
|
|
287
|
-
const entry = entries[index];
|
|
288
|
-
const isLast = index === entries.length - 1;
|
|
289
|
-
const connector = isLast ? "└── " : "├── ";
|
|
290
|
-
const suffix = entry.isDirectory() ? "/" : "";
|
|
291
|
-
lines.push(`${prefix}${connector}${entry.name}${suffix}`);
|
|
292
|
-
entryCount += 1;
|
|
293
|
-
if (entry.isDirectory()) {
|
|
294
|
-
await walk(path.join(dir, entry.name), `${prefix}${isLast ? " " : "│ "}`, depth + 1);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
};
|
|
298
|
-
await walk(root, "", 1);
|
|
299
|
-
const result = lines.join("\n");
|
|
300
|
-
emitStatus(state, { op: "tree", path: root, entries: entryCount, preview: result.slice(0, 1000) });
|
|
301
|
-
return result;
|
|
302
|
-
},
|
|
303
|
-
env: (key?: string, value?: string): string | Record<string, string> | undefined => {
|
|
304
|
-
if (!key) {
|
|
305
|
-
const env = Object.fromEntries(Object.entries(getMergedEnv(state)).sort(([a], [b]) => a.localeCompare(b)));
|
|
306
|
-
emitStatus(state, { op: "env", count: Object.keys(env).length, keys: Object.keys(env).slice(0, 20) });
|
|
307
|
-
return env;
|
|
308
|
-
}
|
|
309
|
-
if (value !== undefined) {
|
|
310
|
-
state.env.set(key, value);
|
|
311
|
-
emitStatus(state, { op: "env", key, value, action: "set" });
|
|
312
|
-
return value;
|
|
313
|
-
}
|
|
314
|
-
const result = state.env.get(key) ?? Bun.env[key];
|
|
315
|
-
emitStatus(state, { op: "env", key, value: result, action: "get" });
|
|
316
|
-
return result;
|
|
317
|
-
},
|
|
318
|
-
};
|
|
147
|
+
try {
|
|
148
|
+
session.worker.send({
|
|
149
|
+
type: "run",
|
|
150
|
+
runId,
|
|
151
|
+
code: options.code,
|
|
152
|
+
filename: options.filename,
|
|
153
|
+
snapshot: { cwd: options.cwd, sessionId: options.sessionId },
|
|
154
|
+
});
|
|
155
|
+
return await promise;
|
|
156
|
+
} finally {
|
|
157
|
+
options.runState.signal?.removeEventListener("abort", onAbort);
|
|
158
|
+
session.pending.delete(runId);
|
|
159
|
+
}
|
|
319
160
|
}
|
|
320
161
|
|
|
321
|
-
function
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
cwd: () => cwd,
|
|
325
|
-
platform: process.platform,
|
|
326
|
-
release: Object.freeze({ ...process.release }),
|
|
327
|
-
version: process.version,
|
|
328
|
-
versions: Object.freeze({ ...process.versions }),
|
|
329
|
-
});
|
|
330
|
-
}
|
|
162
|
+
async function acquireSession(sessionKey: string, snapshot: SessionSnapshot): Promise<JsSession> {
|
|
163
|
+
const existing = sessions.get(sessionKey);
|
|
164
|
+
if (existing && existing.state === "alive") return existing;
|
|
331
165
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
sessionId: string,
|
|
335
|
-
cwd: string,
|
|
336
|
-
session: ToolSession,
|
|
337
|
-
): Promise<VmContextState> {
|
|
338
|
-
const state: VmContextState = {
|
|
166
|
+
const worker = await spawnJsWorker();
|
|
167
|
+
const session: JsSession = {
|
|
339
168
|
sessionKey,
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
context: {} as vm.Context,
|
|
344
|
-
env: new Map(),
|
|
345
|
-
timers: new Set(),
|
|
346
|
-
intervals: new Set(),
|
|
169
|
+
worker,
|
|
170
|
+
state: "alive",
|
|
171
|
+
pending: new Map(),
|
|
347
172
|
queue: Promise.resolve(),
|
|
348
173
|
};
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
emitText(state, `${prefix}${formatConsoleArgs(args)}`);
|
|
364
|
-
},
|
|
365
|
-
__omp_display__: (value: unknown) => displayValue(state, value),
|
|
366
|
-
setTimeout: createTrackedTimeout(state, false),
|
|
367
|
-
setInterval: createTrackedTimeout(state, true),
|
|
368
|
-
clearTimeout: (timer?: NodeJS.Timeout) => clearTrackedTimeout(state, false, timer),
|
|
369
|
-
clearInterval: (timer?: NodeJS.Timeout) => clearTrackedTimeout(state, true, timer),
|
|
370
|
-
queueMicrotask,
|
|
371
|
-
URL,
|
|
372
|
-
URLSearchParams,
|
|
373
|
-
TextEncoder,
|
|
374
|
-
TextDecoder,
|
|
375
|
-
AbortController,
|
|
376
|
-
AbortSignal,
|
|
377
|
-
structuredClone,
|
|
378
|
-
crypto,
|
|
379
|
-
webcrypto: crypto,
|
|
380
|
-
performance,
|
|
381
|
-
atob,
|
|
382
|
-
btoa,
|
|
383
|
-
Buffer,
|
|
384
|
-
Bun,
|
|
385
|
-
process: createProcessSubset(cwd),
|
|
386
|
-
require: buildRequire(cwd),
|
|
387
|
-
createRequire,
|
|
388
|
-
fs,
|
|
389
|
-
fetch,
|
|
390
|
-
Blob,
|
|
391
|
-
File,
|
|
392
|
-
Headers,
|
|
393
|
-
Request,
|
|
394
|
-
Response,
|
|
395
|
-
globalThis: undefined,
|
|
396
|
-
};
|
|
397
|
-
const context = vm.createContext(contextGlobals);
|
|
398
|
-
context.globalThis = context;
|
|
399
|
-
state.context = context;
|
|
400
|
-
vm.runInContext(JAVASCRIPT_PRELUDE_SOURCE, context, {
|
|
401
|
-
filename: "js-prelude.js",
|
|
402
|
-
importModuleDynamically: vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER,
|
|
174
|
+
const { promise: readyPromise, resolve: resolveReady, reject: rejectReady } = Promise.withResolvers<void>();
|
|
175
|
+
let resolved = false;
|
|
176
|
+
const unsubscribe = worker.onMessage(msg => {
|
|
177
|
+
if (!resolved && msg.type === "ready") {
|
|
178
|
+
resolved = true;
|
|
179
|
+
resolveReady();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (!resolved && msg.type === "init-failed") {
|
|
183
|
+
resolved = true;
|
|
184
|
+
rejectReady(errorFromPayload(msg.error));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
handleSessionMessage(session, msg);
|
|
403
188
|
});
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
cwd: string,
|
|
411
|
-
session: ToolSession,
|
|
412
|
-
): Promise<VmContextState> {
|
|
413
|
-
const existing = vmContexts.get(sessionKey);
|
|
414
|
-
if (existing) {
|
|
415
|
-
existing.cwd = cwd;
|
|
416
|
-
existing.sessionId = sessionId;
|
|
417
|
-
existing.session = session;
|
|
418
|
-
return existing;
|
|
189
|
+
try {
|
|
190
|
+
await raceWithTimeout(readyPromise, READY_TIMEOUT_MS, "Timed out initializing JS eval worker");
|
|
191
|
+
} catch (error) {
|
|
192
|
+
unsubscribe();
|
|
193
|
+
await worker.terminate().catch(() => undefined);
|
|
194
|
+
throw error;
|
|
419
195
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
return
|
|
196
|
+
worker.send({ type: "init", snapshot });
|
|
197
|
+
sessions.set(sessionKey, session);
|
|
198
|
+
return session;
|
|
423
199
|
}
|
|
424
200
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
201
|
+
function handleSessionMessage(session: JsSession, msg: WorkerOutbound): void {
|
|
202
|
+
switch (msg.type) {
|
|
203
|
+
case "text": {
|
|
204
|
+
const pending = session.pending.get(msg.runId);
|
|
205
|
+
pending?.runState.onText?.(msg.chunk);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
case "display": {
|
|
209
|
+
const pending = session.pending.get(msg.runId);
|
|
210
|
+
pending?.runState.onDisplay?.(msg.output);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
case "tool-call":
|
|
214
|
+
void handleToolCall(session, msg);
|
|
215
|
+
return;
|
|
216
|
+
case "result":
|
|
217
|
+
settlePending(session, msg);
|
|
218
|
+
return;
|
|
219
|
+
case "log":
|
|
220
|
+
logWorkerMessage(msg);
|
|
221
|
+
return;
|
|
222
|
+
case "ready":
|
|
223
|
+
case "init-failed":
|
|
224
|
+
case "closed":
|
|
225
|
+
return;
|
|
432
226
|
}
|
|
433
|
-
state.intervals.clear();
|
|
434
|
-
state.currentRun = undefined;
|
|
435
227
|
}
|
|
436
228
|
|
|
437
|
-
async function
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
229
|
+
async function handleToolCall(session: JsSession, msg: Extract<WorkerOutbound, { type: "tool-call" }>): Promise<void> {
|
|
230
|
+
const pending = session.pending.get(msg.runId);
|
|
231
|
+
if (!pending) {
|
|
232
|
+
safeSend(session, {
|
|
233
|
+
type: "tool-reply",
|
|
234
|
+
id: msg.id,
|
|
235
|
+
reply: { ok: false, error: { message: "Run no longer active" } },
|
|
236
|
+
});
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const ctrl = new AbortController();
|
|
240
|
+
pending.toolCalls.set(msg.id, ctrl);
|
|
442
241
|
try {
|
|
443
|
-
|
|
242
|
+
const value = await callSessionTool(msg.name, msg.args, {
|
|
243
|
+
session: pending.toolSession,
|
|
244
|
+
signal: ctrl.signal,
|
|
245
|
+
emitStatus: (event: JsStatusEvent) => pending.runState.onDisplay?.({ type: "status", event }),
|
|
246
|
+
});
|
|
247
|
+
safeSend(session, { type: "tool-reply", id: msg.id, reply: { ok: true, value } });
|
|
248
|
+
} catch (error) {
|
|
249
|
+
safeSend(session, { type: "tool-reply", id: msg.id, reply: { ok: false, error: toErrorPayload(error) } });
|
|
444
250
|
} finally {
|
|
445
|
-
|
|
251
|
+
pending.toolCalls.delete(msg.id);
|
|
446
252
|
}
|
|
447
253
|
}
|
|
448
254
|
|
|
449
|
-
function
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
// string literals, template literals, or comments — common in codemods — stay intact.
|
|
459
|
-
|
|
460
|
-
type BabelImportDeclaration = {
|
|
461
|
-
type: "ImportDeclaration";
|
|
462
|
-
start: number;
|
|
463
|
-
end: number;
|
|
464
|
-
source: { value: string };
|
|
465
|
-
specifiers: ReadonlyArray<{
|
|
466
|
-
type: "ImportDefaultSpecifier" | "ImportNamespaceSpecifier" | "ImportSpecifier";
|
|
467
|
-
local: { name: string };
|
|
468
|
-
imported?: { type: "Identifier"; name: string } | { type: "StringLiteral"; value: string };
|
|
469
|
-
}>;
|
|
470
|
-
attributes?: ReadonlyArray<{
|
|
471
|
-
key: { type: "Identifier"; name: string } | { type: "StringLiteral"; value: string };
|
|
472
|
-
value: { value: string };
|
|
473
|
-
}>;
|
|
474
|
-
};
|
|
475
|
-
|
|
476
|
-
function buildDynamicImportCall(sourceLiteral: string, withClause: string | undefined): string {
|
|
477
|
-
return withClause ? `import(${sourceLiteral}, { with: ${withClause} })` : `import(${sourceLiteral})`;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
function buildWithClause(node: BabelImportDeclaration): string | undefined {
|
|
481
|
-
const attrs = node.attributes;
|
|
482
|
-
if (!attrs || attrs.length === 0) return undefined;
|
|
483
|
-
const pairs = attrs.map(attr => {
|
|
484
|
-
const key = attr.key.type === "Identifier" ? attr.key.name : JSON.stringify(attr.key.value);
|
|
485
|
-
return `${key}: ${JSON.stringify(attr.value.value)}`;
|
|
486
|
-
});
|
|
487
|
-
return `{ ${pairs.join(", ")} }`;
|
|
255
|
+
function settlePending(session: JsSession, msg: Extract<WorkerOutbound, { type: "result" }>): void {
|
|
256
|
+
const pending = session.pending.get(msg.runId);
|
|
257
|
+
if (!pending || pending.settled) return;
|
|
258
|
+
pending.settled = true;
|
|
259
|
+
if (msg.ok) {
|
|
260
|
+
pending.resolve({ value: undefined });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
pending.reject(errorFromPayload(msg.error));
|
|
488
264
|
}
|
|
489
265
|
|
|
490
|
-
function
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const importCall = buildDynamicImportCall(sourceLiteral, withClause);
|
|
494
|
-
|
|
495
|
-
let defaultName: string | undefined;
|
|
496
|
-
let namespaceName: string | undefined;
|
|
497
|
-
const namedPairs: Array<[string, string]> = [];
|
|
498
|
-
for (const spec of node.specifiers) {
|
|
499
|
-
if (spec.type === "ImportDefaultSpecifier") {
|
|
500
|
-
defaultName = spec.local.name;
|
|
501
|
-
} else if (spec.type === "ImportNamespaceSpecifier") {
|
|
502
|
-
namespaceName = spec.local.name;
|
|
503
|
-
} else if (spec.type === "ImportSpecifier" && spec.imported) {
|
|
504
|
-
const imported = spec.imported.type === "Identifier" ? spec.imported.name : spec.imported.value;
|
|
505
|
-
namedPairs.push([imported, spec.local.name]);
|
|
506
|
-
}
|
|
266
|
+
async function killSessionFor(session: JsSession, error: Error): Promise<void> {
|
|
267
|
+
if (sessions.get(session.sessionKey) === session) {
|
|
268
|
+
sessions.delete(session.sessionKey);
|
|
507
269
|
}
|
|
270
|
+
await killSession(session, error);
|
|
271
|
+
}
|
|
508
272
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
273
|
+
async function killSession(session: JsSession, error: Error): Promise<void> {
|
|
274
|
+
if (session.state === "dead") return;
|
|
275
|
+
session.state = "dead";
|
|
276
|
+
for (const pending of session.pending.values()) {
|
|
277
|
+
if (pending.settled) continue;
|
|
278
|
+
pending.settled = true;
|
|
279
|
+
for (const ctrl of pending.toolCalls.values()) ctrl.abort(error);
|
|
280
|
+
pending.reject(error);
|
|
513
281
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
}
|
|
517
|
-
if (namespaceName) return `const ${namespaceName} = await ${importCall};`;
|
|
518
|
-
if (defaultName) return `const ${defaultName} = (await ${importCall}).default;`;
|
|
519
|
-
return `await ${importCall};`;
|
|
282
|
+
session.pending.clear();
|
|
283
|
+
await session.worker.terminate().catch(() => undefined);
|
|
520
284
|
}
|
|
521
285
|
|
|
522
|
-
|
|
523
|
-
if (
|
|
524
|
-
|
|
525
|
-
let ast: { program: { body: ReadonlyArray<{ type: string }> } };
|
|
286
|
+
function safeSend(session: JsSession, msg: WorkerInbound): void {
|
|
287
|
+
if (session.state !== "alive") return;
|
|
526
288
|
try {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
allowReturnOutsideFunction: true,
|
|
531
|
-
allowImportExportEverywhere: true,
|
|
532
|
-
allowNewTargetOutsideFunction: true,
|
|
533
|
-
allowSuperOutsideMethod: true,
|
|
534
|
-
allowUndeclaredExports: true,
|
|
535
|
-
errorRecovery: true,
|
|
536
|
-
}) as unknown as typeof ast;
|
|
537
|
-
} catch {
|
|
538
|
-
// Parser bailed entirely — let the VM surface the real syntax error.
|
|
539
|
-
return code;
|
|
289
|
+
session.worker.send(msg);
|
|
290
|
+
} catch (err) {
|
|
291
|
+
logger.debug("js worker send failed", { error: err instanceof Error ? err.message : String(err) });
|
|
540
292
|
}
|
|
293
|
+
}
|
|
541
294
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
}
|
|
548
|
-
if (imports.length === 0) return code;
|
|
295
|
+
function reasonToError(reason: unknown, fallback: string): Error {
|
|
296
|
+
if (reason instanceof Error) return reason;
|
|
297
|
+
if (typeof reason === "string") return new ToolAbortError(reason);
|
|
298
|
+
return new ToolAbortError(fallback);
|
|
299
|
+
}
|
|
549
300
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
301
|
+
function errorFromPayload(payload: RunErrorPayload): Error {
|
|
302
|
+
if (payload.isAbort) {
|
|
303
|
+
const err = new ToolAbortError(payload.message || "Execution aborted");
|
|
304
|
+
if (payload.stack) err.stack = payload.stack;
|
|
305
|
+
return err;
|
|
555
306
|
}
|
|
556
|
-
|
|
307
|
+
const ctor = payload.isToolError ? ToolError : Error;
|
|
308
|
+
const error = new ctor(payload.message);
|
|
309
|
+
if (payload.name) error.name = payload.name;
|
|
310
|
+
if (payload.stack) error.stack = payload.stack;
|
|
311
|
+
return error;
|
|
557
312
|
}
|
|
558
313
|
|
|
559
|
-
function
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
314
|
+
function toErrorPayload(error: unknown): RunErrorPayload {
|
|
315
|
+
if (error instanceof Error) {
|
|
316
|
+
return {
|
|
317
|
+
name: error.name,
|
|
318
|
+
message: error.message,
|
|
319
|
+
stack: error.stack,
|
|
320
|
+
isAbort: error.name === "AbortError" || error.name === "ToolAbortError",
|
|
321
|
+
isToolError: error instanceof ToolError || error.name === "ToolError",
|
|
322
|
+
};
|
|
564
323
|
}
|
|
565
|
-
return {
|
|
566
|
-
source: `(async () => {\n${rewritten}\n})()`,
|
|
567
|
-
asyncWrapped: true,
|
|
568
|
-
};
|
|
324
|
+
return { message: String(error) };
|
|
569
325
|
}
|
|
570
326
|
|
|
571
|
-
|
|
572
|
-
if (
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
const { promise
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
327
|
+
function logWorkerMessage(msg: Extract<WorkerOutbound, { type: "log" }>): void {
|
|
328
|
+
if (msg.level === "debug") logger.debug(msg.msg, msg.meta);
|
|
329
|
+
else if (msg.level === "warn") logger.warn(msg.msg, msg.meta);
|
|
330
|
+
else logger.error(msg.msg, msg.meta);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function raceWithTimeout<T>(promise: Promise<T>, timeoutMs: number, reason: string): Promise<T> {
|
|
334
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
335
|
+
const { promise: timeoutPromise, reject } = Promise.withResolvers<never>();
|
|
336
|
+
const onAbort = (): void => reject(new ToolError(reason));
|
|
337
|
+
timeoutSignal.addEventListener("abort", onAbort, { once: true });
|
|
338
|
+
try {
|
|
339
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
340
|
+
} finally {
|
|
341
|
+
timeoutSignal.removeEventListener("abort", onAbort);
|
|
583
342
|
}
|
|
584
|
-
const onAbort = () => reject(signal.reason ?? new Error("Execution aborted"));
|
|
585
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
586
|
-
promised.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
587
|
-
return promise;
|
|
588
343
|
}
|
|
589
344
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
runState: VmRunState;
|
|
600
|
-
}): Promise<{ value: unknown }> {
|
|
601
|
-
if (options.reset) {
|
|
602
|
-
await resetVmContext(options.sessionKey);
|
|
345
|
+
async function spawnJsWorker(): Promise<WorkerHandle> {
|
|
346
|
+
try {
|
|
347
|
+
const worker = new Worker(jsWorkerEntryUrl, { type: "module" });
|
|
348
|
+
return wrapBunWorker(worker);
|
|
349
|
+
} catch (err) {
|
|
350
|
+
logger.warn("Bun Worker spawn failed; using inline JS eval worker (no sync-loop guard)", {
|
|
351
|
+
error: err instanceof Error ? err.message : String(err),
|
|
352
|
+
});
|
|
353
|
+
return spawnInlineWorker();
|
|
603
354
|
}
|
|
604
|
-
const state = await getOrCreateVmState(options.sessionKey, options.sessionId, options.cwd, options.session);
|
|
605
|
-
return runQueued(state, async () => {
|
|
606
|
-
state.currentRun = options.runState;
|
|
607
|
-
try {
|
|
608
|
-
if (options.runState.signal?.aborted) {
|
|
609
|
-
throw options.runState.signal.reason ?? new Error("Execution aborted");
|
|
610
|
-
}
|
|
611
|
-
const wrapped = wrapCode(options.code);
|
|
612
|
-
const value = vm.runInContext(wrapped.source, state.context, {
|
|
613
|
-
filename: options.filename,
|
|
614
|
-
timeout: options.timeoutMs,
|
|
615
|
-
importModuleDynamically: vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER,
|
|
616
|
-
});
|
|
617
|
-
const awaited = await awaitMaybePromise(value, options.runState.signal);
|
|
618
|
-
displayValue(state, awaited);
|
|
619
|
-
return { value: awaited };
|
|
620
|
-
} finally {
|
|
621
|
-
state.currentRun = undefined;
|
|
622
|
-
}
|
|
623
|
-
});
|
|
624
355
|
}
|
|
625
356
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
357
|
+
function wrapBunWorker(worker: Worker): WorkerHandle {
|
|
358
|
+
return {
|
|
359
|
+
mode: "worker",
|
|
360
|
+
send(msg) {
|
|
361
|
+
worker.postMessage(msg);
|
|
362
|
+
},
|
|
363
|
+
onMessage(handler) {
|
|
364
|
+
const wrap = (event: MessageEvent): void => handler(event.data as WorkerOutbound);
|
|
365
|
+
worker.addEventListener("message", wrap);
|
|
366
|
+
return () => worker.removeEventListener("message", wrap);
|
|
367
|
+
},
|
|
368
|
+
async terminate() {
|
|
369
|
+
worker.terminate();
|
|
370
|
+
},
|
|
371
|
+
};
|
|
631
372
|
}
|
|
632
373
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
374
|
+
/**
|
|
375
|
+
* Inline fallback for environments where Bun cannot spawn the worker entry
|
|
376
|
+
* (e.g. some test runners). Preserves behavior but cannot interrupt synchronous
|
|
377
|
+
* infinite loops because user code runs on the main thread.
|
|
378
|
+
*/
|
|
379
|
+
function spawnInlineWorker(): WorkerHandle {
|
|
380
|
+
const hostListeners = new Set<(message: WorkerOutbound) => void>();
|
|
381
|
+
const workerListeners = new Set<(message: WorkerInbound) => void>();
|
|
382
|
+
const workerTransport: Transport = {
|
|
383
|
+
send: msg =>
|
|
384
|
+
queueMicrotask(() => {
|
|
385
|
+
for (const listener of hostListeners) listener(msg);
|
|
386
|
+
}),
|
|
387
|
+
onMessage: handler => {
|
|
388
|
+
workerListeners.add(handler);
|
|
389
|
+
return () => workerListeners.delete(handler);
|
|
390
|
+
},
|
|
391
|
+
close: () => {},
|
|
392
|
+
};
|
|
393
|
+
new WorkerCore(workerTransport);
|
|
394
|
+
return {
|
|
395
|
+
mode: "inline",
|
|
396
|
+
send: msg =>
|
|
397
|
+
queueMicrotask(() => {
|
|
398
|
+
for (const listener of workerListeners) listener(msg);
|
|
399
|
+
}),
|
|
400
|
+
onMessage: handler => {
|
|
401
|
+
hostListeners.add(handler);
|
|
402
|
+
return () => hostListeners.delete(handler);
|
|
403
|
+
},
|
|
404
|
+
async terminate() {
|
|
405
|
+
hostListeners.clear();
|
|
406
|
+
workerListeners.clear();
|
|
407
|
+
},
|
|
408
|
+
};
|
|
639
409
|
}
|