@boy-offi9-inc/reqkit 0.7.1 → 0.8.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 (34) hide show
  1. package/README.md +92 -66
  2. package/dist/reqkit.mjs +2136 -2014
  3. package/package.json +9 -5
  4. package/src/{createFetch.js → http/createFetch.js} +130 -22
  5. package/src/index.js +18 -18
  6. package/src/{paginate.js → pagination/paginate.js} +2 -2
  7. package/src/{createRateLimiter.js → resilience/rateLimiter.js} +7 -4
  8. package/src/{withRetry.js → resilience/retry.js} +2 -2
  9. package/src/{retryAfterAware.js → resilience/retryAfterAware.js} +3 -3
  10. package/src/{downloadResumable.js → streams/download.js} +5 -5
  11. package/src/{streamSSE.js → streams/streamSSE.js} +5 -5
  12. package/src/{createMockFetch.js → testing/createMockFetch.js} +1 -1
  13. package/types/errors.d.ts +24 -0
  14. package/types/http.d.ts +180 -0
  15. package/types/index.d.ts +7 -538
  16. package/types/pagination.d.ts +62 -0
  17. package/types/resilience.d.ts +171 -0
  18. package/types/streams.d.ts +105 -0
  19. package/types/testing.d.ts +44 -0
  20. /package/{RECIPES.md → docs/recipes.md} +0 -0
  21. /package/src/{HTTPError.js → errors/HTTPError.js} +0 -0
  22. /package/src/{normalizeError.js → errors/normalizeError.js} +0 -0
  23. /package/src/{buildQuery.js → http/buildQuery.js} +0 -0
  24. /package/src/{_cache.js → http/cache/entry.js} +0 -0
  25. /package/src/{createMemoryCache.js → http/cache/memoryCache.js} +0 -0
  26. /package/src/{parseJsonSafe.js → http/parseJsonSafe.js} +0 -0
  27. /package/src/{_http.js → internal/http.js} +0 -0
  28. /package/src/{_sleep.js → internal/sleep.js} +0 -0
  29. /package/src/{createCircuitBreaker.js → resilience/circuitBreaker.js} +0 -0
  30. /package/src/{withDedupe.js → resilience/dedupe.js} +0 -0
  31. /package/src/{createLimiter.js → resilience/limiter.js} +0 -0
  32. /package/src/{withTimeout.js → resilience/timeout.js} +0 -0
  33. /package/src/{withProgress.js → streams/progress.js} +0 -0
  34. /package/src/{streams.js → streams/readers.js} +0 -0
