@gajae-code/utils 0.12.11 → 0.12.13

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.
@@ -10,9 +10,10 @@ export declare function isKnownSinkPeerClosedError(error: unknown): boolean;
10
10
  */
11
11
  export declare function isBrokenPipeError(error: unknown): boolean;
12
12
  /**
13
- * A classifier for process-level stdout `EPIPE` errors. Its direct-write
14
- * evidence is private to each factory instance, so only the owner that
15
- * intercepted `process.stdout.write` can mark an error for this classifier.
13
+ * A classifier for process-level stdout peer-closed errors (`EPIPE`, `EIO`,
14
+ * `EBADF`). Its direct-write evidence is private to each factory instance, so
15
+ * only the owner that intercepted `process.stdout.write` can mark an error
16
+ * for this classifier.
16
17
  */
17
18
  export interface ProcessStdoutEpipeClassifier {
18
19
  markDirectProcessStdoutWriteError(error: unknown): void;
@@ -22,13 +22,16 @@ export interface FetchWithRetryOptions extends RequestInit {
22
22
  /**
23
23
  * Per-delay cap. Server-provided `Retry-After` hints exceeding this return
24
24
  * the current response immediately — caller deals with the `!response.ok`.
25
- * Default `60_000`.
25
+ * Scheduled values are also capped at the platform timer ceiling. Default
26
+ * `60_000`.
26
27
  */
27
28
  maxDelayMs?: number;
28
29
  /**
29
30
  * Fallback delay schedule when no server hint is present. Number, array
30
31
  * (indexed by attempt, clamped to last), or function. Default exponential
31
- * `500ms * 2 ** attempt` capped at `maxDelayMs`.
32
+ * `500ms * 2 ** attempt` capped at `maxDelayMs` and the platform timer
33
+ * ceiling. Values that remain negative or non-finite after capping retry
34
+ * immediately.
32
35
  */
33
36
  defaultDelayMs?: number | readonly number[] | ((attempt: number) => number);
34
37
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/utils",
4
- "version": "0.12.11",
4
+ "version": "0.12.13",
5
5
  "description": "Shared utilities for pi packages",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo",
@@ -31,7 +31,7 @@
31
31
  "fmt": "biome format --write ."
32
32
  },
33
33
  "dependencies": {
34
- "@gajae-code/natives": "0.12.11",
34
+ "@gajae-code/natives": "0.12.13",
35
35
  "beautiful-mermaid": "^1.1.3",
36
36
  "handlebars": "^4.7.9",
37
37
  "winston": "^3.19.0",
@@ -9,6 +9,16 @@
9
9
  import * as fs from "node:fs";
10
10
 
11
11
  const KNOWN_SINK_PEER_CLOSED_CODES = new Set(["EPIPE", "ERR_STREAM_DESTROYED"]);
12
+ /**
13
+ * Errno codes that mean the process's own stdout sink has gone away because
14
+ * the peer (terminal emulator, pager, pipe reader, or pty) was torn down. On
15
+ * macOS a torn-down pty slave produces `EIO`; a closed fd produces `EBADF`;
16
+ * a closed pipe produces `EPIPE`. All three are the same benign condition:
17
+ * the terminal disconnected.
18
+ *
19
+ * Mirrors `CLOSED_STDERR_ERROR_CODES` in `safe-stderr.ts`; keep in sync.
20
+ */
21
+ const PROCESS_STDOUT_PEER_CLOSED_CODES = new Set(["EIO", "EPIPE", "EBADF"]);
12
22
 
13
23
  type ErrorProperty = "code" | "fd" | "syscall";
14
24
 
@@ -70,9 +80,10 @@ export function isBrokenPipeError(error: unknown): boolean {
70
80
  }
71
81
 
72
82
  /**
73
- * A classifier for process-level stdout `EPIPE` errors. Its direct-write
74
- * evidence is private to each factory instance, so only the owner that
75
- * intercepted `process.stdout.write` can mark an error for this classifier.
83
+ * A classifier for process-level stdout peer-closed errors (`EPIPE`, `EIO`,
84
+ * `EBADF`). Its direct-write evidence is private to each factory instance, so
85
+ * only the owner that intercepted `process.stdout.write` can mark an error
86
+ * for this classifier.
76
87
  */
77
88
  export interface ProcessStdoutEpipeClassifier {
78
89
  markDirectProcessStdoutWriteError(error: unknown): void;
@@ -109,7 +120,8 @@ export function createProcessStdoutEpipeClassifier(): ProcessStdoutEpipeClassifi
109
120
  if (!isObjectLike(error)) return false;
110
121
 
111
122
  const code = readErrorProperty(error, "code");
112
- if (!code.available || code.value !== "EPIPE") return false;
123
+ if (!code.available || typeof code.value !== "string" || !PROCESS_STDOUT_PEER_CLOSED_CODES.has(code.value))
124
+ return false;
113
125
 
114
126
  if (directProcessStdoutWriteErrors.has(error)) return true;
115
127
 
@@ -81,13 +81,16 @@ export interface FetchWithRetryOptions extends RequestInit {
81
81
  /**
82
82
  * Per-delay cap. Server-provided `Retry-After` hints exceeding this return
83
83
  * the current response immediately — caller deals with the `!response.ok`.
84
- * Default `60_000`.
84
+ * Scheduled values are also capped at the platform timer ceiling. Default
85
+ * `60_000`.
85
86
  */
86
87
  maxDelayMs?: number;
87
88
  /**
88
89
  * Fallback delay schedule when no server hint is present. Number, array
89
90
  * (indexed by attempt, clamped to last), or function. Default exponential
90
- * `500ms * 2 ** attempt` capped at `maxDelayMs`.
91
+ * `500ms * 2 ** attempt` capped at `maxDelayMs` and the platform timer
92
+ * ceiling. Values that remain negative or non-finite after capping retry
93
+ * immediately.
91
94
  */
92
95
  defaultDelayMs?: number | readonly number[] | ((attempt: number) => number);
93
96
  /**
@@ -106,6 +109,9 @@ export interface FetchWithRetryOptions extends RequestInit {
106
109
 
107
110
  const DEFAULT_MAX_DELAY_MS = 60_000;
108
111
  const DEFAULT_MAX_ATTEMPTS = 5;
112
+ // Node-compatible timers coerce larger delays to 1 ms. Bun follows the same
113
+ // signed 32-bit boundary, so every scheduler input must stay at or below it.
114
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
109
115
 
110
116
  /**
111
117
  * Fetch with bounded retries and sensible defaults. Retries on any
@@ -142,7 +148,8 @@ export async function fetchWithRetry(
142
148
  if (signal?.aborted) throw new Error("Request was aborted");
143
149
  const wrapped = wrapNetworkError(error);
144
150
  if (attempt + 1 >= maxAttempts) throw wrapped;
145
- await scheduler.wait(resolveDefaultDelay(defaultDelayMs, attempt, maxDelayMs), { signal });
151
+ const delayMs = normalizeRetryDelay(resolveDefaultDelay(defaultDelayMs, attempt), maxDelayMs);
152
+ await scheduler.wait(delayMs, { signal });
146
153
  continue;
147
154
  }
148
155
 
@@ -152,7 +159,7 @@ export async function fetchWithRetry(
152
159
  const hint = extractRetryHint(response, await response.clone().text());
153
160
  if (hint !== undefined && hint > maxDelayMs) return response;
154
161
 
155
- const delayMs = Math.min(hint ?? resolveDefaultDelay(defaultDelayMs, attempt, maxDelayMs), maxDelayMs);
162
+ const delayMs = normalizeRetryDelay(hint ?? resolveDefaultDelay(defaultDelayMs, attempt), maxDelayMs);
156
163
  void response.body?.cancel().catch(() => undefined);
157
164
  await scheduler.wait(delayMs, { signal });
158
165
  }
@@ -184,15 +191,16 @@ function wrapNetworkError(error: unknown): Error {
184
191
  return new Error(String(error));
185
192
  }
186
193
 
187
- function resolveDefaultDelay(
188
- option: FetchWithRetryOptions["defaultDelayMs"],
189
- attempt: number,
190
- maxDelayMs: number,
191
- ): number {
192
- if (option === undefined) return Math.min(500 * 2 ** attempt, maxDelayMs);
193
- if (typeof option === "number") return Math.min(option, maxDelayMs);
194
- if (typeof option === "function") return Math.min(option(attempt), maxDelayMs);
195
- return Math.min(option[Math.min(attempt, option.length - 1)] ?? 0, maxDelayMs);
194
+ function resolveDefaultDelay(option: FetchWithRetryOptions["defaultDelayMs"], attempt: number): number {
195
+ if (option === undefined) return 500 * 2 ** attempt;
196
+ if (typeof option === "number") return option;
197
+ if (typeof option === "function") return option(attempt);
198
+ return option[Math.min(attempt, option.length - 1)] ?? 0;
199
+ }
200
+
201
+ function normalizeRetryDelay(delayMs: number, maxDelayMs: number): number {
202
+ const cappedDelayMs = Math.min(delayMs, maxDelayMs, MAX_TIMER_DELAY_MS);
203
+ return Number.isFinite(cappedDelayMs) && cappedDelayMs >= 0 ? cappedDelayMs : 0;
196
204
  }
197
205
 
198
206
  /**