Skip to content

Commit 7734b2a

Browse files
chore(OUT-3544): trim withRetry classifier JSDoc to essentials
The JSDoc on `isRetryableError` had grown to 37 lines — a bullet list of error shapes the code itself documents, plus a verbose retry-nesting paragraph. Keep only the load-bearing pieces: the idempotent gate contract and the nesting hazard one-liner. Tighten the `RetryOptions` field JSDoc and the inline strict-mode comment the same way. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e56615a commit 7734b2a

1 file changed

Lines changed: 14 additions & 44 deletions

File tree

src/app/api/core/utils/withRetry.ts

Lines changed: 14 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -30,54 +30,25 @@ const RETRYABLE_ERROR_NAMES: ReadonlySet<string> = new Set([
3030

3131
export type RetryOptions = {
3232
/**
33-
* Set to false for non-idempotent writes whose upstream lacks a request-key
34-
* dedupe primitive (QBO create/update/void/delete endpoints). In that mode
35-
* the classifier retries ONLY on 429 (explicit server-side reject without
36-
* commit) and explicit `RetryableError.retry === true`. Everything else —
37-
* 5xx, network errors, AbortSignal timeouts — is treated as permanent,
38-
* because each of those can occur after the server has already committed
39-
* and a replay would duplicate the write. Defaults to true (read-path /
40-
* safe-to-replay behavior).
33+
* False for non-idempotent writes (QBO create/update/void/delete) whose
34+
* upstream has no request-key dedupe. In strict mode the classifier
35+
* retries only on 429 and explicit `RetryableError.retry === true`;
36+
* 5xx, network errors, and AbortSignal timeouts all bubble. Defaults
37+
* to true (safe-to-replay reads).
4138
*/
4239
idempotent?: boolean
4340
}
4441

4542
/**
46-
* Centralized classifier for whether an error should trigger a retry.
47-
* Exported so it can be unit-tested independent of pRetry's timer plumbing.
43+
* Classifies whether an error should trigger a retry. 429 and explicit
44+
* `RetryableError.retry === true` retry in both modes. Strict mode
45+
* (`options.idempotent === false`) short-circuits past 429: 5xx, network
46+
* codes, AbortSignal timeouts, and undici fetch-failed envelopes are all
47+
* treated as possibly-after-commit and never replayed.
4848
*
49-
* Retry-set composition is gated by `options.idempotent` (default true):
50-
* - `RetryableError` is honored in both modes (explicit caller signal).
51-
* - `ALWAYS_RETRY_STATUSES` (429) fires in both modes.
52-
* - In strict mode (`idempotent: false`) everything past the 429 check
53-
* short-circuits to non-retryable: 5xx, network codes, AbortSignal
54-
* timeouts, and undici fetch-failed envelopes are all treated as
55-
* possibly-after-commit and never replayed.
56-
*
57-
* The error shapes inspected here come from different layers; a single
58-
* unified type doesn't exist, which is why the input is `unknown` and
59-
* each field is checked defensively:
60-
*
61-
* - `RetryableError` (ours, src/utils/error.ts) — explicit retry flag.
62-
* - `HttpFetchError` (ours) and Copilot SDK's `StatusableError` —
63-
* `status: number` set after a non-2xx response was received.
64-
* - undici (Node fetch) network failures — thrown as
65-
* `TypeError: fetch failed` with the underlying error on `.cause`
66-
* (e.g. `{ code: 'ECONNRESET' }`). No HTTP response was ever built,
67-
* so there is no status to inspect.
68-
* - `AbortSignal.timeout()` rejects with a `DOMException` whose
69-
* `name` is `'TimeoutError'` (retryable). `AbortController.abort()`
70-
* produces `'AbortError'` and is NOT retried (deliberate cancellation).
71-
* - Top-level `error.code` is checked defensively for legacy Node
72-
* error paths; in current Node fetch the code lives under `.cause`.
73-
*
74-
* Retry-nesting hazard: do not call a `withRetry`-wrapped function from
75-
* inside another `withRetry`-wrapped function. With the broadened retry
76-
* set, worst-case wait is `outer × inner × per_call_timeout`, which can
77-
* blow past the 300s webhook execution budget. Inside `IntuitAPI._*`
78-
* methods that are themselves wrapped at the public level (see exports
79-
* at the bottom of `src/utils/intuitAPI.ts`), call the unwrapped `_*`
80-
* counterparts directly (e.g. `this._customQuery`, not `this.customQuery`).
49+
* Retry-nesting hazard: don't call a wrapped function from inside another.
50+
* Inside `IntuitAPI._*` methods call the unwrapped `_*` counterparts
51+
* (e.g. `this._customQuery`, not `this.customQuery`).
8152
*/
8253
export const isRetryableError = (
8354
error: unknown,
@@ -100,8 +71,7 @@ export const isRetryableError = (
10071
if (typeof err.status === 'number' && ALWAYS_RETRY_STATUSES.has(err.status))
10172
return true
10273

103-
// Non-idempotent mode: nothing past this point is safe — 5xx, network blips,
104-
// and timeouts can all occur after the upstream has committed.
74+
// Strict mode: nothing past this point is post-commit-safe.
10575
if (!idempotent) return false
10676

10777
if (

0 commit comments

Comments
 (0)