@gjsify/worker_threads 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,148 @@
1
+ // Reference: Node.js lib/worker_threads.js
2
+ // Reimplemented for GJS — MessageChannel/BroadcastChannel are pure JS,
3
+ // Worker uses Gio.Subprocess (subprocess-based workers)
4
+
5
+ export { MessagePort } from './message-port.ts';
6
+ export { Worker } from './worker.ts';
7
+ export type { WorkerOptions } from './worker.ts';
8
+
9
+ import { MessagePort } from './message-port.ts';
10
+ import { Worker } from './worker.ts';
11
+
12
+ // When running inside a Worker subprocess, the bootstrap script sets
13
+ // globalThis.__gjsify_worker_context before importing the user's code.
14
+ const _ctx = (globalThis as Record<string, unknown>).__gjsify_worker_context as
15
+ { isMainThread: boolean; parentPort: unknown; workerData: unknown; threadId: number } | undefined;
16
+
17
+ export const isMainThread: boolean = !_ctx;
18
+ export const parentPort: MessagePort | null = (_ctx?.parentPort as MessagePort) ?? null;
19
+ export const workerData: unknown = _ctx?.workerData ?? null;
20
+ export const threadId: number = _ctx?.threadId ?? 0;
21
+ export const resourceLimits: Record<string, unknown> = {};
22
+ export const SHARE_ENV: unique symbol = Symbol('worker_threads.SHARE_ENV');
23
+
24
+ export class MessageChannel {
25
+ readonly port1: MessagePort;
26
+ readonly port2: MessagePort;
27
+
28
+ constructor() {
29
+ this.port1 = new MessagePort();
30
+ this.port2 = new MessagePort();
31
+ this.port1._otherPort = this.port2;
32
+ this.port2._otherPort = this.port1;
33
+ }
34
+ }
35
+
36
+ const _broadcastRegistry = new Map<string, Set<BroadcastChannel>>();
37
+
38
+ export class BroadcastChannel {
39
+ readonly name: string;
40
+ private _closed = false;
41
+ onmessage: ((event: { data: unknown }) => void) | null = null;
42
+ onmessageerror: ((event: { data: unknown }) => void) | null = null;
43
+
44
+ constructor(name: string) {
45
+ this.name = String(name);
46
+ if (!_broadcastRegistry.has(this.name)) {
47
+ _broadcastRegistry.set(this.name, new Set());
48
+ }
49
+ _broadcastRegistry.get(this.name)!.add(this);
50
+ }
51
+
52
+ postMessage(message: unknown): void {
53
+ if (this._closed) {
54
+ throw new Error('BroadcastChannel is closed');
55
+ }
56
+
57
+ const channels = _broadcastRegistry.get(this.name);
58
+ if (!channels) return;
59
+
60
+ for (const channel of channels) {
61
+ if (channel !== this && !channel._closed) {
62
+ // Clone per receiver per W3C spec — each recipient gets an independent copy
63
+ const cloned = structuredClone(message);
64
+ Promise.resolve().then(() => {
65
+ if (!channel._closed && channel.onmessage) {
66
+ channel.onmessage({ data: cloned });
67
+ }
68
+ });
69
+ }
70
+ }
71
+ }
72
+
73
+ close(): void {
74
+ if (this._closed) return;
75
+ this._closed = true;
76
+ const channels = _broadcastRegistry.get(this.name);
77
+ if (channels) {
78
+ channels.delete(this);
79
+ if (channels.size === 0) {
80
+ _broadcastRegistry.delete(this.name);
81
+ }
82
+ }
83
+ this.onmessage = null;
84
+ this.onmessageerror = null;
85
+ }
86
+
87
+ addEventListener(type: string, listener: ((event: unknown) => void) | null): void {
88
+ if (type === 'message') {
89
+ this.onmessage = listener as ((event: { data: unknown }) => void) | null;
90
+ } else if (type === 'messageerror') {
91
+ this.onmessageerror = listener as ((event: { data: unknown }) => void) | null;
92
+ }
93
+ }
94
+
95
+ removeEventListener(type: string, _listener: ((event: unknown) => void) | null): void {
96
+ if (type === 'message') this.onmessage = null;
97
+ else if (type === 'messageerror') this.onmessageerror = null;
98
+ }
99
+
100
+ dispatchEvent(_event: Event): boolean {
101
+ return true;
102
+ }
103
+ }
104
+
105
+ const _environmentData = new Map<string, unknown>();
106
+
107
+ export function setEnvironmentData(key: string, value: unknown): void {
108
+ if (value === undefined) {
109
+ _environmentData.delete(key);
110
+ } else {
111
+ _environmentData.set(key, value);
112
+ }
113
+ }
114
+
115
+ export function getEnvironmentData(key: string): unknown {
116
+ return _environmentData.get(key);
117
+ }
118
+
119
+ export function receiveMessageOnPort(port: MessagePort): { message: unknown } | undefined {
120
+ if (!port._hasQueuedMessages) return undefined;
121
+ return { message: port._dequeueMessage() };
122
+ }
123
+
124
+ // No-ops in GJS — all messages are cloned, not transferred
125
+ export function markAsUntransferable(_object: unknown): void {}
126
+ export function markAsUncloneable(_object: unknown): void {}
127
+ export function moveMessagePortToContext(port: MessagePort, _context: unknown): MessagePort {
128
+ return port;
129
+ }
130
+
131
+ export default {
132
+ isMainThread,
133
+ parentPort,
134
+ workerData,
135
+ threadId,
136
+ resourceLimits,
137
+ SHARE_ENV,
138
+ Worker,
139
+ MessageChannel,
140
+ MessagePort,
141
+ BroadcastChannel,
142
+ setEnvironmentData,
143
+ getEnvironmentData,
144
+ receiveMessageOnPort,
145
+ markAsUntransferable,
146
+ markAsUncloneable,
147
+ moveMessagePortToContext,
148
+ };
@@ -0,0 +1,130 @@
1
+ // Reference: Node.js lib/internal/worker/io.js
2
+ // Reimplemented for GJS using EventEmitter
3
+
4
+ import { EventEmitter } from 'node:events';
5
+
6
+ export class MessagePort extends EventEmitter {
7
+ private _started = false;
8
+ private _closed = false;
9
+ private _messageQueue: unknown[] = [];
10
+ /** @internal Linked port for in-process communication */
11
+ _otherPort: MessagePort | null = null;
12
+ /** @internal Maps addEventListener listeners to their internal wrappers */
13
+ private _aeWrappers: Map<((event: unknown) => void), ((data: unknown) => void)> = new Map();
14
+
15
+ start(): void {
16
+ if (this._started || this._closed) return;
17
+ this._started = true;
18
+ this._drainQueue();
19
+ }
20
+
21
+ close(): void {
22
+ if (this._closed) return;
23
+ this._closed = true;
24
+ const other = this._otherPort;
25
+ this._otherPort = null;
26
+ if (other) other._otherPort = null;
27
+ this.emit('close');
28
+ this.removeAllListeners();
29
+ }
30
+
31
+ postMessage(value: unknown, _transferList?: unknown[]): void {
32
+ if (this._closed) return;
33
+ const target = this._otherPort;
34
+ if (!target) return;
35
+
36
+ let cloned: unknown;
37
+ try {
38
+ cloned = structuredClone(value);
39
+ } catch (err) {
40
+ this.emit('messageerror', err instanceof Error ? err : new Error('Could not clone message'));
41
+ return;
42
+ }
43
+ target._receiveMessage(cloned);
44
+ }
45
+
46
+ ref(): this { return this; }
47
+ unref(): this { return this; }
48
+
49
+ _receiveMessage(message: unknown): void {
50
+ if (this._closed) return;
51
+ if (!this._started) {
52
+ this._messageQueue.push(message);
53
+ return;
54
+ }
55
+ this._dispatchMessage(message);
56
+ }
57
+
58
+ get _hasQueuedMessages(): boolean {
59
+ return this._messageQueue.length > 0;
60
+ }
61
+
62
+ _dequeueMessage(): unknown | undefined {
63
+ return this._messageQueue.shift();
64
+ }
65
+
66
+ private _drainQueue(): void {
67
+ while (this._messageQueue.length > 0) {
68
+ this._dispatchMessage(this._messageQueue.shift());
69
+ }
70
+ }
71
+
72
+ private _dispatchMessage(message: unknown): void {
73
+ Promise.resolve().then(() => {
74
+ if (!this._closed) {
75
+ this.emit('message', message);
76
+ }
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Web-compatible addEventListener. Wraps message data in a MessageEvent-like
82
+ * object `{ data, type }` before calling the listener.
83
+ * Requires explicit `port.start()` call (unlike `on('message')` which auto-starts).
84
+ */
85
+ addEventListener(type: string, listener: ((event: unknown) => void) | null): void {
86
+ if (!listener) return;
87
+ if (type === 'message') {
88
+ const wrapper = (data: unknown) => {
89
+ listener({ data, type: 'message' });
90
+ };
91
+ this._aeWrappers.set(listener, wrapper);
92
+ super.on('message', wrapper);
93
+ } else {
94
+ super.on(type, listener);
95
+ }
96
+ }
97
+
98
+ removeEventListener(type: string, listener: ((event: unknown) => void) | null): void {
99
+ if (!listener) return;
100
+ if (type === 'message') {
101
+ const wrapper = this._aeWrappers.get(listener);
102
+ if (wrapper) {
103
+ super.off('message', wrapper);
104
+ this._aeWrappers.delete(listener);
105
+ }
106
+ } else {
107
+ super.off(type, listener);
108
+ }
109
+ }
110
+
111
+ on(event: string | symbol, listener: (...args: unknown[]) => void): this {
112
+ super.on(event, listener);
113
+ if (event === 'message' && !this._started) {
114
+ this.start();
115
+ }
116
+ return this;
117
+ }
118
+
119
+ addListener(event: string | symbol, listener: (...args: unknown[]) => void): this {
120
+ return this.on(event, listener);
121
+ }
122
+
123
+ once(event: string | symbol, listener: (...args: unknown[]) => void): this {
124
+ super.once(event, listener);
125
+ if (event === 'message' && !this._started) {
126
+ this.start();
127
+ }
128
+ return this;
129
+ }
130
+ }
package/src/test.mts ADDED
@@ -0,0 +1,4 @@
1
+ import '@gjsify/node-globals';
2
+ import { run } from '@gjsify/unit';
3
+ import testSuite from './index.spec.js';
4
+ run({ testSuite });
package/src/worker.ts ADDED
@@ -0,0 +1,394 @@
1
+ // Reference: Node.js lib/internal/worker.js
2
+ // Reimplemented for GJS using Gio.Subprocess (subprocess-based workers)
3
+ //
4
+ // Limitations:
5
+ // - No shared memory (SharedArrayBuffer not supported across processes)
6
+ // - Worker scripts must be pre-built .mjs files (no on-the-fly TypeScript)
7
+ // - Higher overhead than native threading (process spawning)
8
+ // - eval mode code must be self-contained (no bare specifier imports)
9
+
10
+ import Gio from '@girs/gio-2.0';
11
+ import GLib from '@girs/glib-2.0';
12
+ import { EventEmitter } from 'node:events';
13
+
14
+ let _nextThreadId = 1;
15
+ const _encoder = new TextEncoder();
16
+
17
+ // GJS worker bootstrap script — runs in the child gjs process,
18
+ // sets up IPC via stdin/stdout pipes using gi:// imports.
19
+ const BOOTSTRAP_CODE = `\
20
+ import GLib from 'gi://GLib';
21
+ import Gio from 'gi://Gio';
22
+
23
+ const loop = new GLib.MainLoop(null, false);
24
+ const stdinStream = Gio.UnixInputStream.new(0, false);
25
+ const dataIn = Gio.DataInputStream.new(stdinStream);
26
+ const stdoutStream = Gio.UnixOutputStream.new(1, false);
27
+
28
+ function send(obj) {
29
+ const line = JSON.stringify(obj) + '\\n';
30
+ stdoutStream.write_all(_encoder.encode(line), null);
31
+ }
32
+
33
+ // Read init data (first line, blocking)
34
+ const [initLine] = dataIn.read_line_utf8(null);
35
+ const init = JSON.parse(initLine);
36
+
37
+ // Simplified EventEmitter for parentPort
38
+ const _listeners = new Map();
39
+ const parentPort = {
40
+ on(ev, fn) {
41
+ if (!_listeners.has(ev)) _listeners.set(ev, []);
42
+ _listeners.get(ev).push(fn);
43
+ return this;
44
+ },
45
+ once(ev, fn) {
46
+ const w = (...a) => { parentPort.off(ev, w); fn(...a); };
47
+ return parentPort.on(ev, w);
48
+ },
49
+ off(ev, fn) {
50
+ const a = _listeners.get(ev);
51
+ if (a) _listeners.set(ev, a.filter(f => f !== fn));
52
+ return this;
53
+ },
54
+ emit(ev, ...a) { (_listeners.get(ev) || []).forEach(fn => fn(...a)); },
55
+ postMessage(data) { send({ type: 'message', data }); },
56
+ close() { send({ type: 'exit', code: 0 }); loop.quit(); },
57
+ removeAllListeners(ev) {
58
+ if (ev) _listeners.delete(ev); else _listeners.clear();
59
+ return this;
60
+ },
61
+ };
62
+
63
+ // Set worker context globals (read by @gjsify/worker_threads when imported by user script)
64
+ globalThis.__gjsify_worker_context = {
65
+ isMainThread: false,
66
+ parentPort,
67
+ workerData: init.workerData ?? null,
68
+ threadId: init.threadId ?? 0,
69
+ };
70
+
71
+ send({ type: 'online' });
72
+
73
+ // Async stdin reader for messages from parent
74
+ function readNext() {
75
+ dataIn.read_line_async(GLib.PRIORITY_DEFAULT, null, (source, result) => {
76
+ try {
77
+ const [line] = source.read_line_finish_utf8(result);
78
+ if (line === null) { loop.quit(); return; }
79
+ const msg = JSON.parse(line);
80
+ if (msg.type === 'message') parentPort.emit('message', msg.data);
81
+ else if (msg.type === 'terminate') { send({ type: 'exit', code: 1 }); loop.quit(); return; }
82
+ readNext();
83
+ } catch { loop.quit(); }
84
+ });
85
+ }
86
+ readNext();
87
+
88
+ // Execute worker code
89
+ try {
90
+ if (init.eval) {
91
+ const AsyncFn = Object.getPrototypeOf(async function(){}).constructor;
92
+ await new AsyncFn('parentPort', 'workerData', 'threadId', init.code)(
93
+ parentPort, init.workerData, init.threadId
94
+ );
95
+ } else {
96
+ await import(init.filename);
97
+ }
98
+ } catch (error) {
99
+ send({ type: 'error', message: error.message, stack: error.stack || '' });
100
+ }
101
+
102
+ loop.run();
103
+ `;
104
+
105
+ const BOOTSTRAP_BYTES = _encoder.encode(BOOTSTRAP_CODE);
106
+
107
+ export interface WorkerOptions {
108
+ argv?: unknown[];
109
+ env?: Record<string, string> | symbol;
110
+ execArgv?: string[];
111
+ stdin?: boolean;
112
+ stdout?: boolean;
113
+ stderr?: boolean;
114
+ workerData?: unknown;
115
+ eval?: boolean;
116
+ transferList?: unknown[];
117
+ resourceLimits?: Record<string, number>;
118
+ name?: string;
119
+ }
120
+
121
+ export class Worker extends EventEmitter {
122
+ readonly threadId: number;
123
+ readonly resourceLimits: Record<string, unknown>;
124
+
125
+ private _subprocess: Gio.Subprocess | null = null;
126
+ private _stdinPipe: Gio.OutputStream | null = null;
127
+ private _exited = false;
128
+ private _bootstrapFile: Gio.File | null = null;
129
+
130
+ constructor(filename: string | URL, options?: WorkerOptions) {
131
+ super();
132
+ this.threadId = _nextThreadId++;
133
+ this.resourceLimits = options?.resourceLimits || {};
134
+
135
+ const isEval = options?.eval === true;
136
+ const resolvedFilename = Worker._resolveFilename(filename, isEval);
137
+
138
+ // Write bootstrap script to temp file
139
+ const tmpDir = GLib.get_tmp_dir();
140
+ const bootstrapPath = `${tmpDir}/gjsify-worker-${this.threadId}-${Date.now()}.mjs`;
141
+ this._bootstrapFile = Gio.File.new_for_path(bootstrapPath);
142
+
143
+ try {
144
+ this._bootstrapFile.replace_contents(
145
+ BOOTSTRAP_BYTES,
146
+ null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, null
147
+ );
148
+ } catch (err) {
149
+ throw new Error(`Failed to create worker bootstrap: ${err instanceof Error ? err.message : err}`);
150
+ }
151
+
152
+ // Spawn GJS subprocess
153
+ const launcher = new Gio.SubprocessLauncher({
154
+ flags: Gio.SubprocessFlags.STDIN_PIPE
155
+ | Gio.SubprocessFlags.STDOUT_PIPE
156
+ | Gio.SubprocessFlags.STDERR_PIPE,
157
+ });
158
+
159
+ if (options?.env && typeof options.env === 'object') {
160
+ for (const [key, value] of Object.entries(options.env as Record<string, string>)) {
161
+ launcher.setenv(key, String(value), true);
162
+ }
163
+ }
164
+
165
+ try {
166
+ this._subprocess = launcher.spawnv(['gjs', '-m', bootstrapPath]);
167
+ } catch (err) {
168
+ this._cleanup();
169
+ throw new Error(`Failed to spawn worker: ${err instanceof Error ? err.message : err}`);
170
+ }
171
+
172
+ this._stdinPipe = this._subprocess.get_stdin_pipe();
173
+ const stdoutPipe = this._subprocess.get_stdout_pipe();
174
+
175
+ // Verify file exists for non-eval workers
176
+ if (!isEval) {
177
+ const filePath = resolvedFilename.startsWith('file://')
178
+ ? resolvedFilename.slice(7)
179
+ : resolvedFilename;
180
+ const file = Gio.File.new_for_path(filePath);
181
+ if (!file.query_exists(null)) {
182
+ this._cleanup();
183
+ const err = new Error(`Cannot find module '${filePath}'`);
184
+ (err as any).code = 'ERR_MODULE_NOT_FOUND';
185
+ // Emit error asynchronously to match Node.js behavior
186
+ Promise.resolve().then(() => {
187
+ this.emit('error', err);
188
+ this._exited = true;
189
+ this.emit('exit', 1);
190
+ });
191
+ // Return early — subprocess was already cleaned up
192
+ return;
193
+ }
194
+ }
195
+
196
+ // Send init data as first line on stdin
197
+ const initData = JSON.stringify({
198
+ threadId: this.threadId,
199
+ workerData: options?.workerData ?? null,
200
+ eval: isEval,
201
+ filename: isEval ? undefined : resolvedFilename,
202
+ code: isEval ? resolvedFilename : undefined,
203
+ }) + '\n';
204
+
205
+ try {
206
+ this._stdinPipe!.write_all(_encoder.encode(initData), null);
207
+ } catch (err) {
208
+ this._cleanup();
209
+ throw new Error(`Failed to send init data: ${err instanceof Error ? err.message : err}`);
210
+ }
211
+
212
+ // Read IPC messages from subprocess stdout
213
+ if (stdoutPipe) {
214
+ const dataStream = Gio.DataInputStream.new(stdoutPipe);
215
+ this._readMessages(dataStream);
216
+ }
217
+
218
+ // Read stderr for error reporting
219
+ const stderrPipe = this._subprocess.get_stderr_pipe();
220
+ if (stderrPipe) {
221
+ this._readStderr(Gio.DataInputStream.new(stderrPipe));
222
+ }
223
+
224
+ // Wait for process exit
225
+ this._subprocess.wait_async(null, () => {
226
+ this._onExit();
227
+ });
228
+ }
229
+
230
+ postMessage(value: unknown, _transferList?: unknown[]): void {
231
+ if (this._exited || !this._stdinPipe) return;
232
+ try {
233
+ const line = JSON.stringify({ type: 'message', data: value }) + '\n';
234
+ this._stdinPipe.write_all(_encoder.encode(line), null);
235
+ } catch {
236
+ // Worker stdin closed
237
+ }
238
+ }
239
+
240
+ terminate(): Promise<number> {
241
+ if (this._exited) return Promise.resolve(0);
242
+
243
+ // Register listener before sending terminate to avoid race
244
+ const exitPromise = new Promise<number>((resolve) => {
245
+ this.once('exit', (code: number) => resolve(code));
246
+ });
247
+
248
+ // Send terminate command
249
+ try {
250
+ if (this._stdinPipe) {
251
+ const msg = JSON.stringify({ type: 'terminate' }) + '\n';
252
+ this._stdinPipe.write_all(_encoder.encode(msg), null);
253
+ }
254
+ } catch {}
255
+
256
+ // Force-exit after timeout
257
+ setTimeout(() => {
258
+ if (!this._exited && this._subprocess) {
259
+ this._subprocess.force_exit();
260
+ }
261
+ }, 500);
262
+
263
+ return exitPromise;
264
+ }
265
+
266
+ ref(): this { return this; }
267
+ unref(): this { return this; }
268
+
269
+ /**
270
+ * Resolve a worker filename to an absolute path or file:// URL.
271
+ *
272
+ * - URL instances → href string
273
+ * - file:// URLs → kept as-is
274
+ * - Absolute paths → converted to file:// URL
275
+ * - Relative paths (./foo, ../bar) → resolved relative to cwd, converted to file:// URL
276
+ * - eval mode → returned as-is (it's code, not a path)
277
+ */
278
+ private static _resolveFilename(filename: string | URL, isEval: boolean): string {
279
+ if (isEval) return String(filename);
280
+
281
+ if (filename instanceof URL) return filename.href;
282
+
283
+ const str = String(filename);
284
+
285
+ // Already a URL
286
+ if (str.startsWith('file://') || str.startsWith('http://') || str.startsWith('https://')) {
287
+ return str;
288
+ }
289
+
290
+ // Absolute path → file:// URL
291
+ if (str.startsWith('/')) {
292
+ return 'file://' + str;
293
+ }
294
+
295
+ // Relative path → resolve from cwd
296
+ if (str.startsWith('./') || str.startsWith('../') || !str.includes('/')) {
297
+ const cwd = GLib.get_current_dir();
298
+ const resolved = GLib.build_filenamev([cwd, str]);
299
+ // Canonicalize (resolve ./ and ../)
300
+ const file = Gio.File.new_for_path(resolved);
301
+ const canonical = file.get_path();
302
+ return 'file://' + (canonical || resolved);
303
+ }
304
+
305
+ // Fallback — treat as absolute
306
+ return 'file://' + str;
307
+ }
308
+
309
+ private _readMessages(dataStream: Gio.DataInputStream): void {
310
+ dataStream.read_line_async(
311
+ GLib.PRIORITY_DEFAULT,
312
+ null,
313
+ (_source: unknown, result: Gio.AsyncResult) => {
314
+ try {
315
+ const [line] = dataStream.read_line_finish_utf8(result);
316
+ if (line === null) return; // EOF
317
+
318
+ const msg = JSON.parse(line);
319
+ switch (msg.type) {
320
+ case 'online':
321
+ this.emit('online');
322
+ break;
323
+ case 'message':
324
+ this.emit('message', msg.data);
325
+ break;
326
+ case 'error': {
327
+ const err = new Error(msg.message);
328
+ if (msg.stack) err.stack = msg.stack;
329
+ this.emit('error', err);
330
+ break;
331
+ }
332
+ }
333
+
334
+ this._readMessages(dataStream);
335
+ } catch {
336
+ // Stream closed or parse error
337
+ }
338
+ },
339
+ );
340
+ }
341
+
342
+ private _stderrChunks: string[] = [];
343
+
344
+ private _readStderr(dataStream: Gio.DataInputStream): void {
345
+ dataStream.read_line_async(
346
+ GLib.PRIORITY_DEFAULT,
347
+ null,
348
+ (_source: unknown, result: Gio.AsyncResult) => {
349
+ try {
350
+ const [line] = dataStream.read_line_finish_utf8(result);
351
+ if (line === null) {
352
+ // EOF — if we collected stderr output and worker errored, emit it
353
+ if (this._stderrChunks.length > 0) {
354
+ const stderrText = this._stderrChunks.join('\n');
355
+ // Only emit if no IPC error was already emitted
356
+ if (this.listenerCount('error') === 0) {
357
+ this.emit('error', new Error(stderrText));
358
+ }
359
+ }
360
+ return;
361
+ }
362
+ this._stderrChunks.push(line);
363
+ this._readStderr(dataStream);
364
+ } catch {
365
+ // Stream closed
366
+ }
367
+ },
368
+ );
369
+ }
370
+
371
+ private _onExit(): void {
372
+ if (this._exited) return;
373
+ this._exited = true;
374
+
375
+ const exitCode = this._subprocess?.get_if_exited()
376
+ ? this._subprocess.get_exit_status()
377
+ : 1;
378
+
379
+ this._cleanup();
380
+ this.emit('exit', exitCode);
381
+ }
382
+
383
+ private _cleanup(): void {
384
+ if (this._bootstrapFile) {
385
+ try { this._bootstrapFile.delete(null); } catch {}
386
+ this._bootstrapFile = null;
387
+ }
388
+ if (this._stdinPipe) {
389
+ try { this._stdinPipe.close(null); } catch {}
390
+ this._stdinPipe = null;
391
+ }
392
+ this._subprocess = null;
393
+ }
394
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "ESNext",
4
+ "target": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "types": [
7
+ "node"
8
+ ],
9
+ "experimentalDecorators": true,
10
+ "emitDeclarationOnly": true,
11
+ "declaration": true,
12
+ "allowImportingTsExtensions": true,
13
+ "outDir": "lib",
14
+ "rootDir": "src",
15
+ "declarationDir": "lib/types",
16
+ "composite": true,
17
+ "skipLibCheck": true,
18
+ "allowJs": true,
19
+ "checkJs": false,
20
+ "strict": false
21
+ },
22
+ "include": [
23
+ "src/**/*.ts"
24
+ ],
25
+ "exclude": [
26
+ "src/test.ts",
27
+ "src/test.mts",
28
+ "src/**/*.spec.ts",
29
+ "src/**/*.spec.mts"
30
+ ]
31
+ }