package/types/index.d.ts CHANGED
@@ -1,538 +1,7 @@
1
- export interface RetryContext {
2
- reason: "error" | "result";
3
- /** The result that triggered the retry (reason "result"). */
4
- result?: unknown;
5
- /** The result's HTTP status (reason "result"). */
6
- status?: number;
7
- }
8
-
9
- export interface CalculateDelayInfo {
10
- attempt: number;
11
- error: unknown;
12
- result: unknown;
13
- /** The delay reqkit would have used. */
14
- delayMs: number;
15
- }
16
-
17
- export interface RetryOptions {
18
- /** Max retries after the first try. Infinity is fine together with maxRetryTime. Default 3. */
19
- retries?: number;
20
- /** Default 300. */
21
- baseDelayMs?: number;
22
- /** Backoff ceiling. Default 10000. */
23
- maxDelayMs?: number;
24
- /** Backoff multiplier. Default 2. */
25
- factor?: number;
26
- /** Extra random fraction (default 0.2), or "full" for uniform [0, delay]. */
27
- jitter?: number | "full";
28
- /** Total time budget across attempts and waits, in ms. */
29
- maxRetryTime?: number;
30
- /** For thrown errors. */
31
- shouldRetry?: (err: unknown, attempt: number) => boolean;
32
- /** Retry when the result's `.status` is in this list. */
33
- retryStatusCodes?: number[];
34
- /** Retry on any result predicate. */
35
- shouldRetryResult?: (result: any, attempt: number) => boolean;
36
- /** Use Retry-After / X-RateLimit-Reset from `result.headers`. Default true. */
37
- respectRetryAfter?: boolean;
38
- /** Cap on a header-provided wait. Default 60000. */
39
- maxRetryAfterMs?: number;
40
- /** Return ms to wait, or false to stop retrying. */
41
- calculateDelay?: (info: CalculateDelayInfo) => number | false | null | undefined;
42
- /** `err` is undefined when a result (not an error) triggered the retry. */
43
- onRetry?: (err: unknown, attempt: number, delayMs: number, ctx: RetryContext) => void;
44
- signal?: AbortSignal;
45
- }
46
-
47
- /** Throw inside `fn` to stop retrying. An Error argument is rethrown as-is. */
48
- export class StopRetry extends Error {
49
- name: "StopRetry";
50
- original: Error | null;
51
- constructor(reasonOrMessage?: unknown);
52
- }
53
-
54
- export class RetryAbortedError extends Error {
55
- name: "RetryAbortedError";
56
- /** The last failure before the loop was aborted, if there was one. */
57
- cause?: unknown;
58
- }
59
-
60
- export function withRetry<T>(fn: (attempt: number) => Promise<T>, opts?: RetryOptions): Promise<T>;
61
-
62
- export class TimeoutError extends Error {
63
- code: "ETIMEDOUT";
64
- constructor(ms: number);
65
- }
66
-
67
- export interface TimeoutOptions {
68
- /** External cancellation, linked into the signal `fn` receives. */
69
- signal?: AbortSignal;
70
- }
71
-
72
- /** `ms` may be Infinity to disable the timer (only link `signal`). */
73
- export function withTimeout<T>(
74
- fn: (signal: AbortSignal) => Promise<T>,
75
- ms: number,
76
- opts?: TimeoutOptions
77
- ): Promise<T>;
78
-
79
- export interface NormalizedError {
80
- message: string;
81
- status: number | null;
82
- code: string | null;
83
- isNetworkError: boolean;
84
- isTimeout: boolean;
85
- /** True when the request was cancelled on purpose (AbortController, axios cancel). */
86
- isAborted: boolean;
87
- cause: unknown;
88
- }
89
-
90
- export function normalizeError(err: unknown): NormalizedError;
91
-
92
- export interface JsonSafeResult<T = unknown> {
93
- data: T | null;
94
- error: Error | null;
95
- }
96
-
97
- export function parseJsonSafe<T = unknown>(input: Response | string): Promise<JsonSafeResult<T>>;
98
-
99
- export function buildQuery(params: Record<string, unknown> | null | undefined): string;
100
-
101
- export interface RetryAfterAwareOptions {
102
- retries?: number;
103
- retryStatusCodes?: number[];
104
- fallbackDelayMs?: number;
105
- maxDelayMs?: number;
106
- onRetry?: (status: number, waitMs: number, attempt: number) => void;
107
- /** Decide whether a thrown error is retried. Default: network errors and timeouts only. */
108
- shouldRetryError?: (err: unknown, attempt: number) => boolean;
109
- onRetryError?: (err: unknown, waitMs: number, attempt: number) => void;
110
- /** Cancels the retry loop, including any pending wait. Throws RetryAbortedError. */
111
- signal?: AbortSignal;
112
- }
113
-
114
- export function retryAfterAware(
115
- fn: (attempt: number) => Promise<Response>,
116
- opts?: RetryAfterAwareOptions
117
- ): Promise<Response>;
118
-
119
- export function parseRetryAfter(headerValue: string | null): number | null;
120
- export function getWaitMsFromHeaders(headers: Headers | null | undefined): number | null;
121
-
122
- export interface DownloadProgress {
123
- loaded: number;
124
- total: number | null;
125
- percent: number | null;
126
- }
127
-
128
- export interface WithProgressOptions {
129
- /** Reject with BodyTooLargeError once more than this many bytes arrive. */
130
- maxBytes?: number;
131
- /** Cancels the read; rejects with the signal's reason. */
132
- signal?: AbortSignal;
133
- }
134
-
135
- export class BodyTooLargeError extends Error {
136
- name: "BodyTooLargeError";
137
- code: "EBODYTOOLARGE";
138
- maxBytes: number;
139
- constructor(maxBytes: number);
140
- }
141
-
142
- export function withProgress(
143
- response: Response,
144
- onProgress?: (progress: DownloadProgress) => void,
145
- opts?: WithProgressOptions
146
- ): Promise<Uint8Array>;
147
-
148
- export interface DedupeOptions {
149
- keyFn?: (...args: any[]) => string;
150
- }
151
-
152
- export function withDedupe<Fn extends (...args: any[]) => Promise<any>>(
153
- fn: Fn,
154
- opts?: DedupeOptions
155
- ): Fn;
156
-
157
- export interface Limiter {
158
- <A extends any[], R>(fn: (...args: A) => R | Promise<R>, ...args: A): Promise<R>;
159
- readonly activeCount: number;
160
- readonly pendingCount: number;
161
- /** Runs fn(item, index) for each item through the limiter; keeps input order. */
162
- map<T, R>(items: Iterable<T>, fn: (item: T, index: number) => R | Promise<R>): Promise<R[]>;
163
- }
164
-
165
- export function createLimiter(concurrency?: number): Limiter;
166
-
167
- export class HTTPError extends Error {
168
- name: "HTTPError";
169
- status: number;
170
- statusText: string;
171
- response: Response;
172
- url: string;
173
- method: string;
174
- constructor(response: Response, request: { method: string; url: string });
175
- }
176
-
177
- export interface CreateFetchRetryOptions {
178
- /** Default 2. */
179
- retries?: number;
180
- /** Default [408, 425, 429, 500, 502, 503, 504]. */
181
- statusCodes?: number[];
182
- /** Methods that may be retried. Default GET, HEAD, OPTIONS, PUT, DELETE. */
183
- methods?: string[];
184
- baseDelayMs?: number;
185
- maxDelayMs?: number;
186
- factor?: number;
187
- jitter?: number | "full";
188
- maxRetryTime?: number;
189
- shouldRetry?: (err: unknown, attempt: number) => boolean;
190
- calculateDelay?: RetryOptions["calculateDelay"];
191
- onRetry?: RetryOptions["onRetry"];
192
- }
193
-
194
- export interface FetchHookContext {
195
- url: string;
196
- init: RequestInit & { headers: Headers };
197
- }
198
-
199
- export interface CreateFetchDefaults {
200
- baseUrl?: string;
201
- headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
202
- /** Per attempt, in ms. false disables. Default 30000. */
203
- timeout?: number | false;
204
- /** false disables retrying. */
205
- retry?: false | CreateFetchRetryOptions;
206
- /**
207
- * `true` only learns from X-RateLimit-* / Retry-After headers; `{ requests, perMs }`
208
- * also enforces a budget; or share a createRateLimiter() instance.
209
- */
210
- rateLimit?: boolean | RateLimiterOptions | RateLimiter;
211
- /** Options for a per-origin breaker, a shared breaker instance, or `(url) => breaker`. */
212
- circuitBreaker?: boolean | CircuitBreakerOptions | CircuitBreaker | ((url: string) => CircuitBreaker);
213
- /**
214
- * Extra origins ("https://cdn.example.com") that may receive the default credentials
215
- * (`auth` and default Authorization/Cookie/X-API-Key headers), besides `baseUrl`'s.
216
- * Absolute URLs to any other origin don't get them. With neither `baseUrl` nor
217
- * `trustedOrigins` there's nothing to compare against and credentials go everywhere.
218
- */
219
- trustedOrigins?: string[];
220
- /** Attach a token and, on a 401, refresh it once (shared by all concurrent requests) and replay. */
221
- auth?: AuthOptions;
222
- /** In-memory GET cache with TTL, stale-while-revalidate and ETag/Last-Modified revalidation. `true` = 60s TTL. */
223
- cache?: boolean | CacheOptions;
224
- /** Merge identical concurrent GET/HEAD calls. Default false. */
225
- dedupe?: boolean;
226
- /** Throw HTTPError on non-2xx. Default true. */
227
- throwHttpErrors?: boolean;
228
- fetch?: typeof fetch;
229
- onRequest?: (ctx: FetchHookContext) => void | Promise<void>;
230
- onResponse?: (ctx: FetchHookContext & { response: Response }) => void | Promise<void>;
231
- }
232
-
233
- export interface ReqkitRequestInit extends Omit<RequestInit, "headers" | "cache"> {
234
- headers?: HeadersInit;
235
- query?: Record<string, unknown>;
236
- /** Serialized as the body with a JSON content-type. */
237
- json?: unknown;
238
- timeout?: number | false;
239
- retry?: false | CreateFetchRetryOptions;
240
- throwHttpErrors?: boolean;
241
- /** `false`/"no-store" bypass the cache; "reload" skips the read but refreshes the entry. */
242
- cache?: false | "no-store" | "reload";
243
- }
244
-
245
- export interface ReqkitFetch {
246
- (input: string, init?: ReqkitRequestInit): Promise<Response>;
247
- /** The `baseUrl` this instance was created with. */
248
- readonly baseUrl: string | undefined;
249
- /** Resolves to the parsed JSON body (null for an empty body). */
250
- json<T = unknown>(input: string, init?: ReqkitRequestInit): Promise<T>;
251
- /** New instance with these defaults merged over the current ones. */
252
- extend(overrides?: CreateFetchDefaults): ReqkitFetch;
253
- }
254
-
255
- export function createFetch(defaults?: CreateFetchDefaults): ReqkitFetch;
256
-
257
- export interface RateLimiterOptions {
258
- /** Max requests per window. Default Infinity. */
259
- requests?: number;
260
- /** Window length in ms. Default 1000. */
261
- perMs?: number;
262
- }
263
-
264
- export interface RateLimiter {
265
- /** Resolves when it's your turn (FIFO). Rejects with the signal's reason if aborted while waiting. */
266
- acquire(opts?: { signal?: AbortSignal }): Promise<void>;
267
- /** Learn from a response's X-RateLimit-* / RateLimit-* / Retry-After headers. */
268
- update(response: { status?: number; headers?: { get(name: string): string | null } } | null | undefined): void;
269
- /** acquire → fn() → update(result). */
270
- run<T>(fn: () => Promise<T>, opts?: { signal?: AbortSignal }): Promise<T>;
271
- /** Ms until a server-imposed pause ends (0 if none). */
272
- readonly blockedForMs: number;
273
- reset(): void;
274
- }
275
-
276
- export function createRateLimiter(opts?: RateLimiterOptions): RateLimiter;
277
-
278
- export class CircuitOpenError extends Error {
279
- name: "CircuitOpenError";
280
- code: "CIRCUIT_OPEN";
281
- /** Ms until the breaker will let a probe through. */
282
- retryAfterMs: number;
283
- constructor(retryAfterMs: number);
284
- }
285
-
286
- export interface CircuitBreakerOptions {
287
- /** Consecutive failures before opening. Default 5. */
288
- failureThreshold?: number;
289
- /** How long to stay open before allowing a probe. Default 30000. */
290
- resetTimeoutMs?: number;
291
- /** Concurrent probes allowed while half-open. Default 1. */
292
- halfOpenMax?: number;
293
- /** Default: any thrown error except AbortError, or a result with status >= 500. */
294
- isFailure?: (valueOrError: any, isError: boolean) => boolean;
295
- onStateChange?: (state: CircuitState, previous: CircuitState) => void;
296
- }
297
-
298
- export type CircuitState = "closed" | "open" | "half-open";
299
-
300
- export interface CircuitBreaker {
301
- run<T>(fn: () => Promise<T>): Promise<T>;
302
- readonly state: CircuitState;
303
- readonly failures: number;
304
- reset(): void;
305
- }
306
-
307
- export function createCircuitBreaker(opts?: CircuitBreakerOptions): CircuitBreaker;
308
-
309
- export interface CacheStore {
310
- get(key: string): any | Promise<any>;
311
- set(key: string, value: any): void | Promise<void>;
312
- delete(key: string): any;
313
- }
314
-
315
- export interface MemoryCache extends CacheStore {
316
- clear(): void;
317
- readonly size: number;
318
- }
319
-
320
- export function createMemoryCache(opts?: { maxEntries?: number }): MemoryCache;
321
-
322
- export interface CacheEvent {
323
- type: "hit" | "stale" | "revalidated" | "miss" | "store";
324
- url: string;
325
- }
326
-
327
- export interface CacheOptions {
328
- /** How long an entry is served without asking the server, in ms. Default 60000. */
329
- ttl?: number;
330
- /** After the TTL, keep serving the stale copy for this long while refreshing in the background. Default 0. */
331
- staleWhileRevalidate?: number;
332
- /** For the built-in memory store. Default 100. */
333
- maxEntries?: number;
334
- /** Responses larger than this aren't cached. Default 1 MiB. */
335
- maxBodyBytes?: number;
336
- /** Bring your own store (Redis, IndexedDB, a Map...). */
337
- store?: CacheStore;
338
- onEvent?: (event: CacheEvent) => void;
339
- }
340
-
341
- export interface MockCall {
342
- url: string;
343
- method: string;
344
- headers: Record<string, string>;
345
- body: BodyInit | null | undefined;
346
- signal?: AbortSignal;
347
- /** The body parsed as JSON, if it was a JSON string. */
348
- readonly json: any;
349
- }
350
-
351
- export interface MockReplyOptions {
352
- headers?: HeadersInit;
353
- /** Wait this long before answering; respects the request's AbortSignal. */
354
- delayMs?: number;
355
- }
356
-
357
- export interface MockRoute {
358
- reply(status?: number, body?: unknown, opts?: MockReplyOptions): MockRoute;
359
- replyOnce(status?: number, body?: unknown, opts?: MockReplyOptions): MockRoute;
360
- replyWith(fn: (call: MockCall) => Response | { status?: number; body?: unknown; headers?: HeadersInit } | Promise<Response | { status?: number; body?: unknown; headers?: HeadersInit }>): MockRoute;
361
- replyWithOnce(fn: (call: MockCall) => Response | { status?: number; body?: unknown; headers?: HeadersInit } | Promise<Response | { status?: number; body?: unknown; headers?: HeadersInit }>): MockRoute;
362
- fail(err?: unknown): MockRoute;
363
- failOnce(err?: unknown): MockRoute;
364
- }
365
-
366
- export type MockMatcher = string | RegExp | ((url: string) => boolean);
367
-
368
- export interface MockFetch {
369
- fetch: typeof fetch;
370
- on(method: string, matcher: MockMatcher): MockRoute;
371
- get(matcher: MockMatcher): MockRoute;
372
- post(matcher: MockMatcher): MockRoute;
373
- put(matcher: MockMatcher): MockRoute;
374
- patch(matcher: MockMatcher): MockRoute;
375
- delete(matcher: MockMatcher): MockRoute;
376
- /** A TypeError("fetch failed") whose `cause.code` is set, like Node's fetch throws. */
377
- networkError(code?: string): TypeError;
378
- readonly calls: MockCall[];
379
- reset(): void;
380
- }
381
-
382
- export function createMockFetch(): MockFetch;
383
-
384
- export interface DownloadResumableOptions {
385
- fetch?: typeof fetch;
386
- headers?: HeadersInit;
387
- /** Consecutive attempts without progress before giving up. Default 5. */
388
- retries?: number;
389
- baseDelayMs?: number;
390
- maxDelayMs?: number;
391
- onProgress?: (progress: DownloadProgress) => void;
392
- /** Throws BodyTooLargeError past this many bytes. */
393
- maxBytes?: number;
394
- signal?: AbortSignal;
395
- onRetry?: (err: unknown, attempt: number, delayMs: number, info: { resumeFrom: number }) => void;
396
- }
397
-
398
- export function downloadResumable(url: string, opts?: DownloadResumableOptions): Promise<Uint8Array>;
399
-
400
- export type StreamSource = Response | ReadableStream<Uint8Array> | AsyncIterable<Uint8Array | string>;
401
-
402
- export interface StreamOptions {
403
- signal?: AbortSignal;
404
- /**
405
- * Longest line (or, for readSSE, one event's data) in characters before throwing
406
- * `code: "LINE_TOO_LONG"` — protects against a server that never sends a newline.
407
- * Default 16 Mi (16 * 1024 * 1024); pass Infinity to disable.
408
- */
409
- maxLineLength?: number;
410
- }
411
-
412
- export interface SSEEvent {
413
- event: string;
414
- data: string;
415
- id: string;
416
- retry: number | undefined;
417
- }
418
-
419
- export function readLines(source: StreamSource, opts?: StreamOptions): AsyncGenerator<string, void, undefined>;
420
- export function readNDJSON<T = unknown>(
421
- source: StreamSource,
422
- opts?: StreamOptions & { skipInvalid?: boolean }
423
- ): AsyncGenerator<T, void, undefined>;
424
- export function readSSE(source: StreamSource, opts?: StreamOptions): AsyncGenerator<SSEEvent, void, undefined>;
425
-
426
- export interface StreamSSEOptions {
427
- /** e.g. a createFetch() instance. Default: globalThis.fetch. */
428
- fetch?: typeof fetch;
429
- /** Default "GET". Only GET/HEAD reconnect unless `reconnect: true`. */
430
- method?: string;
431
- /** A function is re-evaluated before every (re)connect — handy for refreshing a token. */
432
- headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
433
- body?: BodyInit | null;
434
- signal?: AbortSignal;
435
- /** Resume from this id on the very first connect. */
436
- lastEventId?: string;
437
- /** Default: true for GET/HEAD, false otherwise. */
438
- reconnect?: boolean;
439
- /** Consecutive no-progress connection failures before giving up. Default 10 (Infinity = never). */
440
- retries?: number;
441
- /** Base reconnect delay; the server's `retry:` field overrides it. Default 1000. */
442
- reconnectDelayMs?: number;
443
- /** Cap on the exponential backoff. Default 30000. */
444
- maxDelayMs?: number;
445
- /** End the stream (no reconnect) after this event, e.g. `e => e.data === "[DONE]"`. */
446
- stopWhen?: (event: SSEEvent) => boolean;
447
- onOpen?: (info: { connections: number; lastEventId: string }) => void;
448
- onReconnect?: (info: {
449
- reason: "error" | "end" | "status";
450
- error?: unknown;
451
- status?: number;
452
- /** Consecutive reconnects without progress (at least 1). */
453
- attempt: number;
454
- delayMs: number;
455
- lastEventId: string;
456
- }) => void;
457
- }
458
-
459
- /** Auto-reconnecting SSE: resumes with Last-Event-ID, honors `retry:` and Retry-After. */
460
- export function streamSSE(url: string, opts?: StreamSSEOptions): AsyncGenerator<SSEEvent, void, undefined>;
461
-
462
- export interface AuthOptions {
463
- /** The current token (sync or async). Falsy = send no auth header. */
464
- getToken(): string | null | undefined | Promise<string | null | undefined>;
465
- /**
466
- * Called ONCE when requests are rejected (401 by default), no matter how many
467
- * are waiting. Return the new token, or update your store and return nothing
468
- * (reqkit then calls getToken() again). Throwing rejects every waiting request.
469
- */
470
- refresh(ctx: { token: string; response: Response }): string | void | undefined | Promise<string | void | undefined>;
471
- /** Default "authorization". */
472
- header?: string;
473
- /** Default "Bearer". Use "" to send the bare token (e.g. for an X-API-Key header). */
474
- scheme?: string;
475
- /** Default: `res.status === 401`. */
476
- shouldRefresh?: (response: Response) => boolean;
477
- }
478
-
479
- export interface PageContext {
480
- index: number;
481
- signal?: AbortSignal;
482
- }
483
-
484
- export interface PageResult<T, C = any> {
485
- items: T[];
486
- /** Whatever identifies the next page (token, offset, URL...). undefined/null = last page. */
487
- next?: C | null;
488
- }
489
-
490
- export interface Page<T, C = any> {
491
- items: T[];
492
- index: number;
493
- /** The cursor this page was fetched with (undefined for the first, unless `start` was given). */
494
- cursor: C | undefined;
495
- }
496
-
497
- export interface PaginateOptions<C = any> {
498
- /** The cursor for the first page. */
499
- start?: C;
500
- maxPages?: number;
501
- maxItems?: number;
502
- signal?: AbortSignal;
503
- }
504
-
505
- export type FetchPage<T, C = any> = (cursor: C | undefined, ctx: PageContext) => Promise<PageResult<T, C>>;
506
-
507
- /** Yields every item, fetching pages lazily. */
508
- export function paginate<T, C = any>(fetchPage: FetchPage<T, C>, opts?: PaginateOptions<C>): AsyncGenerator<T, void, undefined>;
509
- /** Yields whole pages. */
510
- export function paginatePages<T, C = any>(
511
- fetchPage: FetchPage<T, C>,
512
- opts?: Omit<PaginateOptions<C>, "maxItems">
513
- ): AsyncGenerator<Page<T, C>, void, undefined>;
514
-
515
- export interface PaginateLinksOptions<T = any> {
516
- /** Default: the body if it's an array, else its `items` / `data` / `results` / `records`. */
517
- getItems?: (data: any, response: Response) => T[];
518
- maxPages?: number;
519
- maxItems?: number;
520
- signal?: AbortSignal;
521
- /** Extra options passed to every request. */
522
- init?: Record<string, any>;
523
- /**
524
- * Origins a `Link` may point to. Default: the origin of `url` (or of the createFetch
525
- * instance's `baseUrl`). Any other origin throws `code: "PAGINATE_CROSS_ORIGIN"`.
526
- */
527
- origins?: string[];
528
- }
529
-
530
- /** Follows `Link: <...>; rel="next"` headers and yields the items of every page. */
531
- export function paginateLinks<T = any>(
532
- request: (url: string, init?: any) => Promise<Response>,
533
- url: string,
534
- opts?: PaginateLinksOptions<T>
535
- ): AsyncGenerator<T, void, undefined>;
536
-
537
- /** Parses an RFC 8288 Link header into `{ rel: url }`. Pass `base` to resolve relative URLs. */
538
- export function parseLinkHeader(header: string | null | undefined, base?: string): Record<string, string>;
1
+ // Public type surface. Each domain lives in its own file, mirroring src/.
2
+ export * from "./errors";
3
+ export * from "./resilience";
4
+ export * from "./http";
5
+ export * from "./streams";
6
+ export * from "./pagination";
7
+ export * from "./testing";
@@ -0,0 +1,62 @@
1
+ // Pagination helpers — src/pagination/
2
+
3
+ export interface PageContext {
4
+ index: number;
5
+ signal?: AbortSignal;
6
+ }
7
+
8
+ export interface PageResult<T, C = any> {
9
+ items: T[];
10
+ /** Whatever identifies the next page (token, offset, URL...). undefined/null = last page. */
11
+ next?: C | null;
12
+ }
13
+
14
+ export interface Page<T, C = any> {
15
+ items: T[];
16
+ index: number;
17
+ /** The cursor this page was fetched with (undefined for the first, unless `start` was given). */
18
+ cursor: C | undefined;
19
+ }
20
+
21
+ export interface PaginateOptions<C = any> {
22
+ /** The cursor for the first page. */
23
+ start?: C;
24
+ maxPages?: number;
25
+ maxItems?: number;
26
+ signal?: AbortSignal;
27
+ }
28
+
29
+ export type FetchPage<T, C = any> = (cursor: C | undefined, ctx: PageContext) => Promise<PageResult<T, C>>;
30
+
31
+ /** Yields every item, fetching pages lazily. */
32
+ export function paginate<T, C = any>(fetchPage: FetchPage<T, C>, opts?: PaginateOptions<C>): AsyncGenerator<T, void, undefined>;
33
+ /** Yields whole pages. */
34
+ export function paginatePages<T, C = any>(
35
+ fetchPage: FetchPage<T, C>,
36
+ opts?: Omit<PaginateOptions<C>, "maxItems">
37
+ ): AsyncGenerator<Page<T, C>, void, undefined>;
38
+
39
+ export interface PaginateLinksOptions<T = any> {
40
+ /** Default: the body if it's an array, else its `items` / `data` / `results` / `records`. */
41
+ getItems?: (data: any, response: Response) => T[];
42
+ maxPages?: number;
43
+ maxItems?: number;
44
+ signal?: AbortSignal;
45
+ /** Extra options passed to every request. */
46
+ init?: Record<string, any>;
47
+ /**
48
+ * Origins a `Link` may point to. Default: the origin of `url` (or of the createFetch
49
+ * instance's `baseUrl`). Any other origin throws `code: "PAGINATE_CROSS_ORIGIN"`.
50
+ */
51
+ origins?: string[];
52
+ }
53
+
54
+ /** Follows `Link: <...>; rel="next"` headers and yields the items of every page. */
55
+ export function paginateLinks<T = any>(
56
+ request: (url: string, init?: any) => Promise<Response>,
57
+ url: string,
58
+ opts?: PaginateLinksOptions<T>
59
+ ): AsyncGenerator<T, void, undefined>;
60
+
61
+ /** Parses an RFC 8288 Link header into `{ rel: url }`. Pass `base` to resolve relative URLs. */
62
+ export function parseLinkHeader(header: string | null | undefined, base?: string): Record<string, string>;