@oh-my-pi/pi-utils 6.8.1 → 6.8.2

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/ptree.ts +136 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-utils",
3
- "version": "6.8.1",
3
+ "version": "6.8.2",
4
4
  "description": "Shared utilities for pi packages",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
package/src/ptree.ts CHANGED
@@ -16,6 +16,59 @@ const isWindows = process.platform === "win32";
16
16
  // Set of live children for managed termination/cleanup on shutdown.
17
17
  const managedChildren = new Set<PipedSubprocess>();
18
18
 
19
+ class AsyncQueue<T> {
20
+ #items: T[] = [];
21
+ #resolvers: Array<(result: IteratorResult<T>) => void> = [];
22
+ #closed = false;
23
+
24
+ push(item: T): void {
25
+ if (this.#closed) return;
26
+ const resolver = this.#resolvers.shift();
27
+ if (resolver) {
28
+ resolver({ value: item, done: false });
29
+ return;
30
+ }
31
+ this.#items.push(item);
32
+ }
33
+
34
+ close(): void {
35
+ if (this.#closed) return;
36
+ this.#closed = true;
37
+ while (this.#resolvers.length > 0) {
38
+ const resolver = this.#resolvers.shift();
39
+ if (resolver) {
40
+ resolver({ value: undefined, done: true });
41
+ }
42
+ }
43
+ }
44
+
45
+ async next(): Promise<IteratorResult<T>> {
46
+ if (this.#items.length > 0) {
47
+ return { value: this.#items.shift() as T, done: false };
48
+ }
49
+ if (this.#closed) {
50
+ return { value: undefined, done: true };
51
+ }
52
+ return await new Promise<IteratorResult<T>>((resolve) => {
53
+ this.#resolvers.push(resolve);
54
+ });
55
+ }
56
+ }
57
+
58
+ function createProcessStream(queue: AsyncQueue<Uint8Array>): ReadableStream<Uint8Array> {
59
+ const stream = new ReadableStream<Uint8Array>({
60
+ pull: async (controller) => {
61
+ const result = await queue.next();
62
+ if (result.done) {
63
+ controller.close();
64
+ return;
65
+ }
66
+ controller.enqueue(result.value);
67
+ },
68
+ });
69
+ return stream;
70
+ }
71
+
19
72
  /**
20
73
  * Kill a child process and its descendents.
21
74
  * - Windows: uses taskkill for tree and forceful kill (/T /F)
@@ -71,8 +124,8 @@ function registerManaged(child: PipedSubprocess): void {
71
124
  });
72
125
  }
73
126
 
74
- // A Bun subprocess with stdin=Writable, stdout/stderr=pipe (for tracking/cleanup).
75
- type PipedSubprocess = Subprocess<"pipe" | null, "pipe", "pipe">;
127
+ // A Bun subprocess with stdin=Writable/ignore, stdout/stderr=pipe (for tracking/cleanup).
128
+ type PipedSubprocess = Subprocess<"pipe" | "ignore" | null, "pipe", "pipe">;
76
129
 
77
130
  /**
78
131
  * ChildProcess wraps a managed subprocess, capturing output, errors, and providing
@@ -82,8 +135,11 @@ export class ChildProcess {
82
135
  #proc: PipedSubprocess;
83
136
  #detached = false;
84
137
  #nothrow = false;
85
- #stderrTee: ReadableStream<Uint8Array<ArrayBuffer>>;
86
138
  #stderrBuffer = "";
139
+ #stdoutQueue = new AsyncQueue<Uint8Array>();
140
+ #stderrQueue = new AsyncQueue<Uint8Array>();
141
+ #stdoutStream?: ReadableStream<Uint8Array>;
142
+ #stderrStream?: ReadableStream<Uint8Array>;
87
143
  #exitReason?: Exception;
88
144
  #exitReasonPending?: Exception;
89
145
  #exited: Promise<void>;
@@ -92,23 +148,75 @@ export class ChildProcess {
92
148
  constructor(proc: PipedSubprocess) {
93
149
  registerManaged(proc);
94
150
 
95
- const [left, right] = proc.stderr.tee();
96
- this.#stderrTee = right;
151
+ const exitSettled = proc.exited.then(
152
+ () => {},
153
+ () => {},
154
+ );
155
+
156
+ // Capture stdout at all times. Close the passthrough when the process exits.
157
+ void (async () => {
158
+ const reader = proc.stdout.getReader();
159
+ try {
160
+ while (true) {
161
+ const result = await Promise.race([
162
+ reader.read(),
163
+ exitSettled.then(() => ({ done: true, value: undefined as Uint8Array | undefined })),
164
+ ]);
165
+ if (result.done) break;
166
+ if (!result.value) continue;
167
+ this.#stdoutQueue.push(result.value);
168
+ }
169
+ } catch {
170
+ // ignore
171
+ } finally {
172
+ try {
173
+ await reader.cancel();
174
+ } catch {}
175
+ try {
176
+ reader.releaseLock();
177
+ } catch {}
178
+ this.#stdoutQueue.close();
179
+ }
180
+ })().catch(() => {
181
+ this.#stdoutQueue.close();
182
+ });
97
183
 
98
184
  // Capture stderr at all times, with a capped buffer for errors.
99
185
  const decoder = new TextDecoder();
100
186
  void (async () => {
101
- for await (const chunk of left) {
102
- this.#stderrBuffer += decoder.decode(chunk, { stream: true });
187
+ const reader = proc.stderr.getReader();
188
+ try {
189
+ while (true) {
190
+ const result = await Promise.race([
191
+ reader.read(),
192
+ exitSettled.then(() => ({ done: true, value: undefined as Uint8Array | undefined })),
193
+ ]);
194
+ if (result.done) break;
195
+ if (!result.value) continue;
196
+ this.#stderrQueue.push(result.value);
197
+ this.#stderrBuffer += decoder.decode(result.value, { stream: true });
198
+ if (this.#stderrBuffer.length > NonZeroExitError.MAX_TRACE) {
199
+ this.#stderrBuffer = this.#stderrBuffer.slice(-NonZeroExitError.MAX_TRACE);
200
+ }
201
+ }
202
+ } catch {
203
+ // ignore
204
+ } finally {
205
+ this.#stderrBuffer += decoder.decode();
103
206
  if (this.#stderrBuffer.length > NonZeroExitError.MAX_TRACE) {
104
207
  this.#stderrBuffer = this.#stderrBuffer.slice(-NonZeroExitError.MAX_TRACE);
105
208
  }
209
+ try {
210
+ await reader.cancel();
211
+ } catch {}
212
+ try {
213
+ reader.releaseLock();
214
+ } catch {}
215
+ this.#stderrQueue.close();
106
216
  }
107
- this.#stderrBuffer += decoder.decode();
108
- if (this.#stderrBuffer.length > NonZeroExitError.MAX_TRACE) {
109
- this.#stderrBuffer = this.#stderrBuffer.slice(-NonZeroExitError.MAX_TRACE);
110
- }
111
- })().catch(() => {});
217
+ })().catch(() => {
218
+ this.#stderrQueue.close();
219
+ });
112
220
 
113
221
  const { promise, resolve } = Promise.withResolvers<Exception | undefined>();
114
222
 
@@ -152,11 +260,17 @@ export class ChildProcess {
152
260
  get stdin(): FileSink | undefined {
153
261
  return this.#proc.stdin;
154
262
  }
155
- get stdout(): ReadableStream<Uint8Array<ArrayBuffer>> {
156
- return this.#proc.stdout;
263
+ get stdout(): ReadableStream<Uint8Array> {
264
+ if (!this.#stdoutStream) {
265
+ this.#stdoutStream = createProcessStream(this.#stdoutQueue);
266
+ }
267
+ return this.#stdoutStream;
157
268
  }
158
- get stderr(): ReadableStream<Uint8Array<ArrayBuffer>> {
159
- return this.#stderrTee;
269
+ get stderr(): ReadableStream<Uint8Array> {
270
+ if (!this.#stderrStream) {
271
+ this.#stderrStream = createProcessStream(this.#stderrQueue);
272
+ }
273
+ return this.#stderrStream;
160
274
  }
161
275
 
162
276
  /**
@@ -227,7 +341,7 @@ export class ChildProcess {
227
341
  async blob() {
228
342
  const { promise, resolve, reject } = Promise.withResolvers<Blob>();
229
343
 
230
- const blob = this.#proc.stdout.blob();
344
+ const blob = this.stdout.blob();
231
345
  if (!this.#nothrow) {
232
346
  this.#exited.catch((ex: Exception) => {
233
347
  reject(ex);
@@ -343,7 +457,10 @@ export class TimeoutError extends AbortError {
343
457
  /**
344
458
  * Options for cspawn (child spawn). Always pipes stdout/stderr, allows signal.
345
459
  */
346
- type ChildSpawnOptions = Omit<Spawn.SpawnOptions<"pipe" | null, "pipe", "pipe">, "stdout" | "stderr"> & {
460
+ type ChildSpawnOptions = Omit<
461
+ Spawn.SpawnOptions<"pipe" | "ignore" | Buffer | null, "pipe", "pipe">,
462
+ "stdout" | "stderr"
463
+ > & {
347
464
  signal?: AbortSignal;
348
465
  };
349
466
 
@@ -355,6 +472,7 @@ type ChildSpawnOptions = Omit<Spawn.SpawnOptions<"pipe" | null, "pipe", "pipe">,
355
472
  export function cspawn(cmd: string[], options?: ChildSpawnOptions): ChildProcess {
356
473
  const { timeout, ...rest } = options ?? {};
357
474
  const child = spawn(cmd, {
475
+ stdin: "ignore",
358
476
  ...rest,
359
477
  stdout: "pipe",
360
478
  stderr: "pipe",