@hunt-labs/bytekit-sdk 0.1.0 → 0.2.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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +171 -0
  3. package/dist/allowlist.d.ts +0 -1
  4. package/dist/allowlist.js +0 -1
  5. package/dist/client.d.ts +62 -3
  6. package/dist/client.js +194 -4
  7. package/dist/generated/api.d.ts +6607 -0
  8. package/dist/index.d.ts +5 -3
  9. package/dist/index.js +1 -2
  10. package/dist/resources/account.d.ts +51 -9
  11. package/dist/resources/account.js +18 -3
  12. package/dist/resources/bulk.d.ts +0 -1
  13. package/dist/resources/bulk.js +0 -1
  14. package/dist/resources/fetch.d.ts +0 -1
  15. package/dist/resources/fetch.js +0 -1
  16. package/dist/resources/monitors.d.ts +0 -1
  17. package/dist/resources/monitors.js +0 -1
  18. package/dist/resources/recordings.d.ts +0 -1
  19. package/dist/resources/recordings.js +0 -1
  20. package/dist/resources/scrape.d.ts +50 -8
  21. package/dist/resources/scrape.js +30 -7
  22. package/dist/resources/screenshots.d.ts +54 -4
  23. package/dist/resources/screenshots.js +6 -2
  24. package/dist/resources/search.d.ts +0 -1
  25. package/dist/resources/search.js +0 -1
  26. package/dist/resources/sitemap.d.ts +0 -1
  27. package/dist/resources/sitemap.js +0 -1
  28. package/dist/test-helpers/signal-aware-never-resolver.d.ts +9 -0
  29. package/dist/test-helpers/signal-aware-never-resolver.js +25 -0
  30. package/dist/types.d.ts +0 -1
  31. package/dist/types.js +0 -1
  32. package/package.json +26 -5
  33. package/dist/allowlist.d.ts.map +0 -1
  34. package/dist/allowlist.js.map +0 -1
  35. package/dist/client.d.ts.map +0 -1
  36. package/dist/client.js.map +0 -1
  37. package/dist/index.d.ts.map +0 -1
  38. package/dist/index.js.map +0 -1
  39. package/dist/resources/account.d.ts.map +0 -1
  40. package/dist/resources/account.js.map +0 -1
  41. package/dist/resources/bulk.d.ts.map +0 -1
  42. package/dist/resources/bulk.js.map +0 -1
  43. package/dist/resources/fetch.d.ts.map +0 -1
  44. package/dist/resources/fetch.js.map +0 -1
  45. package/dist/resources/monitors.d.ts.map +0 -1
  46. package/dist/resources/monitors.js.map +0 -1
  47. package/dist/resources/recordings.d.ts.map +0 -1
  48. package/dist/resources/recordings.js.map +0 -1
  49. package/dist/resources/scrape.d.ts.map +0 -1
  50. package/dist/resources/scrape.js.map +0 -1
  51. package/dist/resources/screenshots.d.ts.map +0 -1
  52. package/dist/resources/screenshots.js.map +0 -1
  53. package/dist/resources/search.d.ts.map +0 -1
  54. package/dist/resources/search.js.map +0 -1
  55. package/dist/resources/sitemap.d.ts.map +0 -1
  56. package/dist/resources/sitemap.js.map +0 -1
  57. package/dist/types.d.ts.map +0 -1
  58. package/dist/types.js.map +0 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hunt Labs, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,171 @@
