@scelar/nodepod 1.0.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 (134) hide show
  1. package/LICENSE +43 -0
  2. package/README.md +240 -0
  3. package/dist/child_process-BJOMsZje.js +8233 -0
  4. package/dist/child_process-BJOMsZje.js.map +1 -0
  5. package/dist/child_process-Cj8vOcuc.cjs +7434 -0
  6. package/dist/child_process-Cj8vOcuc.cjs.map +1 -0
  7. package/dist/index-Cb1Cgdnd.js +35308 -0
  8. package/dist/index-Cb1Cgdnd.js.map +1 -0
  9. package/dist/index-DsMGS-xc.cjs +37195 -0
  10. package/dist/index-DsMGS-xc.cjs.map +1 -0
  11. package/dist/index.cjs +65 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.mjs +59 -0
  14. package/dist/index.mjs.map +1 -0
  15. package/package.json +95 -0
  16. package/src/__tests__/smoke.test.ts +11 -0
  17. package/src/constants/cdn-urls.ts +18 -0
  18. package/src/constants/config.ts +236 -0
  19. package/src/cross-origin.ts +26 -0
  20. package/src/engine-factory.ts +176 -0
  21. package/src/engine-types.ts +56 -0
  22. package/src/helpers/byte-encoding.ts +39 -0
  23. package/src/helpers/digest.ts +9 -0
  24. package/src/helpers/event-loop.ts +96 -0
  25. package/src/helpers/wasm-cache.ts +133 -0
  26. package/src/iframe-sandbox.ts +141 -0
  27. package/src/index.ts +192 -0
  28. package/src/isolation-helpers.ts +148 -0
  29. package/src/memory-volume.ts +941 -0
  30. package/src/module-transformer.ts +368 -0
  31. package/src/packages/archive-extractor.ts +248 -0
  32. package/src/packages/browser-bundler.ts +284 -0
  33. package/src/packages/installer.ts +396 -0
  34. package/src/packages/registry-client.ts +131 -0
  35. package/src/packages/version-resolver.ts +411 -0
  36. package/src/polyfills/assert.ts +384 -0
  37. package/src/polyfills/async_hooks.ts +144 -0
  38. package/src/polyfills/buffer.ts +628 -0
  39. package/src/polyfills/child_process.ts +2288 -0
  40. package/src/polyfills/chokidar.ts +336 -0
  41. package/src/polyfills/cluster.ts +106 -0
  42. package/src/polyfills/console.ts +136 -0
  43. package/src/polyfills/constants.ts +123 -0
  44. package/src/polyfills/crypto.ts +885 -0
  45. package/src/polyfills/dgram.ts +87 -0
  46. package/src/polyfills/diagnostics_channel.ts +76 -0
  47. package/src/polyfills/dns.ts +134 -0
  48. package/src/polyfills/domain.ts +68 -0
  49. package/src/polyfills/esbuild.ts +854 -0
  50. package/src/polyfills/events.ts +276 -0
  51. package/src/polyfills/fs.ts +2888 -0
  52. package/src/polyfills/fsevents.ts +79 -0
  53. package/src/polyfills/http.ts +1449 -0
  54. package/src/polyfills/http2.ts +199 -0
  55. package/src/polyfills/https.ts +76 -0
  56. package/src/polyfills/inspector.ts +62 -0
  57. package/src/polyfills/lightningcss.ts +105 -0
  58. package/src/polyfills/module.ts +191 -0
  59. package/src/polyfills/net.ts +353 -0
  60. package/src/polyfills/os.ts +238 -0
  61. package/src/polyfills/path.ts +206 -0
  62. package/src/polyfills/perf_hooks.ts +102 -0
  63. package/src/polyfills/process.ts +690 -0
  64. package/src/polyfills/punycode.ts +159 -0
  65. package/src/polyfills/querystring.ts +93 -0
  66. package/src/polyfills/quic.ts +118 -0
  67. package/src/polyfills/readdirp.ts +229 -0
  68. package/src/polyfills/readline.ts +692 -0
  69. package/src/polyfills/repl.ts +134 -0
  70. package/src/polyfills/rollup.ts +119 -0
  71. package/src/polyfills/sea.ts +33 -0
  72. package/src/polyfills/sqlite.ts +78 -0
  73. package/src/polyfills/stream.ts +1620 -0
  74. package/src/polyfills/string_decoder.ts +25 -0
  75. package/src/polyfills/tailwindcss-oxide.ts +309 -0
  76. package/src/polyfills/test.ts +197 -0
  77. package/src/polyfills/timers.ts +32 -0
  78. package/src/polyfills/tls.ts +105 -0
  79. package/src/polyfills/trace_events.ts +50 -0
  80. package/src/polyfills/tty.ts +71 -0
  81. package/src/polyfills/url.ts +174 -0
  82. package/src/polyfills/util.ts +559 -0
  83. package/src/polyfills/v8.ts +126 -0
  84. package/src/polyfills/vm.ts +132 -0
  85. package/src/polyfills/volume-registry.ts +15 -0
  86. package/src/polyfills/wasi.ts +44 -0
  87. package/src/polyfills/worker_threads.ts +326 -0
  88. package/src/polyfills/ws.ts +595 -0
  89. package/src/polyfills/zlib.ts +881 -0
  90. package/src/request-proxy.ts +716 -0
  91. package/src/script-engine.ts +3375 -0
  92. package/src/sdk/nodepod-fs.ts +93 -0
  93. package/src/sdk/nodepod-process.ts +86 -0
  94. package/src/sdk/nodepod-terminal.ts +350 -0
  95. package/src/sdk/nodepod.ts +509 -0
  96. package/src/sdk/types.ts +70 -0
  97. package/src/shell/commands/bun.ts +121 -0
  98. package/src/shell/commands/directory.ts +297 -0
  99. package/src/shell/commands/file-ops.ts +525 -0
  100. package/src/shell/commands/git.ts +2142 -0
  101. package/src/shell/commands/node.ts +80 -0
  102. package/src/shell/commands/npm.ts +198 -0
  103. package/src/shell/commands/pm-types.ts +45 -0
  104. package/src/shell/commands/pnpm.ts +82 -0
  105. package/src/shell/commands/search.ts +264 -0
  106. package/src/shell/commands/shell-env.ts +352 -0
  107. package/src/shell/commands/text-processing.ts +1152 -0
  108. package/src/shell/commands/yarn.ts +84 -0
  109. package/src/shell/shell-builtins.ts +19 -0
  110. package/src/shell/shell-helpers.ts +250 -0
  111. package/src/shell/shell-interpreter.ts +514 -0
  112. package/src/shell/shell-parser.ts +429 -0
  113. package/src/shell/shell-types.ts +85 -0
  114. package/src/syntax-transforms.ts +561 -0
  115. package/src/threading/engine-worker.ts +64 -0
  116. package/src/threading/inline-worker.ts +372 -0
  117. package/src/threading/offload-types.ts +112 -0
  118. package/src/threading/offload-worker.ts +383 -0
  119. package/src/threading/offload.ts +271 -0
  120. package/src/threading/process-context.ts +92 -0
  121. package/src/threading/process-handle.ts +275 -0
  122. package/src/threading/process-manager.ts +956 -0
  123. package/src/threading/process-worker-entry.ts +854 -0
  124. package/src/threading/shared-vfs.ts +352 -0
  125. package/src/threading/sync-channel.ts +135 -0
  126. package/src/threading/task-queue.ts +177 -0
  127. package/src/threading/vfs-bridge.ts +231 -0
  128. package/src/threading/worker-pool.ts +233 -0
  129. package/src/threading/worker-protocol.ts +358 -0
  130. package/src/threading/worker-vfs.ts +218 -0
  131. package/src/types/externals.d.ts +38 -0
  132. package/src/types/fs-streams.ts +142 -0
  133. package/src/types/manifest.ts +17 -0
  134. package/src/worker-sandbox.ts +90 -0
