@boy-offi9-inc/reqkit 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/README.md +174 -1
- package/RECIPES.md +243 -0
- package/dist/reqkit.mjs +2507 -0
- package/package.json +25 -5
- package/src/HTTPError.js +14 -0
- package/src/_cache.js +55 -0
- package/src/_http.js +44 -13
- package/src/_sleep.js +20 -1
- package/src/createCircuitBreaker.js +124 -0
- package/src/createFetch.js +225 -45
- package/src/createMemoryCache.js +40 -0
- package/src/createMockFetch.js +184 -0
- package/src/createRateLimiter.js +111 -0
- package/src/downloadResumable.js +193 -0
- package/src/index.js +17 -0
- package/src/index.mjs +10 -0
- package/src/normalizeError.js +1 -1
- package/src/streamSSE.js +197 -0
- package/src/streams.js +149 -0
- package/types/index.d.ts +211 -1
package/types/index.d.ts
CHANGED
|
@@ -203,6 +203,15 @@ export interface CreateFetchDefaults {
|
|
|
203
203
|
timeout?: number | false;
|
|
204
204
|
/** false disables retrying. */
|
|
205
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
|
+
/** In-memory GET cache with TTL, stale-while-revalidate and ETag/Last-Modified revalidation. `true` = 60s TTL. */
|
|
214
|
+
cache?: boolean | CacheOptions;
|
|
206
215
|
/** Merge identical concurrent GET/HEAD calls. Default false. */
|
|
207
216
|
dedupe?: boolean;
|
|
208
217
|
/** Throw HTTPError on non-2xx. Default true. */
|
|
@@ -212,7 +221,7 @@ export interface CreateFetchDefaults {
|
|
|
212
221
|
onResponse?: (ctx: FetchHookContext & { response: Response }) => void | Promise<void>;
|
|
213
222
|
}
|
|
214
223
|
|
|
215
|
-
export interface ReqkitRequestInit extends Omit<RequestInit, "headers"> {
|
|
224
|
+
export interface ReqkitRequestInit extends Omit<RequestInit, "headers" | "cache"> {
|
|
216
225
|
headers?: HeadersInit;
|
|
217
226
|
query?: Record<string, unknown>;
|
|
218
227
|
/** Serialized as the body with a JSON content-type. */
|
|
@@ -220,6 +229,8 @@ export interface ReqkitRequestInit extends Omit<RequestInit, "headers"> {
|
|
|
220
229
|
timeout?: number | false;
|
|
221
230
|
retry?: false | CreateFetchRetryOptions;
|
|
222
231
|
throwHttpErrors?: boolean;
|
|
232
|
+
/** `false`/"no-store" bypass the cache; "reload" skips the read but refreshes the entry. */
|
|
233
|
+
cache?: false | "no-store" | "reload";
|
|
223
234
|
}
|
|
224
235
|
|
|
225
236
|
export interface ReqkitFetch {
|
|
@@ -231,3 +242,202 @@ export interface ReqkitFetch {
|
|
|
231
242
|
}
|
|
232
243
|
|
|
233
244
|
export function createFetch(defaults?: CreateFetchDefaults): ReqkitFetch;
|
|
245
|
+
|
|
246
|
+
export interface RateLimiterOptions {
|
|
247
|
+
/** Max requests per window. Default Infinity. */
|
|
248
|
+
requests?: number;
|
|
249
|
+
/** Window length in ms. Default 1000. */
|
|
250
|
+
perMs?: number;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface RateLimiter {
|
|
254
|
+
/** Resolves when it's your turn (FIFO). Rejects with the signal's reason if aborted while waiting. */
|
|
255
|
+
acquire(opts?: { signal?: AbortSignal }): Promise<void>;
|
|
256
|
+
/** Learn from a response's X-RateLimit-* / RateLimit-* / Retry-After headers. */
|
|
257
|
+
update(response: { status?: number; headers?: { get(name: string): string | null } } | null | undefined): void;
|
|
258
|
+
/** acquire → fn() → update(result). */
|
|
259
|
+
run<T>(fn: () => Promise<T>, opts?: { signal?: AbortSignal }): Promise<T>;
|
|
260
|
+
/** Ms until a server-imposed pause ends (0 if none). */
|
|
261
|
+
readonly blockedForMs: number;
|
|
262
|
+
reset(): void;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function createRateLimiter(opts?: RateLimiterOptions): RateLimiter;
|
|
266
|
+
|
|
267
|
+
export class CircuitOpenError extends Error {
|
|
268
|
+
name: "CircuitOpenError";
|
|
269
|
+
code: "CIRCUIT_OPEN";
|
|
270
|
+
/** Ms until the breaker will let a probe through. */
|
|
271
|
+
retryAfterMs: number;
|
|
272
|
+
constructor(retryAfterMs: number);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export interface CircuitBreakerOptions {
|
|
276
|
+
/** Consecutive failures before opening. Default 5. */
|
|
277
|
+
failureThreshold?: number;
|
|
278
|
+
/** How long to stay open before allowing a probe. Default 30000. */
|
|
279
|
+
resetTimeoutMs?: number;
|
|
280
|
+
/** Concurrent probes allowed while half-open. Default 1. */
|
|
281
|
+
halfOpenMax?: number;
|
|
282
|
+
/** Default: any thrown error except AbortError, or a result with status >= 500. */
|
|
283
|
+
isFailure?: (valueOrError: any, isError: boolean) => boolean;
|
|
284
|
+
onStateChange?: (state: CircuitState, previous: CircuitState) => void;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export type CircuitState = "closed" | "open" | "half-open";
|
|
288
|
+
|
|
289
|
+
export interface CircuitBreaker {
|
|
290
|
+
run<T>(fn: () => Promise<T>): Promise<T>;
|
|
291
|
+
readonly state: CircuitState;
|
|
292
|
+
readonly failures: number;
|
|
293
|
+
reset(): void;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function createCircuitBreaker(opts?: CircuitBreakerOptions): CircuitBreaker;
|
|
297
|
+
|
|
298
|
+
export interface CacheStore {
|
|
299
|
+
get(key: string): any | Promise<any>;
|
|
300
|
+
set(key: string, value: any): void | Promise<void>;
|
|
301
|
+
delete(key: string): any;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export interface MemoryCache extends CacheStore {
|
|
305
|
+
clear(): void;
|
|
306
|
+
readonly size: number;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function createMemoryCache(opts?: { maxEntries?: number }): MemoryCache;
|
|
310
|
+
|
|
311
|
+
export interface CacheEvent {
|
|
312
|
+
type: "hit" | "stale" | "revalidated" | "miss" | "store";
|
|
313
|
+
url: string;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export interface CacheOptions {
|
|
317
|
+
/** How long an entry is served without asking the server, in ms. Default 60000. */
|
|
318
|
+
ttl?: number;
|
|
319
|
+
/** After the TTL, keep serving the stale copy for this long while refreshing in the background. Default 0. */
|
|
320
|
+
staleWhileRevalidate?: number;
|
|
321
|
+
/** For the built-in memory store. Default 100. */
|
|
322
|
+
maxEntries?: number;
|
|
323
|
+
/** Responses larger than this aren't cached. Default 1 MiB. */
|
|
324
|
+
maxBodyBytes?: number;
|
|
325
|
+
/** Bring your own store (Redis, IndexedDB, a Map...). */
|
|
326
|
+
store?: CacheStore;
|
|
327
|
+
onEvent?: (event: CacheEvent) => void;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export interface MockCall {
|
|
331
|
+
url: string;
|
|
332
|
+
method: string;
|
|
333
|
+
headers: Record<string, string>;
|
|
334
|
+
body: BodyInit | null | undefined;
|
|
335
|
+
signal?: AbortSignal;
|
|
336
|
+
/** The body parsed as JSON, if it was a JSON string. */
|
|
337
|
+
readonly json: any;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export interface MockReplyOptions {
|
|
341
|
+
headers?: HeadersInit;
|
|
342
|
+
/** Wait this long before answering; respects the request's AbortSignal. */
|
|
343
|
+
delayMs?: number;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export interface MockRoute {
|
|
347
|
+
reply(status?: number, body?: unknown, opts?: MockReplyOptions): MockRoute;
|
|
348
|
+
replyOnce(status?: number, body?: unknown, opts?: MockReplyOptions): MockRoute;
|
|
349
|
+
replyWith(fn: (call: MockCall) => Response | { status?: number; body?: unknown; headers?: HeadersInit } | Promise<Response | { status?: number; body?: unknown; headers?: HeadersInit }>): MockRoute;
|
|
350
|
+
replyWithOnce(fn: (call: MockCall) => Response | { status?: number; body?: unknown; headers?: HeadersInit } | Promise<Response | { status?: number; body?: unknown; headers?: HeadersInit }>): MockRoute;
|
|
351
|
+
fail(err?: unknown): MockRoute;
|
|
352
|
+
failOnce(err?: unknown): MockRoute;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export type MockMatcher = string | RegExp | ((url: string) => boolean);
|
|
356
|
+
|
|
357
|
+
export interface MockFetch {
|
|
358
|
+
fetch: typeof fetch;
|
|
359
|
+
on(method: string, matcher: MockMatcher): MockRoute;
|
|
360
|
+
get(matcher: MockMatcher): MockRoute;
|
|
361
|
+
post(matcher: MockMatcher): MockRoute;
|
|
362
|
+
put(matcher: MockMatcher): MockRoute;
|
|
363
|
+
patch(matcher: MockMatcher): MockRoute;
|
|
364
|
+
delete(matcher: MockMatcher): MockRoute;
|
|
365
|
+
/** A TypeError("fetch failed") whose `cause.code` is set, like Node's fetch throws. */
|
|
366
|
+
networkError(code?: string): TypeError;
|
|
367
|
+
readonly calls: MockCall[];
|
|
368
|
+
reset(): void;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function createMockFetch(): MockFetch;
|
|
372
|
+
|
|
373
|
+
export interface DownloadResumableOptions {
|
|
374
|
+
fetch?: typeof fetch;
|
|
375
|
+
headers?: HeadersInit;
|
|
376
|
+
/** Consecutive attempts without progress before giving up. Default 5. */
|
|
377
|
+
retries?: number;
|
|
378
|
+
baseDelayMs?: number;
|
|
379
|
+
maxDelayMs?: number;
|
|
380
|
+
onProgress?: (progress: DownloadProgress) => void;
|
|
381
|
+
/** Throws BodyTooLargeError past this many bytes. */
|
|
382
|
+
maxBytes?: number;
|
|
383
|
+
signal?: AbortSignal;
|
|
384
|
+
onRetry?: (err: unknown, attempt: number, delayMs: number, info: { resumeFrom: number }) => void;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function downloadResumable(url: string, opts?: DownloadResumableOptions): Promise<Uint8Array>;
|
|
388
|
+
|
|
389
|
+
export type StreamSource = Response | ReadableStream<Uint8Array> | AsyncIterable<Uint8Array | string>;
|
|
390
|
+
|
|
391
|
+
export interface StreamOptions {
|
|
392
|
+
signal?: AbortSignal;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export interface SSEEvent {
|
|
396
|
+
event: string;
|
|
397
|
+
data: string;
|
|
398
|
+
id: string;
|
|
399
|
+
retry: number | undefined;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function readLines(source: StreamSource, opts?: StreamOptions): AsyncGenerator<string, void, undefined>;
|
|
403
|
+
export function readNDJSON<T = unknown>(
|
|
404
|
+
source: StreamSource,
|
|
405
|
+
opts?: StreamOptions & { skipInvalid?: boolean }
|
|
406
|
+
): AsyncGenerator<T, void, undefined>;
|
|
407
|
+
export function readSSE(source: StreamSource, opts?: StreamOptions): AsyncGenerator<SSEEvent, void, undefined>;
|
|
408
|
+
|
|
409
|
+
export interface StreamSSEOptions {
|
|
410
|
+
/** e.g. a createFetch() instance. Default: globalThis.fetch. */
|
|
411
|
+
fetch?: typeof fetch;
|
|
412
|
+
/** Default "GET". Only GET/HEAD reconnect unless `reconnect: true`. */
|
|
413
|
+
method?: string;
|
|
414
|
+
/** A function is re-evaluated before every (re)connect — handy for refreshing a token. */
|
|
415
|
+
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
|
416
|
+
body?: BodyInit | null;
|
|
417
|
+
signal?: AbortSignal;
|
|
418
|
+
/** Resume from this id on the very first connect. */
|
|
419
|
+
lastEventId?: string;
|
|
420
|
+
/** Default: true for GET/HEAD, false otherwise. */
|
|
421
|
+
reconnect?: boolean;
|
|
422
|
+
/** Consecutive no-progress connection failures before giving up. Default 10 (Infinity = never). */
|
|
423
|
+
retries?: number;
|
|
424
|
+
/** Base reconnect delay; the server's `retry:` field overrides it. Default 1000. */
|
|
425
|
+
reconnectDelayMs?: number;
|
|
426
|
+
/** Cap on the exponential backoff. Default 30000. */
|
|
427
|
+
maxDelayMs?: number;
|
|
428
|
+
/** End the stream (no reconnect) after this event, e.g. `e => e.data === "[DONE]"`. */
|
|
429
|
+
stopWhen?: (event: SSEEvent) => boolean;
|
|
430
|
+
onOpen?: (info: { connections: number; lastEventId: string }) => void;
|
|
431
|
+
onReconnect?: (info: {
|
|
432
|
+
reason: "error" | "end" | "status";
|
|
433
|
+
error?: unknown;
|
|
434
|
+
status?: number;
|
|
435
|
+
/** Consecutive reconnects without progress (at least 1). */
|
|
436
|
+
attempt: number;
|
|
437
|
+
delayMs: number;
|
|
438
|
+
lastEventId: string;
|
|
439
|
+
}) => void;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Auto-reconnecting SSE: resumes with Last-Event-ID, honors `retry:` and Retry-After. */
|
|
443
|
+
export function streamSSE(url: string, opts?: StreamSSEOptions): AsyncGenerator<SSEEvent, void, undefined>;
|