@boy-offi9-inc/reqkit 0.7.1 → 0.9.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 (37) hide show
  1. package/README.md +134 -66
  2. package/dist/reqkit.mjs +2429 -2020
  3. package/package.json +11 -5
  4. package/src/{createFetch.js → http/createFetch.js} +203 -26
  5. package/src/http/latency.js +41 -0
  6. package/src/index.js +20 -18
  7. package/src/index.mjs +1 -0
  8. package/src/{paginate.js → pagination/paginate.js} +2 -2
  9. package/src/resilience/hedge.js +168 -0
  10. package/src/{createRateLimiter.js → resilience/rateLimiter.js} +7 -4
  11. package/src/{withRetry.js → resilience/retry.js} +2 -2
  12. package/src/{retryAfterAware.js → resilience/retryAfterAware.js} +3 -3
  13. package/src/{downloadResumable.js → streams/download.js} +5 -5
  14. package/src/{streamSSE.js → streams/streamSSE.js} +5 -5
  15. package/src/{createMockFetch.js → testing/createMockFetch.js} +1 -1
  16. package/types/errors.d.ts +24 -0
  17. package/types/http.d.ts +209 -0
  18. package/types/index.d.ts +7 -538
  19. package/types/pagination.d.ts +62 -0
  20. package/types/resilience.d.ts +194 -0
  21. package/types/streams.d.ts +105 -0
  22. package/types/testing.d.ts +44 -0
  23. /package/{RECIPES.md → docs/recipes.md} +0 -0
  24. /package/src/{HTTPError.js → errors/HTTPError.js} +0 -0
  25. /package/src/{normalizeError.js → errors/normalizeError.js} +0 -0
  26. /package/src/{buildQuery.js → http/buildQuery.js} +0 -0
  27. /package/src/{_cache.js → http/cache/entry.js} +0 -0
  28. /package/src/{createMemoryCache.js → http/cache/memoryCache.js} +0 -0
  29. /package/src/{parseJsonSafe.js → http/parseJsonSafe.js} +0 -0
  30. /package/src/{_http.js → internal/http.js} +0 -0
  31. /package/src/{_sleep.js → internal/sleep.js} +0 -0
  32. /package/src/{createCircuitBreaker.js → resilience/circuitBreaker.js} +0 -0
  33. /package/src/{withDedupe.js → resilience/dedupe.js} +0 -0
  34. /package/src/{createLimiter.js → resilience/limiter.js} +0 -0
  35. /package/src/{withTimeout.js → resilience/timeout.js} +0 -0
  36. /package/src/{withProgress.js → streams/progress.js} +0 -0
  37. /package/src/{streams.js → streams/readers.js} +0 -0
