@hunt-labs/bytekit-cli 0.3.0 → 0.6.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/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ByteKit } from '@hunt-labs/bytekit-sdk';
1
+ import { ByteKit, ByteKitConnectionError } from '@hunt-labs/bytekit-sdk';
2
2
  export interface GlobalOptions {
3
3
  key?: string | undefined;
4
4
  baseUrl?: string | undefined;
@@ -22,6 +22,24 @@ export declare const MIN_RETRY_AFTER_MS = 1000;
22
22
  export declare const MAX_RETRY_AFTER_MS = 60000;
23
23
  /** Exactly one retry. A second 429 is terminal — no loop. */
24
24
  export declare const MAX_RETRIES = 1;
25
+ /**
26
+ * Cap on a best-effort, discard-only body drain (#2587 cr2-1). Independent of the per-attempt
27
+ * abort budget: the 429 drain below (`response.text().catch(() => {})`) exists ONLY so undici can
28
+ * return the socket to the connection pool — nothing downstream reads the drained bytes — but
29
+ * binding that read to the FULL per-attempt budget (120 s default, or, under `timeoutMs <= 0`,
30
+ * to no bound at all) let a stalled 429 body burn the whole budget, or hang forever, before a
31
+ * retry that needs nothing from that body. Short enough that a legitimately stalled 429 body costs
32
+ * the retry very little wall-clock time.
33
+ *
34
+ * #2587 cr3-2: past this cap, the read is not merely abandoned — {@link drainBestEffort} actively
35
+ * ABORTS its underlying request. Resolving the race without aborting anything (the earlier
36
+ * cr2-1 shape) let the retry proceed on schedule while the abandoned `response.text()` kept
37
+ * reading against a live socket forever, which — because the CLI exits via a soft
38
+ * `process.exitCode` (#2544) rather than `process.exit()` — kept the whole process alive
39
+ * indefinitely. `.catch`-guarding the read only made it safe to abandon the RESULT; it never made
40
+ * abandoning the underlying READ safe.
41
+ */
42
+ export declare const DRAIN_TIMEOUT_MS = 2000;
25
43
  /**
26
44
  * Default CLIENT-SIDE abort budget for a single fetch attempt, in ms.
27
45
  *
@@ -33,6 +51,49 @@ export declare const MAX_RETRIES = 1;
33
51
  * exists.
34
52
  */
35
53
  export declare const DEFAULT_TIMEOUT_MS = 120000;
54
+ /**
55
+ * A per-attempt abort budget: an `AbortController`, optionally fired by a `setTimeout`.
56
+ *
57
+ * The budget spans the WHOLE attempt — time-to-headers **and** the response body read (#2546).
58
+ * Arming it only until headers arrive left `response.text()` / `response.arrayBuffer()` unbounded,
59
+ * so a server that answered with headers and then stalled its body hung the CLI forever.
60
+ *
61
+ * The controller ALWAYS exists, even when `ms <= 0` — only its own timeout timer is conditional
62
+ * on `ms > 0`. `sendWithRetry`'s 429-drain cap ({@link DRAIN_TIMEOUT_MS}, via the returned
63
+ * `abort()`) needs a live controller to tear down an abandoned body read regardless of whether the
64
+ * OVERALL per-attempt budget is enabled — `ms <= 0` must still mean "no full-attempt timeout", not
65
+ * "no controller to abort a stalled drain with" (#2587 cr3-2: the earlier drain fix abandoned the
66
+ * read entirely rather than aborting it, so the socket — and the event loop — stayed alive
67
+ * indefinitely, worse than the pre-fix baseline it replaced).
68
+ *
69
+ * Deliberately NOT `AbortSignal.timeout` (which the SDK prefers): that schedules on a timer the
70
+ * test runner's fake timers cannot advance, so the transport's timeout would be untestable under
71
+ * `vi.useFakeTimers()`. CLI requests are short-lived and single-shot, so the GC advantage
72
+ * `AbortSignal.timeout` buys the long-running SDK does not apply here. The timer is `unref`'d so a
73
+ * pending budget never keeps the process alive, and `cleanup()` clears it on every exit path.
74
+ *
75
+ * Exported so `lib/download-artifact.ts` can reuse the same budget for the artifact-download path
76
+ * (#2587) rather than growing a second timeout implementation — that path sits entirely outside
77
+ * `sendWithRetry` (it is not a JSON/raw API call), so it would otherwise never inherit this bound.
78
+ */
79
+ export declare function createAttemptBudget(ms: number): {
80
+ signal: AbortSignal;
81
+ cleanup: () => void;
82
+ abort: (reason: unknown) => void;
83
+ };
84
+ /**
85
+ * Map a `fetch` or body-read rejection (no usable HTTP payload was produced) onto the SDK's
86
+ * `ByteKitConnectionError` taxonomy, so a CLI failure carries the same `{name, code, cause}` a
87
+ * direct SDK call would.
88
+ *
89
+ * `timedOut` is true when this attempt's own budget fired the abort, which is the reliable
90
+ * discriminator (undici rejects with the signal's reason — or a wrapper carrying it, see
91
+ * {@link abortNameOf} — whose `name` we also inspect as a fallback for a caller-style AbortError).
92
+ *
93
+ * Exported so `lib/download-artifact.ts` reuses the same taxonomy for the artifact-download path
94
+ * (#2587) instead of hand-rolling a second error mapping.
95
+ */
96
+ export declare function toConnectionError(err: unknown, method: string, path: string, timedOut: boolean): ByteKitConnectionError;
36
97
  /**
37
98
  * Parse a `Retry-After` header into a clamped millisecond delay.
38
99
  *
@@ -42,19 +103,64 @@ export declare const DEFAULT_TIMEOUT_MS = 120000;
42
103
  */
43
104
  export declare function parseRetryAfterMs(raw: string | null): number;
44
105
  /**
45
- * Build the CLI's HTTP transport: the SDK's `request` contract, plus a single bounded retry on 429.
46
- *
47
- * Why the CLI owns the `fetch` instead of wrapping the SDK's `request()`: the SDK throws a
48
- * header-less `ByteKitError` and discards the `Response`, so a delegating wrapper can see
49
- * `status === 429` but can never read `Retry-After`. Owning the fetch is the only way to honor the
50
- * server's advertised backoff without editing the SDK.
106
+ * Build the CLI's JSON transport: the SDK's `request` contract, plus the single bounded 429 retry.
51
107
  *
52
108
  * Every SDK resource dereferences `this.client.request` at call time, so installing this as an own
53
- * property of the client (see {@link buildClient}) routes *all* commands through it from one seam.
109
+ * property of the client (see {@link buildClient}) routes all JSON commands through it from one
110
+ * seam. `/v1/fetch` does NOT ride this transport — it rides `requestRaw` (see
111
+ * {@link createRetryingRequestRaw}); `/v1/fetch/bulk` is a JSON job envelope and does.
54
112
  *
55
- * Retries apply to 429 only. Non-429 statuses and network rejections propagate untouched.
113
+ * DECISION (#2586 dod-4): the 4th `RequestOptions` parameter of the SDK's contract is accepted
114
+ * here — so the signature matches `ByteKit['request']` exactly, closing the gap where a caller-typo
115
+ * of a 5th positional argument would silently no-op instead of a compile error — but deliberately
116
+ * NOT forwarded into `sendWithRetry`. Two reasons: (1) no CLI command constructs a `RequestOptions`
117
+ * today (no `--timeout` flag exists yet, and `signal` has no CLI-level source), so the parameter is
118
+ * always `undefined` in practice; (2) the CLI already owns a per-attempt abort budget with its own
119
+ * semantics (`createAttemptBudget`, `timeoutMs` above) that would need explicit composition with a
120
+ * caller `signal` to honor `RequestOptions` correctly (the SDK's `composeSignals`) — inventing that
121
+ * unexercised composition here, with no caller able to reach it, would be undertested surface for no
122
+ * present behavior change. If a future CLI flag needs per-call overrides, wire it through
123
+ * `sendWithRetry`'s `timeoutMs` (already per-attempt) at that point, with its own tests.
56
124
  */
57
125
  export declare function createRetryingRequest(apiKey: string, baseUrl: string, timeoutMs?: number): ByteKit['request'];
126
+ /**
127
+ * Build the CLI's RAW transport: the SDK's `requestRaw` contract, plus the same bounded 429 retry.
128
+ *
129
+ * `/v1/fetch` returns the fetched content AS the body (HTML, markdown, or true binary) with its
130
+ * metadata in `X-Fetch-*` headers, so the SDK routes it through `requestRaw` rather than the JSON
131
+ * `request`. The CLI must override BOTH seams or fetch commands silently bypass the retry and the
132
+ * CLI's error taxonomy — the #2535 defect, where only `request` was replaced.
133
+ *
134
+ * The returned shape is the SDK's `requestRaw` contract verbatim (`{body, bytes, headers, status}`)
135
+ * so `FetchResource` builds an ordinary `FetchResult` from it and the CLI never forks fetch
136
+ * handling away from the SDK. `bytes` is byte-exact (binary-safe); `body` is the best-effort UTF-8
137
+ * decode, matching the SDK's own semantics.
138
+ *
139
+ * The 4th `RequestOptions` parameter is accepted but not forwarded — see the DECISION note on
140
+ * {@link createRetryingRequest}, which applies here verbatim. `FetchResource.create`/`get` DO thread
141
+ * a `requestOptions` argument through to this seam (unlike every other resource), but no CLI `fetch`
142
+ * command constructs one today (no `--timeout` flag), so it is still always `undefined` in practice.
143
+ */
144
+ export declare function createRetryingRequestRaw(apiKey: string, baseUrl: string, timeoutMs?: number): ByteKit['requestRaw'];
145
+ /**
146
+ * Build the CLI's header-carrying JSON transport: the SDK's `requestWithResponse` contract
147
+ * (`{data, headers, status}`, #2547), plus the same bounded 429 retry every other CLI transport
148
+ * gets.
149
+ *
150
+ * Unused by any CLI command today — `screenshots.createWithResponse` / `getWithResponse` and
151
+ * `sitemap.getWithResponse` all exist on the SDK surface but no command builds on them yet. It is
152
+ * still installed (see {@link buildClient}) rather than left un-overridden: the transport-coverage
153
+ * contract test (#2586 dod-3) derives its method list from the real SDK prototype, and leaving a
154
+ * transport method un-overridden is exactly how #2535 shipped broken — the first command that DOES
155
+ * call a `*WithResponse` resource method must not silently ride the bare SDK transport and skip the
156
+ * CLI's retry/timeout/error taxonomy.
157
+ *
158
+ * Same body-read shape as {@link createRetryingRequest} (204/empty → `undefined`, `JSON.parse`
159
+ * otherwise), with `headers` and `status` threaded through from the successful response alongside
160
+ * the parsed `data`. The 4th `RequestOptions` parameter is accepted but not forwarded — see the
161
+ * DECISION note on {@link createRetryingRequest}.
162
+ */
163
+ export declare function createRetryingRequestWithResponse(apiKey: string, baseUrl: string, timeoutMs?: number): ByteKit['requestWithResponse'];
58
164
  /**
59
165
  * Build an authenticated SDK client from global CLI options.
60
166
  *
@@ -63,7 +169,18 @@ export declare function createRetryingRequest(apiKey: string, baseUrl: string, t
63
169
  * baseUrl: --base-url flag → BYTEKIT_BASE_URL → DEFAULT_BASE_URL.
64
170
  * Exits with a descriptive error when no key is available.
65
171
  *
66
- * The returned client's `request` is replaced by the retrying transport, so every command inherits
67
- * the 429 retry from this one seam.
172
+ * The returned client's `request`, `requestRaw`, AND `requestWithResponse` are all replaced by the
173
+ * retrying transports, so every command inherits the 429 retry — JSON commands via `request`, the
174
+ * `/v1/fetch` content commands via `requestRaw`, and the header-carrying `*WithResponse` surface via
175
+ * `requestWithResponse`. Overriding only `request` is the #2535 defect: the SDK's fetch resource
176
+ * rides `requestRaw`, so fetch bypassed the retry and the CLI's error taxonomy entirely; leaving
177
+ * `requestWithResponse` un-overridden is the same class of defect (#2586) — it is unused by any CLI
178
+ * command TODAY, but a future command built on `screenshots.createWithResponse` /
179
+ * `sitemap.getWithResponse` would otherwise silently ride the bare SDK transport.
180
+ *
181
+ * `baseUrl` is normalized ONCE, here, before it reaches either the SDK constructor or the CLI's own
182
+ * transport builders below — the CLI's transports build their request URL from this local variable
183
+ * directly (they do not call the SDK's own normalized `send()`), so a trailing-slash flag would
184
+ * otherwise double-slash every request the CLI makes (#2586).
68
185
  */
69
186
  export declare function buildClient(opts: GlobalOptions): ByteKit;