@@ -0,0 +1,956 @@
1
+ // ProcessManager — main-thread process lifecycle manager.
2
+ // Spawns Web Worker processes, routes I/O, handles VFS sync, tracks process tree.
3
+
4
+ import { EventEmitter } from "../polyfills/events";
5
+ import type { MemoryVolume } from "../memory-volume";
6
+ import { ProcessHandle } from "./process-handle";
7
+ import type {
8
+ SpawnConfig,
9
+ ProcessInfo,
10
+ MainToWorker_Init,
11
+ VFSBinarySnapshot,
12
+ WorkerToMain_SpawnRequest,
13
+ WorkerToMain_ForkRequest,
14
+ WorkerToMain_WorkerThreadRequest,
15
+ WorkerToMain_SpawnSync,
16
+ WorkerToMain_HttpResponse,
17
+ } from "./worker-protocol";
18
+ import type { VFSBridge } from "./vfs-bridge";
19
+ import { PROCESS_WORKER_BUNDLE } from "virtual:process-worker-bundle";
20
+ import { SLOT_SIZE } from "./sync-channel";
21
+
22
+ /* ------------------------------------------------------------------ */
23
+ /* Constants */
24
+ /* ------------------------------------------------------------------ */
25
+
26
+ const MAX_PROCESS_DEPTH = 10;
27
+ const MAX_PROCESSES = 50;
28
+
29
+ /* ------------------------------------------------------------------ */
30
+ /* ProcessManager */
31
+ /* ------------------------------------------------------------------ */
32
+
33
+ export class ProcessManager extends EventEmitter {
34
+ private _processes = new Map<number, ProcessHandle>();
35
+ private _nextPid = 100;
36
+ private _volume: MemoryVolume;
37
+ private _vfsBridge: VFSBridge | null = null;
38
+ private _sharedBuffer: SharedArrayBuffer | null = null;
39
+ private _syncBuffer: SharedArrayBuffer | null = null;
40
+
41
+ // port → owning PID
42
+ private _serverPorts = new Map<number, number>();
43
+ // parent PID → child PIDs (for exit deferral)
44
+ private _childPids = new Map<number, Set<number>>();
45
+ // pending HTTP request callbacks
46
+ private _httpCallbacks = new Map<number, (resp: WorkerToMain_HttpResponse) => void>();
47
+ private _nextHttpRequestId = 1;
48
+
49
+ constructor(volume: MemoryVolume) {
50
+ super();
51
+ this._volume = volume;
52
+ }
53
+
54
+ setVFSBridge(bridge: VFSBridge): void {
55
+ this._vfsBridge = bridge;
56
+ }
57
+
58
+ setSharedBuffer(buf: SharedArrayBuffer): void {
59
+ this._sharedBuffer = buf;
60
+ }
61
+
62
+ setSyncBuffer(buf: SharedArrayBuffer): void {
63
+ this._syncBuffer = buf;
64
+ }
65
+
66
+ spawn(config: {
67
+ command: string;
68
+ args?: string[];
69
+ cwd?: string;
70
+ env?: Record<string, string>;
71
+ parentPid?: number;
72
+ }): ProcessHandle {
73
+ if (this._processes.size >= MAX_PROCESSES) {
74
+ throw new Error(`Process limit exceeded (max ${MAX_PROCESSES})`);
75
+ }
76
+
77
+ if (config.parentPid !== undefined) {
78
+ let depth = 0;
79
+ let pid: number | undefined = config.parentPid;
80
+ while (pid !== undefined && depth < MAX_PROCESS_DEPTH) {
81
+ const parent = this._processes.get(pid);
82
+ pid = parent?.parentPid;
83
+ depth++;
84
+ }
85
+ if (depth >= MAX_PROCESS_DEPTH) {
86
+ throw new Error(`Process tree depth limit exceeded (max ${MAX_PROCESS_DEPTH})`);
87
+ }
88
+ }
89
+
90
+ const pid = this._nextPid++;
91
+
92
+ const snapshot = this._vfsBridge
93
+ ? this._vfsBridge.createSnapshot()
94
+ : this._createEmptySnapshot();
95
+
96
+ const spawnConfig: SpawnConfig = {
97
+ command: config.command,
98
+ args: config.args ?? [],
99
+ cwd: config.cwd ?? "/",
100
+ env: config.env ?? {},
101
+ snapshot,
102
+ sharedBuffer: this._sharedBuffer ?? undefined,
103
+ syncBuffer: this._syncBuffer ?? undefined,
104
+ parentPid: config.parentPid,
105
+ };
106
+
107
+ const worker = this._createWorker();
108
+ const handle = new ProcessHandle(worker, spawnConfig);
109
+ handle._setPid(pid);
110
+
111
+ this._processes.set(pid, handle);
112
+ this._wireHandleEvents(handle);
113
+
114
+ const initMsg: MainToWorker_Init = {
115
+ type: "init",
116
+ pid,
117
+ cwd: spawnConfig.cwd,
118
+ env: spawnConfig.env,
119
+ snapshot: spawnConfig.snapshot,
120
+ sharedBuffer: spawnConfig.sharedBuffer,
121
+ syncBuffer: spawnConfig.syncBuffer,
122
+ };
123
+ handle.init(initMsg);
124
+
125
+ this.emit("spawn", pid, config.command, config.args);
126
+ return handle;
127
+ }
128
+
129
+ getProcess(pid: number): ProcessHandle | undefined {
130
+ return this._processes.get(pid);
131
+ }
132
+
133
+ listProcesses(): ProcessInfo[] {
134
+ const result: ProcessInfo[] = [];
135
+ for (const [pid, handle] of this._processes) {
136
+ result.push({
137
+ pid,
138
+ command: handle.command,
139
+ args: handle.args,
140
+ state: handle.state,
141
+ exitCode: handle.exitCode,
142
+ parentPid: handle.parentPid,
143
+ });
144
+ }
145
+ return result;
146
+ }
147
+
148
+ // Kills process and ALL descendants recursively, cleans up server ports
149
+ kill(pid: number, signal: string = "SIGTERM"): boolean {
150
+ const handle = this._processes.get(pid);
151
+ if (!handle) return false;
152
+ handle.kill(signal);
153
+ this._killDescendants(pid, signal);
154
+ this._cleanupServerPorts(pid);
155
+ return true;
156
+ }
157
+
158
+ private _cleanupServerPorts(pid: number): void {
159
+ for (const [port, ownerPid] of this._serverPorts) {
160
+ if (ownerPid === pid) {
161
+ this._serverPorts.delete(port);
162
+ this.emit("server-close", pid, port);
163
+ }
164
+ }
165
+ const children = this._childPids.get(pid);
166
+ if (children) {
167
+ for (const childPid of children) {
168
+ this._cleanupServerPorts(childPid);
169
+ }
170
+ }
171
+ }
172
+
173
+ private _killDescendants(pid: number, signal: string): void {
174
+ const children = this._childPids.get(pid);
175
+ if (!children) return;
176
+ for (const childPid of children) {
177
+ const childHandle = this._processes.get(childPid);
178
+ if (childHandle && childHandle.state !== "exited") {
179
+ childHandle.kill(signal);
180
+ // Prevent stale output from dying workers leaking into the terminal
181
+ childHandle.removeAllListeners("stdout");
182
+ childHandle.removeAllListeners("stderr");
183
+ }
184
+ this._killDescendants(childPid, signal);
185
+ }
186
+ }
187
+
188
+ teardown(): void {
189
+ for (const [pid, handle] of this._processes) {
190
+ try { handle.kill("SIGKILL"); } catch {
191
+ /* ignore */
192
+ }
193
+ }
194
+ this._processes.clear();
195
+ }
196
+
197
+ get processCount(): number {
198
+ return this._processes.size;
199
+ }
200
+
201
+ registerServerPort(port: number, pid: number): void {
202
+ this._serverPorts.set(port, pid);
203
+ }
204
+
205
+ unregisterServerPort(port: number): void {
206
+ this._serverPorts.delete(port);
207
+ }
208
+
209
+ getServerPorts(): number[] {
210
+ return [...this._serverPorts.keys()];
211
+ }
212
+
213
+ // Dispatch HTTP request to the worker owning the port
214
+ dispatchHttpRequest(
215
+ port: number,
216
+ method: string,
217
+ path: string,
218
+ headers: Record<string, string>,
219
+ body?: string | null,
220
+ ): Promise<{ statusCode: number; statusMessage: string; headers: Record<string, string>; body: string }> {
221
+ const pid = this._serverPorts.get(port);
222
+ if (pid === undefined) {
223
+ return Promise.resolve({
224
+ statusCode: 503,
225
+ statusMessage: "Service Unavailable",
226
+ headers: { "Content-Type": "text/plain" },
227
+ body: `No server on port ${port}`,
228
+ });
229
+ }
230
+
231
+ const handle = this._processes.get(pid);
232
+ if (!handle || handle.state === "exited") {
233
+ this._serverPorts.delete(port);
234
+ return Promise.resolve({
235
+ statusCode: 503,
236
+ statusMessage: "Service Unavailable",
237
+ headers: { "Content-Type": "text/plain" },
238
+ body: `Server process exited (pid ${pid})`,
239
+ });
240
+ }
241
+
242
+ const requestId = this._nextHttpRequestId++;
243
+ // console.log(`[PM] dispatchHttpRequest #${requestId} ${method} ${path} → pid ${pid}`);
244
+ return new Promise((resolve) => {
245
+ this._httpCallbacks.set(requestId, (resp) => {
246
+ this._httpCallbacks.delete(requestId);
247
+ // console.log(`[PM] http-response #${requestId} status=${resp.statusCode}`);
248
+ resolve({
249
+ statusCode: resp.statusCode,
250
+ statusMessage: resp.statusMessage,
251
+ headers: resp.headers,
252
+ body: resp.body,
253
+ });
254
+ });
255
+
256
+ handle.postMessage({
257
+ type: "http-request",
258
+ requestId,
259
+ port,
260
+ method,
261
+ path,
262
+ headers,
263
+ body: body ?? null,
264
+ });
265
+ });
266
+ }
267
+
268
+ // Returns owning PID, or -1 if no server found
269
+ dispatchWsUpgrade(
270
+ port: number,
271
+ uid: string,
272
+ path: string,
273
+ headers: Record<string, string>,
274
+ ): number {
275
+ const pid = this._serverPorts.get(port);
276
+ if (pid === undefined) return -1;
277
+
278
+ const handle = this._processes.get(pid);
279
+ if (!handle || handle.state === "exited") {
280
+ this._serverPorts.delete(port);
281
+ return -1;
282
+ }
283
+
284
+ handle.postMessage({ type: "ws-upgrade", uid, port, path, headers });
285
+ return pid;
286
+ }
287
+
288
+ dispatchWsData(pid: number, uid: string, frame: number[]): void {
289
+ const handle = this._processes.get(pid);
290
+ if (!handle || handle.state === "exited") return;
291
+ handle.postMessage({ type: "ws-data", uid, frame });
292
+ }
293
+
294
+ dispatchWsClose(pid: number, uid: string, code: number): void {
295
+ const handle = this._processes.get(pid);
296
+ if (!handle || handle.state === "exited") return;
297
+ handle.postMessage({ type: "ws-close", uid, code });
298
+ }
299
+
300
+ /* ---- Internal ---- */
301
+
302
+ private static _workerBlobUrl: string | null = null;
303
+
304
+ private _createWorker(): Worker {
305
+ // Blob URL from pre-bundled source — works in any environment
306
+ if (!ProcessManager._workerBlobUrl) {
307
+ const blob = new Blob([PROCESS_WORKER_BUNDLE], { type: "application/javascript" });
308
+ ProcessManager._workerBlobUrl = URL.createObjectURL(blob);
309
+ }
310
+ return new Worker(ProcessManager._workerBlobUrl);
311
+ }
312
+
313
+ private _wireHandleEvents(handle: ProcessHandle): void {
314
+ // Forward signals to all descendants (handles both running and exited parent)
315
+ handle.on("signal", (signal: string) => {
316
+ this._killDescendants(handle.pid, signal);
317
+ });
318
+
319
+ // Forward stdin to children (even if parent is blocked on Atomics.wait)
320
+ handle.on("stdin-forward", (data: string) => {
321
+ const children = this._childPids.get(handle.pid);
322
+ if (children) {
323
+ for (const childPid of children) {
324
+ const childHandle = this._processes.get(childPid);
325
+ if (childHandle && childHandle.state !== "exited") {
326
+ childHandle.sendStdin(data);
327
+ }
328
+ }
329
+ }
330
+ });
331
+
332
+ handle.on("exit", (exitCode: number) => {
333
+ for (const [port, pid] of this._serverPorts) {
334
+ if (pid === handle.pid) {
335
+ this._serverPorts.delete(port);
336
+ this.emit("server-close", handle.pid, port);
337
+ }
338
+ }
339
+ this.emit("exit", handle.pid, exitCode);
340
+ // Delay removal so event handlers finish
341
+ setTimeout(() => {
342
+ this._processes.delete(handle.pid);
343
+ }, 100);
344
+ });
345
+
346
+ handle.on("vfs-write", (path: string, content: ArrayBuffer, isDirectory: boolean) => {
347
+ if (this._vfsBridge) {
348
+ if (isDirectory) {
349
+ this._vfsBridge.handleWorkerMkdir(path);
350
+ } else {
351
+ this._vfsBridge.handleWorkerWrite(path, new Uint8Array(content));
352
+ }
353
+ this._vfsBridge.broadcastChange(path, content, handle.pid);
354
+ }
355
+ });
356
+
357
+ handle.on("vfs-delete", (path: string) => {
358
+ if (this._vfsBridge) {
359
+ this._vfsBridge.handleWorkerDelete(path);
360
+ this._vfsBridge.broadcastChange(path, null, handle.pid);
361
+ }
362
+ });
363
+
364
+ handle.on("vfs-read", (requestId: number, path: string) => {
365
+ try {
366
+ if (this._volume.existsSync(path)) {
367
+ const stat = this._volume.statSync(path);
368
+ if (stat.isDirectory()) {
369
+ handle.postMessage({
370
+ type: "vfs-sync",
371
+ path,
372
+ content: null,
373
+ isDirectory: true,
374
+ });
375
+ } else {
376
+ const data = this._volume.readFileSync(path);
377
+ const buffer = (data.buffer as ArrayBuffer).slice(
378
+ data.byteOffset,
379
+ data.byteOffset + data.byteLength,
380
+ );
381
+ handle.postMessage({
382
+ type: "vfs-sync",
383
+ path,
384
+ content: buffer,
385
+ isDirectory: false,
386
+ }, [buffer]);
387
+ }
388
+ } else {
389
+ handle.postMessage({
390
+ type: "vfs-sync",
391
+ path,
392
+ content: null,
393
+ isDirectory: false,
394
+ });
395
+ }
396
+ } catch {
397
+ // Send null so worker doesn't hang
398
+ try {
399
+ handle.postMessage({
400
+ type: "vfs-sync",
401
+ path,
402
+ content: null,
403
+ isDirectory: false,
404
+ });
405
+ } catch { /* worker may have died */ }
406
+ }
407
+ });
408
+
409
+ handle.on("spawn-request", (msg: WorkerToMain_SpawnRequest) => {
410
+ const fullCmd = msg.args.length ? `${msg.command} ${msg.args.join(" ")}` : msg.command;
411
+ try {
412
+ const childHandle = this.spawn({
413
+ command: msg.command,
414
+ args: msg.args,
415
+ cwd: msg.cwd,
416
+ env: msg.env,
417
+ parentPid: handle.pid,
418
+ });
419
+
420
+ if (!this._childPids.has(handle.pid)) {
421
+ this._childPids.set(handle.pid, new Set());
422
+ }
423
+ this._childPids.get(handle.pid)!.add(childHandle.pid);
424
+
425
+ // Defer parent exit/done until child finishes (e.g. create-vite -> vite dev)
426
+ handle.holdExit();
427
+ handle.holdShellDone();
428
+
429
+ handle.postMessage({
430
+ type: "spawn-result",
431
+ requestId: msg.requestId,
432
+ pid: childHandle.pid,
433
+ });
434
+
435
+ // Detect bare node commands and send as direct execution
436
+ const isNodeBin = /(?:^|\/)node$/.test(msg.command);
437
+ const sendExec = () => {
438
+ if (isNodeBin && msg.args.length > 0) {
439
+ childHandle.exec({
440
+ type: "exec",
441
+ filePath: msg.args[0],
442
+ args: msg.args.slice(1),
443
+ cwd: msg.cwd,
444
+ env: msg.env,
445
+ isShell: false,
446
+ });
447
+ } else {
448
+ childHandle.exec({
449
+ type: "exec",
450
+ filePath: "",
451
+ args: msg.args,
452
+ cwd: msg.cwd,
453
+ env: msg.env,
454
+ isShell: true,
455
+ shellCommand: fullCmd,
456
+ });
457
+ }
458
+ };
459
+
460
+ if (childHandle.state === "running") {
461
+ sendExec();
462
+ } else {
463
+ childHandle.on("ready", sendExec);
464
+ }
465
+
466
+ // Relay child output: direct emit if parent done, postMessage if still running
467
+ childHandle.on("stdout", (data: string) => {
468
+ if (handle.workerExited || handle.shellCommandDone) {
469
+ handle.emit("stdout", data);
470
+ } else {
471
+ handle.postMessage({
472
+ type: "child-output",
473
+ requestId: msg.requestId,
474
+ stream: "stdout",
475
+ data,
476
+ });
477
+ }
478
+ });
479
+ childHandle.on("stderr", (data: string) => {
480
+ if (handle.workerExited || handle.shellCommandDone) {
481
+ handle.emit("stderr", data);
482
+ } else {
483
+ handle.postMessage({
484
+ type: "child-output",
485
+ requestId: msg.requestId,
486
+ stream: "stderr",
487
+ data,
488
+ });
489
+ }
490
+ });
491
+
492
+ childHandle.on("stdin-raw-status", (isRaw: boolean) => {
493
+ handle.emit("stdin-raw-status", isRaw);
494
+ });
495
+
496
+ childHandle.on("exit", (exitCode: number) => {
497
+ if (!handle.workerExited) {
498
+ handle.postMessage({
499
+ type: "child-exit",
500
+ requestId: msg.requestId,
501
+ exitCode,
502
+ stdout: childHandle.stdout,
503
+ stderr: childHandle.stderr,
504
+ });
505
+ }
506
+ const children = this._childPids.get(handle.pid);
507
+ if (children) {
508
+ children.delete(childHandle.pid);
509
+ if (children.size === 0) this._childPids.delete(handle.pid);
510
+ }
511
+ handle.releaseExit();
512
+ handle.releaseShellDone();
513
+ });
514
+ } catch (e) {
515
+ handle.postMessage({
516
+ type: "spawn-result",
517
+ requestId: msg.requestId,
518
+ pid: -1,
519
+ error: e instanceof Error ? e.message : String(e),
520
+ });
521
+ }
522
+ });
523
+
524
+ handle.on("fork-request", (msg: WorkerToMain_ForkRequest) => {
525
+ try {
526
+ const childHandle = this.spawn({
527
+ command: "node",
528
+ args: [msg.modulePath, ...msg.args],
529
+ cwd: msg.cwd,
530
+ env: msg.env,
531
+ parentPid: handle.pid,
532
+ });
533
+
534
+ if (!this._childPids.has(handle.pid)) {
535
+ this._childPids.set(handle.pid, new Set());
536
+ }
537
+ this._childPids.get(handle.pid)!.add(childHandle.pid);
538
+ handle.holdExit();
539
+ handle.holdShellDone();
540
+
541
+ handle.postMessage({
542
+ type: "spawn-result",
543
+ requestId: msg.requestId,
544
+ pid: childHandle.pid,
545
+ });
546
+
547
+ const sendExec = () => {
548
+ childHandle.exec({
549
+ type: "exec",
550
+ filePath: msg.modulePath,
551
+ args: msg.args,
552
+ cwd: msg.cwd,
553
+ env: msg.env,
554
+ isShell: false,
555
+ isFork: true,
556
+ });
557
+ };
558
+
559
+ if (childHandle.state === "running") {
560
+ sendExec();
561
+ } else {
562
+ childHandle.on("ready", sendExec);
563
+ }
564
+
565
+ childHandle.on("stdout", (data: string) => {
566
+ if (handle.workerExited || handle.shellCommandDone) {
567
+ handle.emit("stdout", data);
568
+ } else {
569
+ handle.postMessage({
570
+ type: "child-output",
571
+ requestId: msg.requestId,
572
+ stream: "stdout",
573
+ data,
574
+ });
575
+ }
576
+ });
577
+ childHandle.on("stderr", (data: string) => {
578
+ if (handle.workerExited || handle.shellCommandDone) {
579
+ handle.emit("stderr", data);
580
+ } else {
581
+ handle.postMessage({
582
+ type: "child-output",
583
+ requestId: msg.requestId,
584
+ stream: "stderr",
585
+ data,
586
+ });
587
+ }
588
+ });
589
+
590
+ // IPC: child -> parent
591
+ childHandle.on("ipc-message", (ipcMsg: any) => {
592
+ const payload = ipcMsg?.data ?? ipcMsg;
593
+ if (!handle.workerExited) {
594
+ handle.postMessage({
595
+ type: "ipc-message",
596
+ targetRequestId: msg.requestId,
597
+ data: payload,
598
+ } as any);
599
+ }
600
+ });
601
+
602
+ // IPC: parent -> child
603
+ handle.on("ipc-message", (ipcMsg: any) => {
604
+ if (ipcMsg.targetRequestId === msg.requestId) {
605
+ childHandle.postMessage({
606
+ type: "ipc-message",
607
+ data: ipcMsg.data,
608
+ });
609
+ }
610
+ });
611
+
612
+ childHandle.on("exit", (exitCode: number) => {
613
+ if (!handle.workerExited) {
614
+ handle.postMessage({
615
+ type: "child-exit",
616
+ requestId: msg.requestId,
617
+ exitCode,
618
+ stdout: childHandle.stdout,
619
+ stderr: childHandle.stderr,
620
+ });
621
+ }
622
+ const children = this._childPids.get(handle.pid);
623
+ if (children) {
624
+ children.delete(childHandle.pid);
625
+ if (children.size === 0) this._childPids.delete(handle.pid);
626
+ }
627
+ handle.releaseExit();
628
+ handle.releaseShellDone();
629
+ });
630
+ } catch (e) {
631
+ handle.postMessage({
632
+ type: "spawn-result",
633
+ requestId: msg.requestId,
634
+ pid: -1,
635
+ error: e instanceof Error ? e.message : String(e),
636
+ });
637
+ }
638
+ });
639
+
640
+ handle.on("workerthread-request", (msg: WorkerToMain_WorkerThreadRequest) => {
641
+ try {
642
+ let modulePath = msg.modulePath;
643
+ // eval mode: write code to temp VFS file
644
+ if (msg.isEval) {
645
+ const evalPath = `/__wt_eval_${msg.threadId}__.js`;
646
+ this._volume.writeFileSync(evalPath, msg.modulePath);
647
+ modulePath = evalPath;
648
+ if (this._vfsBridge) {
649
+ const encoder = new TextEncoder();
650
+ const content = encoder.encode(msg.modulePath).buffer as ArrayBuffer;
651
+ this._vfsBridge.handleWorkerWrite(evalPath, new Uint8Array(content));
652
+ this._vfsBridge.broadcastChange(evalPath, content, handle.pid);
653
+ }
654
+ }
655
+
656
+ const childHandle = this.spawn({
657
+ command: "node",
658
+ args: [modulePath],
659
+ cwd: msg.cwd,
660
+ env: msg.env,
661
+ parentPid: handle.pid,
662
+ });
663
+
664
+ if (!this._childPids.has(handle.pid)) {
665
+ this._childPids.set(handle.pid, new Set());
666
+ }
667
+ this._childPids.get(handle.pid)!.add(childHandle.pid);
668
+ handle.holdExit();
669
+ handle.holdShellDone();
670
+
671
+ handle.postMessage({
672
+ type: "spawn-result",
673
+ requestId: msg.requestId,
674
+ pid: childHandle.pid,
675
+ });
676
+
677
+ const sendExec = () => {
678
+ childHandle.exec({
679
+ type: "exec",
680
+ filePath: modulePath,
681
+ args: msg.args || [],
682
+ cwd: msg.cwd,
683
+ env: msg.env,
684
+ isShell: false,
685
+ isFork: true,
686
+ isWorkerThread: true,
687
+ workerData: msg.workerData,
688
+ threadId: msg.threadId,
689
+ });
690
+ };
691
+
692
+ if (childHandle.state === "running") {
693
+ sendExec();
694
+ } else {
695
+ childHandle.on("ready", sendExec);
696
+ }
697
+
698
+ childHandle.on("stdout", (data: string) => {
699
+ if (handle.workerExited || handle.shellCommandDone) {
700
+ handle.emit("stdout", data);
701
+ } else {
702
+ handle.postMessage({
703
+ type: "child-output",
704
+ requestId: msg.requestId,
705
+ stream: "stdout",
706
+ data,
707
+ });
708
+ }
709
+ });
710
+ childHandle.on("stderr", (data: string) => {
711
+ if (handle.workerExited || handle.shellCommandDone) {
712
+ handle.emit("stderr", data);
713
+ } else {
714
+ handle.postMessage({
715
+ type: "child-output",
716
+ requestId: msg.requestId,
717
+ stream: "stderr",
718
+ data,
719
+ });
720
+ }
721
+ });
722
+
723
+ childHandle.on("ipc-message", (ipcMsg: any) => {
724
+ const payload = ipcMsg?.data ?? ipcMsg;
725
+ if (!handle.workerExited) {
726
+ handle.postMessage({
727
+ type: "ipc-message",
728
+ targetRequestId: msg.requestId,
729
+ data: payload,
730
+ } as any);
731
+ }
732
+ });
733
+
734
+ handle.on("ipc-message", (ipcMsg: any) => {
735
+ if (ipcMsg.targetRequestId === msg.requestId) {
736
+ childHandle.postMessage({
737
+ type: "ipc-message",
738
+ data: ipcMsg.data,
739
+ });
740
+ }
741
+ });
742
+
743
+ childHandle.on("exit", (exitCode: number) => {
744
+ if (msg.isEval) {
745
+ try {
746
+ this._volume.unlinkSync(`/__wt_eval_${msg.threadId}__.js`);
747
+ } catch {
748
+ /* ignore */
749
+ }
750
+ }
751
+
752
+ if (!handle.workerExited) {
753
+ handle.postMessage({
754
+ type: "child-exit",
755
+ requestId: msg.requestId,
756
+ exitCode,
757
+ stdout: childHandle.stdout,
758
+ stderr: childHandle.stderr,
759
+ });
760
+ }
761
+ const children = this._childPids.get(handle.pid);
762
+ if (children) {
763
+ children.delete(childHandle.pid);
764
+ if (children.size === 0) this._childPids.delete(handle.pid);
765
+ }
766
+ handle.releaseExit();
767
+ handle.releaseShellDone();
768
+ });
769
+ } catch (e) {
770
+ handle.postMessage({
771
+ type: "spawn-result",
772
+ requestId: msg.requestId,
773
+ pid: -1,
774
+ error: e instanceof Error ? e.message : String(e),
775
+ });
776
+ }
777
+ });
778
+
779
+ handle.on("spawn-sync", (msg: WorkerToMain_SpawnSync) => {
780
+ if (!this._syncBuffer) {
781
+ return;
782
+ }
783
+
784
+ const fullCmd = msg.shellCommand ??
785
+ (msg.args.length ? `${msg.command} ${msg.args.join(" ")}` : msg.command);
786
+ const maxStdoutLen = (SLOT_SIZE - 3) * 4;
787
+
788
+ const signalError = (exitCode: number) => {
789
+ try {
790
+ const int32 = new Int32Array(this._syncBuffer!);
791
+ const slotBase = msg.syncSlot * SLOT_SIZE;
792
+ Atomics.store(int32, slotBase + 1, exitCode);
793
+ Atomics.store(int32, slotBase + 2, 0);
794
+ Atomics.store(int32, slotBase, 2); // STATUS_ERROR
795
+ Atomics.notify(int32, slotBase);
796
+ } catch {
797
+ // Buffer unusable — worker will time out
798
+ }
799
+ };
800
+
801
+ try {
802
+ const childHandle = this.spawn({
803
+ command: msg.command,
804
+ args: msg.args,
805
+ cwd: msg.cwd,
806
+ env: msg.env,
807
+ parentPid: handle.pid,
808
+ });
809
+
810
+ // Must track for Ctrl+C signal propagation via _killDescendants
811
+ if (!this._childPids.has(handle.pid)) {
812
+ this._childPids.set(handle.pid, new Set());
813
+ }
814
+ this._childPids.get(handle.pid)!.add(childHandle.pid);
815
+
816
+ handle.holdExit();
817
+ handle.holdShellDone();
818
+ handle.holdSync(); // parent is blocked on Atomics.wait — stdin must bypass
819
+
820
+ const sendExec = () => {
821
+ childHandle.exec({
822
+ type: "exec",
823
+ filePath: "",
824
+ args: msg.args,
825
+ cwd: msg.cwd,
826
+ env: msg.env,
827
+ isShell: true,
828
+ shellCommand: fullCmd,
829
+ });
830
+ };
831
+
832
+ if (childHandle.state === "running") {
833
+ sendExec();
834
+ } else {
835
+ childHandle.on("ready", sendExec);
836
+ }
837
+
838
+ // Parent is blocked on Atomics.wait — can't process postMessage, emit directly
839
+ childHandle.on("stdout", (data: string) => {
840
+ handle.emit("stdout", data);
841
+ });
842
+ childHandle.on("stderr", (data: string) => {
843
+ handle.emit("stderr", data);
844
+ });
845
+
846
+ childHandle.on("stdin-raw-status", (isRaw: boolean) => {
847
+ handle.emit("stdin-raw-status", isRaw);
848
+ });
849
+
850
+ childHandle.on("exit", (exitCode: number) => {
851
+ try {
852
+ const int32 = new Int32Array(this._syncBuffer!);
853
+ const encoder = new TextEncoder();
854
+ const slotBase = msg.syncSlot * SLOT_SIZE;
855
+ const stdoutBytes = encoder.encode(childHandle.stdout);
856
+ const truncatedLen = Math.min(stdoutBytes.byteLength, maxStdoutLen);
857
+
858
+ Atomics.store(int32, slotBase + 1, exitCode);
859
+ Atomics.store(int32, slotBase + 2, truncatedLen);
860
+
861
+ const uint8 = new Uint8Array(this._syncBuffer!);
862
+ const dataOffset = (slotBase + 3) * 4;
863
+ uint8.set(stdoutBytes.subarray(0, truncatedLen), dataOffset);
864
+
865
+ // Must be last — wakes the waiting worker
866
+ Atomics.store(int32, slotBase, 1);
867
+ Atomics.notify(int32, slotBase);
868
+ } catch {
869
+ signalError(1);
870
+ }
871
+
872
+ const children = this._childPids.get(handle.pid);
873
+ if (children) {
874
+ children.delete(childHandle.pid);
875
+ if (children.size === 0) this._childPids.delete(handle.pid);
876
+ }
877
+
878
+ handle.releaseSync();
879
+ handle.releaseExit();
880
+ handle.releaseShellDone();
881
+ });
882
+
883
+ childHandle.on("worker-error", () => {
884
+ signalError(1);
885
+ const children = this._childPids.get(handle.pid);
886
+ if (children) {
887
+ children.delete(childHandle.pid);
888
+ if (children.size === 0) this._childPids.delete(handle.pid);
889
+ }
890
+ handle.releaseSync();
891
+ handle.releaseExit();
892
+ handle.releaseShellDone();
893
+ });
894
+ } catch {
895
+ signalError(1);
896
+ }
897
+ });
898
+
899
+ handle.on("server-listen", (port: number, hostname: string) => {
900
+ this.registerServerPort(port, handle.pid);
901
+ this.emit("server-listen", handle.pid, port, hostname);
902
+ });
903
+
904
+ handle.on("server-close", (port: number) => {
905
+ this.unregisterServerPort(port);
906
+ this.emit("server-close", handle.pid, port);
907
+ });
908
+
909
+ handle.on("http-response", (msg: WorkerToMain_HttpResponse) => {
910
+ const cb = this._httpCallbacks.get(msg.requestId);
911
+ if (cb) cb(msg);
912
+ });
913
+
914
+ handle.on("ws-frame", (msg: any) => {
915
+ this.emit("ws-frame", msg);
916
+ });
917
+
918
+ handle.on("cwd-change", (cwd: string) => {
919
+ this.emit("cwd-change", handle.pid, cwd);
920
+ });
921
+
922
+ handle.on("stdin-raw-status", (isRaw: boolean) => {
923
+ this.emit("stdin-raw-status", handle.pid, isRaw);
924
+ });
925
+
926
+ handle.on("worker-error", (message: string, stack?: string) => {
927
+ this.emit("error", handle.pid, message, stack);
928
+ });
929
+ }
930
+
931
+ private _createEmptySnapshot(): VFSBinarySnapshot {
932
+ return {
933
+ manifest: [],
934
+ data: new ArrayBuffer(0),
935
+ };
936
+ }
937
+
938
+ broadcastVFSChange(path: string, content: ArrayBuffer | null, isDirectory: boolean, excludePid: number): void {
939
+ for (const [pid, handle] of this._processes) {
940
+ if (pid === excludePid || handle.state === "exited") continue;
941
+ try {
942
+ // ArrayBuffer can only be transferred once, clone for each recipient
943
+ const clonedContent = content ? content.slice(0) : null;
944
+ handle.postMessage({
945
+ type: "vfs-sync",
946
+ path,
947
+ content: clonedContent,
948
+ isDirectory,
949
+ }, clonedContent ? [clonedContent] : []);
950
+ } catch {
951
+ /* ignore */
952
+ }
953
+ }
954
+ }
955
+ }
956
+