1
+ # @hunt-labs/bytekit-sdk
2
+
3
+ Official TypeScript SDK for the [ByteKit](https://bytekit.com) API — web data
4
+ infrastructure for AI agents and training pipelines: scraping, raw fetch,
5
+ screenshots, sitemaps, page monitors, and web search.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @hunt-labs/bytekit-sdk
11
+ ```
12
+
13
+ > **ESM-only.** This package ships as ESM (`"type": "module"`) and has no
14
+ > CommonJS build. Use it from an ESM project (`"type": "module"` in your
15
+ > `package.json`, or a `.mjs` entrypoint), or load it from CommonJS with a
16
+ > dynamic `await import('@hunt-labs/bytekit-sdk')`. A top-level
17
+ > `require('@hunt-labs/bytekit-sdk')` will not work.
18
+
19
+ Requires Node.js 18+ (the client uses the global `fetch`).
20
+
21
+ ## Authentication
22
+
23
+ Create an API key at [app.bytekit.com](https://app.bytekit.com) (keys are
24
+ prefixed `sk_live_`) and pass it to the client constructor:
25
+
26
+ ```ts
27
+ import { RapidCrawl } from '@hunt-labs/bytekit-sdk';
28
+
29
+ const client = new RapidCrawl({ apiKey: process.env.BYTEKIT_API_KEY! }); // sk_live_...
30
+ ```
31
+
32
+ The key is sent as `Authorization: Bearer <apiKey>` on every request. Keep it
33
+ server-side — it is not safe to ship in a browser bundle.
34
+
35
+ > The client class is named `RapidCrawl` for historical reasons (ByteKit's
36
+ > previous product name). It is the SDK's only entrypoint class.
37
+
38
+ ## Base-URL override
39
+
40
+ Requests go to `https://api.bytekit.com` by default. Point the client at
41
+ staging, a local gateway, or a proxy with the `baseUrl` option:
42
+
43
+ ```ts
44
+ const client = new RapidCrawl({
45
+ apiKey: process.env.BYTEKIT_API_KEY!,
46
+ baseUrl: 'https://api-stg.bytekit.com', // default: https://api.bytekit.com
47
+ });
48
+ ```
49
+
50
+ ## Usage
51
+
52
+ ### Scrape a page
53
+
54
+ ```ts
55
+ const result = await client.scrape.create({
56
+ url: 'https://example.com',
57
+ formats: ['markdown', 'html'],
58
+ });
59
+ ```
60
+
61
+ ### Fetch an async scrape by id
62
+
63
+ ```ts
64
+ const scrape = await client.scrape.get('sc_123abc');
65
+ ```
66
+
67
+ ### Raw fetch (unprocessed response body)
68
+
69
+ ```ts
70
+ const raw = await client.fetch.create({ url: 'https://example.com' });
71
+ ```
72
+
73
+ ### Web search
74
+
75
+ ```ts
76
+ const results = await client.search.query({ query: 'ai web scraping', limit: 10 });
77
+ for (const hit of results.results) {
78
+ console.log(hit.position, hit.title, hit.url);
79
+ }
80
+ ```
81
+
82
+ ### Screenshot a page
83
+
84
+ ```ts
85
+ const shot = await client.screenshots.create({
86
+ url: 'https://example.com',
87
+ full_page: true,
88
+ });
89
+ const ready = await client.screenshots.get('ss_123abc');
90
+ ```
91
+
92
+ ### Bulk jobs
93
+
94
+ `scrape` and `fetch` each expose a `bulk` sub-resource that takes a URL list
95
+ and a webhook to notify on completion:
96
+
97
+ ```ts
98
+ const job = await client.scrape.bulk.create({
99
+ urls: ['https://example.com', 'https://example.org'],
100
+ webhook_url: 'https://your.app/hooks/bytekit',
101
+ });
102
+ const status = await client.scrape.bulk.get('bulk_123abc');
103
+ ```
104
+
105
+ `client.fetch.bulk.create()` / `client.fetch.bulk.get()` follow the same shape.
106
+
107
+ Bulk **screenshots** are not a `screenshots.bulk` sub-resource — they go
108
+ through the separate top-level `client.bulk` resource instead:
109
+
110
+ ```ts
111
+ const shots = await client.bulk.create({
112
+ urls: ['https://example.com', 'https://example.org'],
113
+ webhook_url: 'https://your.app/hooks/bytekit',
114
+ });
115
+ const shotStatus = await client.bulk.get('bulk_123abc');
116
+ await client.bulk.screenshots.list('bulk_123abc');
117
+ ```
118
+
119
+ ### Page monitors
120
+
121
+ ```ts
122
+ const monitor = await client.monitors.create({
123
+ url: 'https://example.com/pricing',
124
+ interval_type: 'daily',
125
+ webhook_url: 'https://your.app/hooks/bytekit',
126
+ });
127
+ await client.monitors.list();
128
+ await client.monitors.delete('mon_123abc');
129
+ ```
130
+
131
+ ### Sitemap crawl
132
+
133
+ ```ts
134
+ const crawl = await client.sitemap.create({ url: 'https://example.com' });
135
+ const sitemap = await client.sitemap.get('sm_123abc');
136
+ ```
137
+
138
+ ### Account, usage, and API keys
139
+
140
+ ```ts
141
+ const account = await client.account.get();
142
+ const keys = await client.account.apiKeys.list();
143
+ ```
144
+
145
+ > `client.recordings.*` targets the `/v1/recordings` endpoints, which are
146
+ > deactivated server-side and return HTTP 410.
147
+
148
+ ## Error handling
149
+
150
+ Any 4xx/5xx response throws a `RapidCrawlError` carrying the HTTP status and
151
+ the API's error code:
152
+
153
+ ```ts
154
+ import { RapidCrawl, RapidCrawlError } from '@hunt-labs/bytekit-sdk';
155
+
156
+ try {
157
+ await client.scrape.create({ url: 'https://example.com' });
158
+ } catch (err) {
159
+ if (err instanceof RapidCrawlError) {
160
+ console.error(err.status, err.code, err.message);
161
+ }
162
+ }
163
+ ```
164
+
165
+ ## Documentation
166
+
167
+ Full API reference: [bytekit.com/docs](https://bytekit.com/docs).
168
+
169
+ ## License
170
+
171
+ MIT — see [LICENSE](./LICENSE).
@@ -10,4 +10,3 @@
10
10
  */
11
11
  export declare const V01_METHODS: readonly ["scrape.create", "scrape.get", "screenshots.create", "screenshots.get", "bulk.create", "bulk.get", "bulk.cancel", "bulk.screenshots.list", "scrape.bulk.create", "scrape.bulk.get", "fetch.create", "fetch.get", "fetch.bulk.create", "fetch.bulk.get", "monitors.create", "monitors.list", "monitors.get", "monitors.update", "monitors.delete", "monitors.captures.list", "sitemap.create", "sitemap.get", "search.query", "account.get"];
12
12
  export type V01Method = (typeof V01_METHODS)[number];
13
- //# sourceMappingURL=allowlist.d.ts.map
package/dist/allowlist.js CHANGED
@@ -35,4 +35,3 @@ export const V01_METHODS = [
35
35
  'search.query',
36
36
  'account.get',
37
37
  ];
38
- //# sourceMappingURL=allowlist.js.map
package/dist/client.d.ts CHANGED
@@ -10,16 +10,60 @@ import { AccountResource } from './resources/account.js';
10
10
  export interface RapidCrawlOptions {
11
11
  apiKey: string;
12
12
  baseUrl?: string;
13
+ /**
14
+ * Default CLIENT-SIDE request timeout in ms — the abort budget for the whole HTTP
15
+ * round-trip made by this SDK. `0` disables the SDK-side timeout. Default 120000.
16
+ *
17
+ * NOT the same thing as `ScrapeOpts.timeout_ms`, which is the SERVER-side budget the
18
+ * gateway applies to the scrape itself. Override per call with
19
+ * `RequestOptions.requestTimeoutMs`.
20
+ */
21
+ timeoutMs?: number;
22
+ }
23
+ /** Per-call overrides accepted by `request()` and by resource methods that opt in. */
24
+ export interface RequestOptions {
25
+ /**
26
+ * Caller-owned abort signal. When supplied WITHOUT a per-call `requestTimeoutMs`, it
27
+ * fully replaces the constructor timeout — supply `requestTimeoutMs` alongside it to
28
+ * keep a bound (the request then aborts on whichever fires first).
29
+ */
30
+ signal?: AbortSignal;
31
+ /**
32
+ * Per-call CLIENT-SIDE abort budget in ms. Wins over the constructor's `timeoutMs`.
33
+ * `0` disables the SDK-side timeout for this call.
34
+ *
35
+ * Deliberately NOT named `timeoutMs`: it rides on the same call as `ScrapeOpts.timeout_ms`
36
+ * (the SERVER-side scrape budget), and the two mean different things —
37
+ * `scrape.create({ timeout_ms: 30_000 }, { requestTimeoutMs: 5_000 })` says "let the
38
+ * server spend up to 30 s scraping, but give up on the wire after 5 s".
39
+ */
40
+ requestTimeoutMs?: number;
13
41
  }
42
+ /** Why a request never produced an HTTP response. */
43
+ export type ConnectionErrorCode = 'timeout' | 'aborted' | 'connection';
14
44
  /** Thrown when the API responds with a 4xx or 5xx status code. */
15
45
  export declare class RapidCrawlError extends Error {
16
46
  readonly status: number;
17
47
  readonly code: string;
18
48
  constructor(status: number, code: string, message: string);
19
49
  }
50
+ /**
51
+ * Thrown when the request never produced an HTTP response at all — DNS, socket, TLS,
52
+ * a client-side timeout, or a caller-initiated abort. Distinct from `RapidCrawlError`,
53
+ * which always carries a real HTTP status: catch both to cover every failure.
54
+ *
55
+ * The original failure is preserved on `.cause`.
56
+ */
57
+ export declare class RapidCrawlConnectionError extends Error {
58
+ readonly code: ConnectionErrorCode;
59
+ constructor(code: ConnectionErrorCode, message: string, options?: {
60
+ cause?: unknown;
61
+ });
62
+ }
20
63
  export declare class RapidCrawl {
21
64
  private readonly apiKey;
22
65
  private readonly baseUrl;
66
+ private readonly defaultTimeoutMs;
23
67
  readonly scrape: ScrapeResource;
24
68
  readonly screenshots: ScreenshotsResource;
25
69
  readonly recordings: RecordingsResource;
@@ -30,11 +74,26 @@ export declare class RapidCrawl {
30
74
  readonly search: SearchResource;
31
75
  readonly account: AccountResource;
32
76
  constructor(options: RapidCrawlOptions);
77
+ /**
78
+ * Resolve the abort budget for one request from the caller's options and the
79
+ * constructor default. Precedence:
80
+ *
81
+ * | caller `signal` | per-call `requestTimeoutMs` | signal attached to `fetch` |
82
+ * |-----------------|----------------------------|---------------------------------------|
83
+ * | absent | absent | timeout from the constructor default |
84
+ * | absent | present | timeout from the per-call value |
85
+ * | present | absent | the caller's signal, verbatim |
86
+ * | present | present | composition — aborts on either |
87
+ *
88
+ * A timeout of `0` (either level) means "no SDK-side timeout".
89
+ */
90
+ private resolveSignal;
33
91
  /**
34
92
  * Make an authenticated JSON request to the RapidCrawl API.
35
93
  *
36
- * Throws `RapidCrawlError` on 4xx/5xx responses.
94
+ * Throws `RapidCrawlError` on 4xx/5xx responses, and `RapidCrawlConnectionError`
95
+ * when no HTTP response was produced at all (DNS/socket/TLS failure, client-side
96
+ * timeout, or caller abort).
37
97
  */
38
- request<T>(method: string, path: string, body?: unknown): Promise<T>;
98
+ request<T>(method: string, path: string, body?: unknown, options?: RequestOptions): Promise<T>;
39
99
  }
40
- //# sourceMappingURL=client.d.ts.map
package/dist/client.js CHANGED
@@ -8,6 +8,14 @@ import { SitemapResource } from './resources/sitemap.js';
8
8
  import { SearchResource } from './resources/search.js';
9
9
  import { AccountResource } from './resources/account.js';
10
10
  const DEFAULT_BASE_URL = 'https://api.bytekit.com';
11
+ /**
12
+ * Default client-side request timeout (2 minutes).
13
+ *
14
+ * Above the slowest expected synchronous scrape, and well below undici's ~5-minute
15
+ * headers timeout, so the SDK is always the tighter bound and a hung upstream can
16
+ * never hang the caller indefinitely.
17
+ */
18
+ const DEFAULT_TIMEOUT_MS = 120_000;
11
19
  /** Thrown when the API responds with a 4xx or 5xx status code. */
12
20
  export class RapidCrawlError extends Error {
13
21
  status;
@@ -19,9 +27,134 @@ export class RapidCrawlError extends Error {
19
27
  this.code = code;
20
28
  }
21
29
  }
30
+ /**
31
+ * Thrown when the request never produced an HTTP response at all — DNS, socket, TLS,
32
+ * a client-side timeout, or a caller-initiated abort. Distinct from `RapidCrawlError`,
33
+ * which always carries a real HTTP status: catch both to cover every failure.
34
+ *
35
+ * The original failure is preserved on `.cause`.
36
+ */
37
+ export class RapidCrawlConnectionError extends Error {
38
+ code;
39
+ constructor(code, message, options) {
40
+ super(message, options);
41
+ this.name = 'RapidCrawlConnectionError';
42
+ this.code = code;
43
+ }
44
+ }
45
+ const NOOP = () => { };
46
+ /**
47
+ * Validate a timeout option. `undefined` passes through; anything that is not a
48
+ * non-negative finite number throws a `TypeError` naming the option as the caller
49
+ * spelled it (`"timeoutMs"` at the constructor, `"requestTimeoutMs"` per call).
50
+ */
51
+ function validateTimeout(value, optionName) {
52
+ if (value === undefined)
53
+ return undefined;
54
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
55
+ throw new TypeError(`RapidCrawl: ${optionName} must be a non-negative finite number (got ${describe(value)})`);
56
+ }
57
+ return value;
58
+ }
59
+ /** Render a rejected option value for an error message without ever stringifying an object. */
60
+ function describe(value) {
61
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
62
+ return String(value);
63
+ }
64
+ if (typeof value === 'string')
65
+ return JSON.stringify(value);
66
+ if (value === null)
67
+ return 'null';
68
+ return typeof value;
69
+ }
70
+ /** The abort reason used by the `setTimeout` fallback; named so it maps to `code: 'timeout'`. */
71
+ function timeoutReason(ms) {
72
+ const reason = new Error(`RapidCrawl: request timed out after ${ms}ms`);
73
+ reason.name = 'TimeoutError';
74
+ return reason;
75
+ }
76
+ /** `AbortSignal.timeout(ms)` where available, else an `AbortController` + `setTimeout`. */
77
+ function createTimeoutSignal(ms) {
78
+ if (typeof AbortSignal.timeout === 'function') {
79
+ return { signal: AbortSignal.timeout(ms), cleanup: NOOP };
80
+ }
81
+ const controller = new AbortController();
82
+ const timer = setTimeout(() => controller.abort(timeoutReason(ms)), ms);
83
+ timer.unref?.();
84
+ return {
85
+ signal: controller.signal,
86
+ cleanup: () => clearTimeout(timer),
87
+ };
88
+ }
89
+ /**
90
+ * Abort on whichever source fires first.
91
+ *
92
+ * Prefers `AbortSignal.any`, whose sources hold only WEAK references to the composed
93
+ * signal — so nothing has to be (and nothing must be) retained to keep it collectable.
94
+ * The manual fallback is the path that has to clean up after itself: it attaches a real
95
+ * listener to the CALLER's signal, which typically outlives the request (one long-lived
96
+ * `AbortController` reused across many calls), so `cleanup()` removes it again.
97
+ */
98
+ function composeSignals(caller, timeout) {
99
+ const anyFn = AbortSignal.any?.bind(AbortSignal);
100
+ if (typeof anyFn === 'function') {
101
+ return { signal: anyFn([caller, timeout]), cleanup: NOOP };
102
+ }
103
+ const controller = new AbortController();
104
+ const sources = [caller, timeout];
105
+ const onAbort = (event) => {
106
+ controller.abort(event.target.reason);
107
+ };
108
+ let done = false;
109
+ const cleanup = () => {
110
+ if (done)
111
+ return;
112
+ done = true;
113
+ for (const source of sources)
114
+ source.removeEventListener('abort', onAbort);
115
+ };
116
+ for (const source of sources)
117
+ source.addEventListener('abort', onAbort);
118
+ for (const source of sources) {
119
+ if (source.aborted) {
120
+ controller.abort(source.reason);
121
+ break;
122
+ }
123
+ }
124
+ return { signal: controller.signal, cleanup };
125
+ }
126
+ /** Map a `fetch` rejection onto the connection-error taxonomy. Never leaks the API key. */
127
+ function toConnectionError(err, method, url, timeoutSignal) {
128
+ // undici rejects an aborted fetch with the signal's own reason, so the thrown value's
129
+ // `name` is the primary discriminator (`TimeoutError` / `AbortError`).
130
+ const rawName = typeof err === 'object' && err !== null && 'name' in err ? err.name : undefined;
131
+ const name = typeof rawName === 'string' ? rawName : '';
132
+ let code;
133
+ if (name === 'TimeoutError') {
134
+ code = 'timeout';
135
+ }
136
+ else if (name === 'AbortError') {
137
+ code = 'aborted';
138
+ }
139
+ else if (timeoutSignal?.aborted === true) {
140
+ code = 'timeout';
141
+ }
142
+ else {
143
+ code = 'connection';
144
+ }
145
+ const detail = code === 'timeout'
146
+ ? 'request timed out'
147
+ : code === 'aborted'
148
+ ? 'request aborted by the caller'
149
+ : 'connection failed before a response was received';
150
+ return new RapidCrawlConnectionError(code, `RapidCrawl: ${method} ${url} — ${detail}`, {
151
+ cause: err,
152
+ });
153
+ }
22
154
  export class RapidCrawl {
23
155
  apiKey;
24
156
  baseUrl;
157
+ defaultTimeoutMs;
25
158
  scrape;
26
159
  screenshots;
27
160
  recordings;
@@ -32,8 +165,12 @@ export class RapidCrawl {
32
165
  search;
33
166
  account;
34
167
  constructor(options) {
168
+ if (!options?.apiKey) {
169
+ throw new TypeError('RapidCrawl: `apiKey` is required — got an empty value. Pass options.apiKey (e.g. from process.env.BYTEKIT_API_KEY).');
170
+ }
35
171
  this.apiKey = options.apiKey;
36
172
  this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
173
+ this.defaultTimeoutMs = validateTimeout(options.timeoutMs, '"timeoutMs"') ?? DEFAULT_TIMEOUT_MS;
37
174
  this.scrape = new ScrapeResource(this);
38
175
  this.screenshots = new ScreenshotsResource(this);
39
176
  this.recordings = new RecordingsResource(this);
@@ -44,13 +181,52 @@ export class RapidCrawl {
44
181
  this.search = new SearchResource(this);
45
182
  this.account = new AccountResource(this);
46
183
  }
184
+ /**
185
+ * Resolve the abort budget for one request from the caller's options and the
186
+ * constructor default. Precedence:
187
+ *
188
+ * | caller `signal` | per-call `requestTimeoutMs` | signal attached to `fetch` |
189
+ * |-----------------|----------------------------|---------------------------------------|
190
+ * | absent | absent | timeout from the constructor default |
191
+ * | absent | present | timeout from the per-call value |
192
+ * | present | absent | the caller's signal, verbatim |
193
+ * | present | present | composition — aborts on either |
194
+ *
195
+ * A timeout of `0` (either level) means "no SDK-side timeout".
196
+ */
197
+ resolveSignal(options) {
198
+ const callerSignal = options?.signal;
199
+ const perCallMs = validateTimeout(options?.requestTimeoutMs, '"requestTimeoutMs"');
200
+ // A caller-supplied signal suppresses the constructor default (it does not
201
+ // suppress an explicit per-call budget, which composes with it instead).
202
+ const effectiveMs = perCallMs ?? (callerSignal !== undefined ? 0 : this.defaultTimeoutMs);
203
+ if (effectiveMs === 0) {
204
+ return { signal: callerSignal, timeoutSignal: undefined, cleanup: NOOP };
205
+ }
206
+ const timeout = createTimeoutSignal(effectiveMs);
207
+ if (callerSignal === undefined) {
208
+ return { signal: timeout.signal, timeoutSignal: timeout.signal, cleanup: timeout.cleanup };
209
+ }
210
+ const composed = composeSignals(callerSignal, timeout.signal);
211
+ return {
212
+ signal: composed.signal,
213
+ timeoutSignal: timeout.signal,
214
+ cleanup: () => {
215
+ composed.cleanup();
216
+ timeout.cleanup();
217
+ },
218
+ };
219
+ }
47
220
  /**
48
221
  * Make an authenticated JSON request to the RapidCrawl API.
49
222
  *
50
- * Throws `RapidCrawlError` on 4xx/5xx responses.
223
+ * Throws `RapidCrawlError` on 4xx/5xx responses, and `RapidCrawlConnectionError`
224
+ * when no HTTP response was produced at all (DNS/socket/TLS failure, client-side
225
+ * timeout, or caller abort).
51
226
  */
52
- async request(method, path, body) {
227
+ async request(method, path, body, options) {
53
228
  const url = `${this.baseUrl}${path}`;
229
+ const { signal, timeoutSignal, cleanup } = this.resolveSignal(options);
54
230
  const init = {
55
231
  method,
56
232
  headers: {
@@ -61,7 +237,22 @@ export class RapidCrawl {
61
237
  if (body !== undefined) {
62
238
  init.body = JSON.stringify(body);
63
239
  }
64
- const response = await fetch(url, init);
240
+ // `exactOptionalPropertyTypes` — assign only when we actually have a signal.
241
+ if (signal !== undefined) {
242
+ init.signal = signal;
243
+ }
244
+ let response;
245
+ try {
246
+ response = await fetch(url, init);
247
+ }
248
+ catch (err) {
249
+ throw toConnectionError(err, method, url, timeoutSignal);
250
+ }
251
+ finally {
252
+ // Runs on success, on HTTP error, and on abort alike — no dangling timer,
253
+ // no listener left behind on the caller's (long-lived) signal.
254
+ cleanup();
255
+ }
65
256
  if (!response.ok) {
66
257
  let code = 'internal_error';
67
258
  let message = response.statusText;
@@ -78,4 +269,3 @@ export class RapidCrawl {
78
269
  return response.json();
79
270
  }
80
271
  }
81
- //# sourceMappingURL=client.js.map