@oh-my-pi/pi-utils 15.12.3 → 15.12.4

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 CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.12.4] - 2026-06-13
6
+
7
+ ### Fixed
8
+
9
+ - Fixed abortable stream wrappers to cancel the source stream on abort, so timeout watchdogs release upstream HTTP bodies instead of only stopping the local reader.
10
+
5
11
  ## [15.12.0] - 2026-06-12
6
12
 
7
13
  ### Added
@@ -4,6 +4,11 @@ export declare class AbortError extends Error {
4
4
  /**
5
5
  * Creates an abortable stream from a given stream and signal.
6
6
  *
7
+ * Unlike `stream.pipeThrough(..., { signal })`, this explicitly cancels the
8
+ * source reader when the signal aborts. That propagates HTTP-client disconnects
9
+ * and stream watchdog timeouts all the way to the backend request instead of
10
+ * only stopping the local consumer.
11
+ *
7
12
  * @param stream - The stream to make abortable
8
13
  * @param signal - The signal to abort the stream
9
14
  * @returns The abortable stream
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-utils",
4
- "version": "15.12.3",
4
+ "version": "15.12.4",
5
5
  "description": "Shared utilities for pi packages",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -31,7 +31,7 @@
31
31
  "fmt": "biome format --write ."
32
32
  },
33
33
  "dependencies": {
34
- "@oh-my-pi/pi-natives": "15.12.3",
34
+ "@oh-my-pi/pi-natives": "15.12.4",
35
35
  "beautiful-mermaid": "^1.1.3",
36
36
  "handlebars": "^4.7.9",
37
37
  "winston": "^3.19.0",
package/src/abortable.ts CHANGED
@@ -10,16 +10,101 @@ export class AbortError extends Error {
10
10
  }
11
11
  }
12
12
 
13
+ type AbortableStreamReadResult<T> = { done: true; value?: T } | { done: false; value: T };
14
+
15
+ interface AbortableStreamReader<T> {
16
+ read(): Promise<AbortableStreamReadResult<T>>;
17
+ cancel(reason?: unknown): Promise<void>;
18
+ releaseLock(): void;
19
+ }
20
+
13
21
  /**
14
22
  * Creates an abortable stream from a given stream and signal.
15
23
  *
24
+ * Unlike `stream.pipeThrough(..., { signal })`, this explicitly cancels the
25
+ * source reader when the signal aborts. That propagates HTTP-client disconnects
26
+ * and stream watchdog timeouts all the way to the backend request instead of
27
+ * only stopping the local consumer.
28
+ *
16
29
  * @param stream - The stream to make abortable
17
30
  * @param signal - The signal to abort the stream
18
31
  * @returns The abortable stream
19
32
  */
20
33
  export function createAbortableStream<T>(stream: ReadableStream<T>, signal?: AbortSignal): ReadableStream<T> {
21
34
  if (!signal) return stream;
22
- return stream.pipeThrough(new TransformStream<T, T>(), { signal });
35
+ let reader: AbortableStreamReader<T> | undefined;
36
+ let closed = false;
37
+ let onAbort: (() => void) | undefined;
38
+
39
+ const cleanup = () => {
40
+ if (onAbort) signal.removeEventListener("abort", onAbort);
41
+ onAbort = undefined;
42
+ const currentReader = reader;
43
+ reader = undefined;
44
+ try {
45
+ currentReader?.releaseLock();
46
+ } catch {}
47
+ };
48
+
49
+ const cancelReader = (reason: unknown): Promise<void> => {
50
+ if (closed) return Promise.resolve();
51
+ closed = true;
52
+ const currentReader = reader;
53
+ reader = undefined;
54
+ if (onAbort) signal.removeEventListener("abort", onAbort);
55
+ onAbort = undefined;
56
+ if (!currentReader) return Promise.resolve();
57
+ return currentReader
58
+ .cancel(reason)
59
+ .catch(() => {})
60
+ .finally(() => {
61
+ try {
62
+ currentReader.releaseLock();
63
+ } catch {}
64
+ });
65
+ };
66
+
67
+ return new ReadableStream<T>({
68
+ start(controller) {
69
+ reader = stream.getReader();
70
+ onAbort = () => {
71
+ void cancelReader(signal.reason);
72
+ try {
73
+ controller.error(new AbortError(signal));
74
+ } catch {}
75
+ };
76
+ if (signal.aborted) {
77
+ onAbort();
78
+ return;
79
+ }
80
+ signal.addEventListener("abort", onAbort, { once: true });
81
+ void (async () => {
82
+ try {
83
+ for (;;) {
84
+ const currentReader = reader;
85
+ if (!currentReader) return;
86
+ const { value, done } = await currentReader.read();
87
+ if (closed) return;
88
+ if (done) {
89
+ closed = true;
90
+ cleanup();
91
+ controller.close();
92
+ return;
93
+ }
94
+ controller.enqueue(value);
95
+ }
96
+ } catch (error) {
97
+ if (closed) return;
98
+ closed = true;
99
+ cleanup();
100
+ controller.error(signal.aborted ? new AbortError(signal) : error);
101
+ }
102
+ })();
103
+ },
104
+ cancel(reason) {
105
+ return cancelReader(reason);
106
+ },
107
+ });
23
108
  }
24
109
 
25
110
  /**