@@ -0,0 +1,194 @@
1
+ // Retry, timeout, dedupe, limiters and the circuit breaker — src/resilience/
2
+
3
+ export interface RetryContext {
4
+ reason: "error" | "result";
5
+ /** The result that triggered the retry (reason "result"). */
6
+ result?: unknown;
7
+ /** The result's HTTP status (reason "result"). */
8
+ status?: number;
9
+ }
10
+
11
+ export interface CalculateDelayInfo {
12
+ attempt: number;
13
+ error: unknown;
14
+ result: unknown;
15
+ /** The delay reqkit would have used. */
16
+ delayMs: number;
17
+ }
18
+
19
+ export interface RetryOptions {
20
+ /** Max retries after the first try. Infinity is fine together with maxRetryTime. Default 3. */
21
+ retries?: number;
22
+ /** Default 300. */
23
+ baseDelayMs?: number;
24
+ /** Backoff ceiling. Default 10000. */
25
+ maxDelayMs?: number;
26
+ /** Backoff multiplier. Default 2. */
27
+ factor?: number;
28
+ /** Extra random fraction (default 0.2), or "full" for uniform [0, delay]. */
29
+ jitter?: number | "full";
30
+ /** Total time budget across attempts and waits, in ms. */
31
+ maxRetryTime?: number;
32
+ /** For thrown errors. */
33
+ shouldRetry?: (err: unknown, attempt: number) => boolean;
34
+ /** Retry when the result's `.status` is in this list. */
35
+ retryStatusCodes?: number[];
36
+ /** Retry on any result predicate. */
37
+ shouldRetryResult?: (result: any, attempt: number) => boolean;
38
+ /** Use Retry-After / X-RateLimit-Reset from `result.headers`. Default true. */
39
+ respectRetryAfter?: boolean;
40
+ /** Cap on a header-provided wait. Default 60000. */
41
+ maxRetryAfterMs?: number;
42
+ /** Return ms to wait, or false to stop retrying. */
43
+ calculateDelay?: (info: CalculateDelayInfo) => number | false | null | undefined;
44
+ /** `err` is undefined when a result (not an error) triggered the retry. */
45
+ onRetry?: (err: unknown, attempt: number, delayMs: number, ctx: RetryContext) => void;
46
+ signal?: AbortSignal;
47
+ }
48
+
49
+ /** Throw inside `fn` to stop retrying. An Error argument is rethrown as-is. */
50
+ export class StopRetry extends Error {
51
+ name: "StopRetry";
52
+ original: Error | null;
53
+ constructor(reasonOrMessage?: unknown);
54
+ }
55
+
56
+ export class RetryAbortedError extends Error {
57
+ name: "RetryAbortedError";
58
+ /** The last failure before the loop was aborted, if there was one. */
59
+ cause?: unknown;
60
+ }
61
+
62
+ export function withRetry<T>(fn: (attempt: number) => Promise<T>, opts?: RetryOptions): Promise<T>;
63
+
64
+ export class TimeoutError extends Error {
65
+ code: "ETIMEDOUT";
66
+ constructor(ms: number);
67
+ }
68
+
69
+ export interface TimeoutOptions {
70
+ /** External cancellation, linked into the signal `fn` receives. */
71
+ signal?: AbortSignal;
72
+ }
73
+
74
+ /** `ms` may be Infinity to disable the timer (only link `signal`). */
75
+ export function withTimeout<T>(
76
+ fn: (signal: AbortSignal) => Promise<T>,
77
+ ms: number,
78
+ opts?: TimeoutOptions
79
+ ): Promise<T>;
80
+
81
+ export interface RetryAfterAwareOptions {
82
+ retries?: number;
83
+ retryStatusCodes?: number[];
84
+ fallbackDelayMs?: number;
85
+ maxDelayMs?: number;
86
+ onRetry?: (status: number, waitMs: number, attempt: number) => void;
87
+ /** Decide whether a thrown error is retried. Default: network errors and timeouts only. */
88
+ shouldRetryError?: (err: unknown, attempt: number) => boolean;
89
+ onRetryError?: (err: unknown, waitMs: number, attempt: number) => void;
90
+ /** Cancels the retry loop, including any pending wait. Throws RetryAbortedError. */
91
+ signal?: AbortSignal;
92
+ }
93
+
94
+ export function retryAfterAware(
95
+ fn: (attempt: number) => Promise<Response>,
96
+ opts?: RetryAfterAwareOptions
97
+ ): Promise<Response>;
98
+
99
+ export function parseRetryAfter(headerValue: string | null): number | null;
100
+ export function getWaitMsFromHeaders(headers: Headers | null | undefined): number | null;
101
+
102
+ export interface DedupeOptions {
103
+ keyFn?: (...args: any[]) => string;
104
+ }
105
+
106
+ export function withDedupe<Fn extends (...args: any[]) => Promise<any>>(
107
+ fn: Fn,
108
+ opts?: DedupeOptions
109
+ ): Fn;
110
+
111
+ export interface Limiter {
112
+ <A extends any[], R>(fn: (...args: A) => R | Promise<R>, ...args: A): Promise<R>;
113
+ readonly activeCount: number;
114
+ readonly pendingCount: number;
115
+ /** Runs fn(item, index) for each item through the limiter; keeps input order. */
116
+ map<T, R>(items: Iterable<T>, fn: (item: T, index: number) => R | Promise<R>): Promise<R[]>;
117
+ }
118
+
119
+ export function createLimiter(concurrency?: number): Limiter;
120
+
121
+ export interface RateLimiterOptions {
122
+ /** Max requests per window. Default Infinity. */
123
+ requests?: number;
124
+ /** Window length in ms. Default 1000. */
125
+ perMs?: number;
126
+ }
127
+
128
+ export interface RateLimiter {
129
+ /** Resolves when it's your turn (FIFO). Rejects with the signal's reason if aborted while waiting. */
130
+ acquire(opts?: { signal?: AbortSignal }): Promise<void>;
131
+ /** Learn from a response's X-RateLimit-* / RateLimit-* / Retry-After headers. */
132
+ update(response: { status?: number; headers?: { get(name: string): string | null } } | null | undefined): void;
133
+ /** acquire → fn() → update(result). */
134
+ run<T>(fn: () => Promise<T>, opts?: { signal?: AbortSignal }): Promise<T>;
135
+ /** Ms until a server-imposed pause ends (0 if none). */
136
+ readonly blockedForMs: number;
137
+ reset(): void;
138
+ }
139
+
140
+ export function createRateLimiter(opts?: RateLimiterOptions): RateLimiter;
141
+
142
+ export class CircuitOpenError extends Error {
143
+ name: "CircuitOpenError";
144
+ code: "CIRCUIT_OPEN";
145
+ /** Ms until the breaker will let a probe through. */
146
+ retryAfterMs: number;
147
+ constructor(retryAfterMs: number);
148
+ }
149
+
150
+ export interface CircuitBreakerOptions {
151
+ /** Consecutive failures before opening. Default 5. */
152
+ failureThreshold?: number;
153
+ /** How long to stay open before allowing a probe. Default 30000. */
154
+ resetTimeoutMs?: number;
155
+ /** Concurrent probes allowed while half-open. Default 1. */
156
+ halfOpenMax?: number;
157
+ /** Default: any thrown error except AbortError, or a result with status >= 500. */
158
+ isFailure?: (valueOrError: any, isError: boolean) => boolean;
159
+ onStateChange?: (state: CircuitState, previous: CircuitState) => void;
160
+ }
161
+
162
+ export type CircuitState = "closed" | "open" | "half-open";
163
+
164
+ export interface CircuitBreaker {
165
+ run<T>(fn: () => Promise<T>): Promise<T>;
166
+ readonly state: CircuitState;
167
+ readonly failures: number;
168
+ reset(): void;
169
+ }
170
+
171
+ export function createCircuitBreaker(opts?: CircuitBreakerOptions): CircuitBreaker;
172
+
173
+ export interface HedgeOptions<T = any> {
174
+ /** Wait before launching backup #index (1-based): a number of ms, or a function of the index. */
175
+ delayMs: number | ((index: number) => number);
176
+ /** How many backups at most. Default 1. */
177
+ maxHedges?: number;
178
+ /** Results that count as failed (and launch the next backup at once). Default: `result.status >= 500`. */
179
+ isFailure?: (result: T) => boolean;
180
+ /** Launch the next backup immediately when an attempt throws. Default true. */
181
+ hedgeOnError?: boolean;
182
+ /** A backup was launched. */
183
+ onHedge?: (info: { index: number; afterMs: number }) => void;
184
+ /** Which attempt produced the result (0 = the original). */
185
+ onWinner?: (index: number) => void;
186
+ /** Cancels everything; rejects with the signal's reason. */
187
+ signal?: AbortSignal;
188
+ }
189
+
190
+ /**
191
+ * Hedged requests: if `fn` hasn't answered after `delayMs`, start a backup and take whichever
192
+ * succeeds first; the others are aborted. Only hedge calls that are safe to run twice.
193
+ */
194
+ export function withHedge<T>(fn: (signal: AbortSignal, index: number) => Promise<T> | T, opts: HedgeOptions<T>): Promise<T>;
@@ -0,0 +1,105 @@
1
+ // Stream readers, SSE, progress and resumable downloads — src/streams/
2
+
3
+ export interface DownloadProgress {
4
+ loaded: number;
5
+ total: number | null;
6
+ percent: number | null;
7
+ }
8
+
9
+ export interface WithProgressOptions {
10
+ /** Reject with BodyTooLargeError once more than this many bytes arrive. */
11
+ maxBytes?: number;
12
+ /** Cancels the read; rejects with the signal's reason. */
13
+ signal?: AbortSignal;
14
+ }
15
+
16
+ export class BodyTooLargeError extends Error {
17
+ name: "BodyTooLargeError";
18
+ code: "EBODYTOOLARGE";
19
+ maxBytes: number;
20
+ constructor(maxBytes: number);
21
+ }
22
+
23
+ export function withProgress(
24
+ response: Response,
25
+ onProgress?: (progress: DownloadProgress) => void,
26
+ opts?: WithProgressOptions
27
+ ): Promise<Uint8Array>;
28
+
29
+ export interface DownloadResumableOptions {
30
+ fetch?: typeof fetch;
31
+ headers?: HeadersInit;
32
+ /** Consecutive attempts without progress before giving up. Default 5. */
33
+ retries?: number;
34
+ baseDelayMs?: number;
35
+ maxDelayMs?: number;
36
+ onProgress?: (progress: DownloadProgress) => void;
37
+ /** Throws BodyTooLargeError past this many bytes. */
38
+ maxBytes?: number;
39
+ signal?: AbortSignal;
40
+ onRetry?: (err: unknown, attempt: number, delayMs: number, info: { resumeFrom: number }) => void;
41
+ }
42
+
43
+ export function downloadResumable(url: string, opts?: DownloadResumableOptions): Promise<Uint8Array>;
44
+
45
+ export type StreamSource = Response | ReadableStream<Uint8Array> | AsyncIterable<Uint8Array | string>;
46
+
47
+ export interface StreamOptions {
48
+ signal?: AbortSignal;
49
+ /**
50
+ * Longest line (or, for readSSE, one event's data) in characters before throwing
51
+ * `code: "LINE_TOO_LONG"` — protects against a server that never sends a newline.
52
+ * Default 16 Mi (16 * 1024 * 1024); pass Infinity to disable.
53
+ */
54
+ maxLineLength?: number;
55
+ }
56
+
57
+ export interface SSEEvent {
58
+ event: string;
59
+ data: string;
60
+ id: string;
61
+ retry: number | undefined;
62
+ }
63
+
64
+ export function readLines(source: StreamSource, opts?: StreamOptions): AsyncGenerator<string, void, undefined>;
65
+ export function readNDJSON<T = unknown>(
66
+ source: StreamSource,
67
+ opts?: StreamOptions & { skipInvalid?: boolean }
68
+ ): AsyncGenerator<T, void, undefined>;
69
+ export function readSSE(source: StreamSource, opts?: StreamOptions): AsyncGenerator<SSEEvent, void, undefined>;
70
+
71
+ export interface StreamSSEOptions {
72
+ /** e.g. a createFetch() instance. Default: globalThis.fetch. */
73
+ fetch?: typeof fetch;
74
+ /** Default "GET". Only GET/HEAD reconnect unless `reconnect: true`. */
75
+ method?: string;
76
+ /** A function is re-evaluated before every (re)connect — handy for refreshing a token. */
77
+ headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
78
+ body?: BodyInit | null;
79
+ signal?: AbortSignal;
80
+ /** Resume from this id on the very first connect. */
81
+ lastEventId?: string;
82
+ /** Default: true for GET/HEAD, false otherwise. */
83
+ reconnect?: boolean;
84
+ /** Consecutive no-progress connection failures before giving up. Default 10 (Infinity = never). */
85
+ retries?: number;
86
+ /** Base reconnect delay; the server's `retry:` field overrides it. Default 1000. */
87
+ reconnectDelayMs?: number;
88
+ /** Cap on the exponential backoff. Default 30000. */
89
+ maxDelayMs?: number;
90
+ /** End the stream (no reconnect) after this event, e.g. `e => e.data === "[DONE]"`. */
91
+ stopWhen?: (event: SSEEvent) => boolean;
92
+ onOpen?: (info: { connections: number; lastEventId: string }) => void;
93
+ onReconnect?: (info: {
94
+ reason: "error" | "end" | "status";
95
+ error?: unknown;
96
+ status?: number;
97
+ /** Consecutive reconnects without progress (at least 1). */
98
+ attempt: number;
99
+ delayMs: number;
100
+ lastEventId: string;
101
+ }) => void;
102
+ }
103
+
104
+ /** Auto-reconnecting SSE: resumes with Last-Event-ID, honors `retry:` and Retry-After. */
105
+ export function streamSSE(url: string, opts?: StreamSSEOptions): AsyncGenerator<SSEEvent, void, undefined>;
@@ -0,0 +1,44 @@
1
+ // Test doubles — src/testing/
2
+
3
+ export interface MockCall {
4
+ url: string;
5
+ method: string;
6
+ headers: Record<string, string>;
7
+ body: BodyInit | null | undefined;
8
+ signal?: AbortSignal;
9
+ /** The body parsed as JSON, if it was a JSON string. */
10
+ readonly json: any;
11
+ }
12
+
13
+ export interface MockReplyOptions {
14
+ headers?: HeadersInit;
15
+ /** Wait this long before answering; respects the request's AbortSignal. */
16
+ delayMs?: number;
17
+ }
18
+
19
+ export interface MockRoute {
20
+ reply(status?: number, body?: unknown, opts?: MockReplyOptions): MockRoute;
21
+ replyOnce(status?: number, body?: unknown, opts?: MockReplyOptions): MockRoute;
22
+ replyWith(fn: (call: MockCall) => Response | { status?: number; body?: unknown; headers?: HeadersInit } | Promise<Response | { status?: number; body?: unknown; headers?: HeadersInit }>): MockRoute;
23
+ replyWithOnce(fn: (call: MockCall) => Response | { status?: number; body?: unknown; headers?: HeadersInit } | Promise<Response | { status?: number; body?: unknown; headers?: HeadersInit }>): MockRoute;
24
+ fail(err?: unknown): MockRoute;
25
+ failOnce(err?: unknown): MockRoute;
26
+ }
27
+
28
+ export type MockMatcher = string | RegExp | ((url: string) => boolean);
29
+
30
+ export interface MockFetch {
31
+ fetch: typeof fetch;
32
+ on(method: string, matcher: MockMatcher): MockRoute;
33
+ get(matcher: MockMatcher): MockRoute;
34
+ post(matcher: MockMatcher): MockRoute;
35
+ put(matcher: MockMatcher): MockRoute;
36
+ patch(matcher: MockMatcher): MockRoute;
37
+ delete(matcher: MockMatcher): MockRoute;
38
+ /** A TypeError("fetch failed") whose `cause.code` is set, like Node's fetch throws. */
39
+ networkError(code?: string): TypeError;
40
+ readonly calls: MockCall[];
41
+ reset(): void;
42
+ }
43
+
44
+ export function createMockFetch(): MockFetch;
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes