fastfetch-api-fetch-enhancer 2.0.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 (72) hide show
  1. package/.idea/FastFetch-Smart-API-Fetcher.iml +12 -0
  2. package/.idea/modules.xml +8 -0
  3. package/.idea/vcs.xml +6 -0
  4. package/LICENSE +21 -0
  5. package/README.md +101 -0
  6. package/__tests__/demo.test.ts +216 -0
  7. package/__tests__/test_database.json +5002 -0
  8. package/coverage/clover.xml +463 -0
  9. package/coverage/coverage-final.json +11 -0
  10. package/coverage/lcov-report/base.css +224 -0
  11. package/coverage/lcov-report/block-navigation.js +87 -0
  12. package/coverage/lcov-report/circuit-breaker.ts.html +547 -0
  13. package/coverage/lcov-report/client.ts.html +1858 -0
  14. package/coverage/lcov-report/errors.ts.html +415 -0
  15. package/coverage/lcov-report/fastFetch.ts.html +1045 -0
  16. package/coverage/lcov-report/favicon.png +0 -0
  17. package/coverage/lcov-report/index.html +251 -0
  18. package/coverage/lcov-report/index.ts.html +241 -0
  19. package/coverage/lcov-report/metrics.ts.html +685 -0
  20. package/coverage/lcov-report/middleware.ts.html +403 -0
  21. package/coverage/lcov-report/offline-queue.ts.html +535 -0
  22. package/coverage/lcov-report/prettify.css +1 -0
  23. package/coverage/lcov-report/prettify.js +2 -0
  24. package/coverage/lcov-report/queue.ts.html +421 -0
  25. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  26. package/coverage/lcov-report/sorter.js +196 -0
  27. package/coverage/lcov-report/streaming.ts.html +466 -0
  28. package/coverage/lcov.info +908 -0
  29. package/dist/circuit-breaker.d.ts +61 -0
  30. package/dist/circuit-breaker.d.ts.map +1 -0
  31. package/dist/circuit-breaker.js +106 -0
  32. package/dist/client.d.ts +215 -0
  33. package/dist/client.d.ts.map +1 -0
  34. package/dist/client.js +391 -0
  35. package/dist/errors.d.ts +56 -0
  36. package/dist/errors.d.ts.map +1 -0
  37. package/dist/errors.js +91 -0
  38. package/dist/fastFetch.d.ts +65 -0
  39. package/dist/fastFetch.d.ts.map +1 -0
  40. package/dist/fastFetch.js +209 -0
  41. package/dist/index.d.ts +18 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +18 -0
  44. package/dist/metrics.d.ts +71 -0
  45. package/dist/metrics.d.ts.map +1 -0
  46. package/dist/metrics.js +131 -0
  47. package/dist/middleware.d.ts +66 -0
  48. package/dist/middleware.d.ts.map +1 -0
  49. package/dist/middleware.js +45 -0
  50. package/dist/offline-queue.d.ts +65 -0
  51. package/dist/offline-queue.d.ts.map +1 -0
  52. package/dist/offline-queue.js +120 -0
  53. package/dist/queue.d.ts +33 -0
  54. package/dist/queue.d.ts.map +1 -0
  55. package/dist/queue.js +76 -0
  56. package/dist/streaming.d.ts +40 -0
  57. package/dist/streaming.d.ts.map +1 -0
  58. package/dist/streaming.js +98 -0
  59. package/index.d.ts +167 -0
  60. package/jest.config.js +16 -0
  61. package/package.json +55 -0
  62. package/src/circuit-breaker.ts +154 -0
  63. package/src/client.ts +591 -0
  64. package/src/errors.ts +110 -0
  65. package/src/fastFetch.ts +320 -0
  66. package/src/index.ts +52 -0
  67. package/src/metrics.ts +200 -0
  68. package/src/middleware.ts +106 -0
  69. package/src/offline-queue.ts +150 -0
  70. package/src/queue.ts +112 -0
  71. package/src/streaming.ts +127 -0
  72. package/tsconfig.json +18 -0
package/dist/client.js ADDED
@@ -0,0 +1,391 @@
1
+ /**
2
+ * FastFetch client factory — `createClient()`.
3
+ *
4
+ * Creates a pre-configured `FastFetchClient` instance with:
5
+ * - Base URL + default headers
6
+ * - Request / Response / Error interceptors (v1.x, preserved)
7
+ * - Koa-style middleware pipeline (`api.use()`)
8
+ * - Circuit breaker
9
+ * - Concurrency-limited request queue
10
+ * - Built-in metrics
11
+ * - SSE streaming (`api.stream()`)
12
+ * - Offline request queue
13
+ * - Typed HTTP method helpers (get / post / put / patch / delete / json)
14
+ * - Auto-serialisation of request body (plain objects → JSON)
15
+ */
16
+ import { fastFetch } from "./fastFetch.js";
17
+ import { HttpError, NetworkError } from "./errors.js";
18
+ import { MetricsCollector } from "./metrics.js";
19
+ import { CircuitBreaker, } from "./circuit-breaker.js";
20
+ import { RequestQueue } from "./queue.js";
21
+ import { compose, } from "./middleware.js";
22
+ import { consumeSSE } from "./streaming.js";
23
+ import { OfflineQueue } from "./offline-queue.js";
24
+ // ---------------------------------------------------------------------------
25
+ // FastFetchClient
26
+ // ---------------------------------------------------------------------------
27
+ export class FastFetchClient {
28
+ constructor(options = {}) {
29
+ // v2.x systems
30
+ this.middlewares = [];
31
+ const { baseURL = "", headers = {}, interceptors = {}, throwOnError = true, circuitBreaker: cbOptions, maxConcurrent, metricsWindowSize, offlineQueue: useOfflineQueue,
32
+ // FastFetchOptions passed to every request
33
+ retries, retryDelay, deduplicate, shouldRetry, timeout, fastCache, cacheTTL, onRetry, debug, } = options;
34
+ this.baseURL = baseURL.replace(/\/$/, "");
35
+ this.defaultHeaders = headers;
36
+ this.clientThrowOnError = throwOnError;
37
+ this.defaultOptions = {
38
+ retries,
39
+ retryDelay,
40
+ deduplicate,
41
+ shouldRetry,
42
+ timeout,
43
+ fastCache,
44
+ cacheTTL,
45
+ onRetry,
46
+ debug,
47
+ };
48
+ // v1.x interceptors
49
+ this.requestInterceptors = interceptors.request ?? [];
50
+ this.responseInterceptors = interceptors.response ?? [];
51
+ this.errorInterceptors = interceptors.error ?? [];
52
+ // v2.x systems
53
+ this._metrics = new MetricsCollector(metricsWindowSize);
54
+ if (cbOptions) {
55
+ this._circuitBreaker = new CircuitBreaker(cbOptions);
56
+ }
57
+ if (maxConcurrent !== undefined) {
58
+ this._queue = new RequestQueue(maxConcurrent);
59
+ }
60
+ if (useOfflineQueue) {
61
+ this._offlineQueue = new OfflineQueue();
62
+ this._offlineQueue.start();
63
+ }
64
+ }
65
+ // ── Middleware registration ──────────────────────────────────────────────
66
+ /**
67
+ * Register a middleware function.
68
+ *
69
+ * Middleware runs in registration order (Koa onion model):
70
+ * - Code *before* `await next()` executes on the way *down* (pre-request).
71
+ * - Code *after* `await next()` executes on the way *up* (post-response).
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * api.use(async (ctx, next) => {
76
+ * ctx.init.headers['X-Request-Id'] = crypto.randomUUID();
77
+ * const start = Date.now();
78
+ * await next();
79
+ * console.log(`${ctx.method} ${ctx.url} — ${Date.now() - start}ms ${ctx.response?.status}`);
80
+ * });
81
+ * ```
82
+ */
83
+ use(middleware) {
84
+ this.middlewares.push(middleware);
85
+ return this; // chainable
86
+ }
87
+ // ── Metrics ──────────────────────────────────────────────────────────────
88
+ /**
89
+ * Built-in metrics collector.
90
+ * Call `.metrics.snapshot()` to get request counts, error rates, and latency percentiles.
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * console.table(api.metrics.snapshot().byEndpoint);
95
+ * ```
96
+ */
97
+ get metrics() {
98
+ return this._metrics;
99
+ }
100
+ // ── Circuit breaker ───────────────────────────────────────────────────────
101
+ /**
102
+ * Direct access to the circuit breaker (if configured).
103
+ * Useful for inspecting state or manually resetting in tests.
104
+ */
105
+ get circuitBreaker() {
106
+ return this._circuitBreaker;
107
+ }
108
+ // ── Concurrency queue ────────────────────────────────────────────────────
109
+ /**
110
+ * Direct access to the concurrency queue (if configured).
111
+ * Useful for observing `pending` / `active` counts.
112
+ */
113
+ get queue() {
114
+ return this._queue;
115
+ }
116
+ // ── Internal helpers ─────────────────────────────────────────────────────
117
+ resolveURL(path) {
118
+ if (!this.baseURL ||
119
+ path.startsWith("http://") ||
120
+ path.startsWith("https://")) {
121
+ return path;
122
+ }
123
+ const sep = path.startsWith("/") ? "" : "/";
124
+ return `${this.baseURL}${sep}${path}`;
125
+ }
126
+ mergeInit(init) {
127
+ return {
128
+ ...this.defaultOptions,
129
+ ...init,
130
+ headers: {
131
+ ...this.defaultHeaders,
132
+ ...init?.headers,
133
+ },
134
+ };
135
+ }
136
+ /**
137
+ * Auto-serialise the request body.
138
+ * - Plain objects / arrays → JSON string + `Content-Type: application/json`
139
+ * - FormData / URLSearchParams / Blob / ArrayBuffer / string → passed through
140
+ */
141
+ autoSerializeBody(body, headers) {
142
+ if (body === undefined || body === null) {
143
+ return { body: undefined, headers };
144
+ }
145
+ if (body instanceof FormData ||
146
+ body instanceof URLSearchParams ||
147
+ body instanceof Blob ||
148
+ body instanceof ArrayBuffer ||
149
+ typeof body === "string") {
150
+ return { body: body, headers };
151
+ }
152
+ // Plain object or array — serialize to JSON
153
+ return {
154
+ body: JSON.stringify(body),
155
+ headers: { "Content-Type": "application/json", ...headers },
156
+ };
157
+ }
158
+ // ── Core fetch method ────────────────────────────────────────────────────
159
+ /**
160
+ * Make an HTTP request. Runs the full pipeline:
161
+ * request interceptors → middleware → circuit breaker → queue → fastFetch → response interceptors.
162
+ *
163
+ * Throws `HttpError` on non-2xx by default (disable with `throwOnError: false`).
164
+ */
165
+ async fetch(path, init) {
166
+ const url = this.resolveURL(path);
167
+ const mergedInit = this.mergeInit(init);
168
+ const method = (mergedInit.method ?? "GET").toUpperCase();
169
+ // ── Offline queue check ──────────────────────────────────────────────
170
+ if (this._offlineQueue?.shouldQueue(method)) {
171
+ this._offlineQueue.enqueue(url, mergedInit);
172
+ return new Response(JSON.stringify({ queued: true, offline: true }), {
173
+ status: 202,
174
+ headers: { "Content-Type": "application/json" },
175
+ });
176
+ }
177
+ // ── Build context ────────────────────────────────────────────────────
178
+ const ctx = {
179
+ url,
180
+ method,
181
+ init: mergedInit,
182
+ meta: {},
183
+ };
184
+ const start = Date.now();
185
+ let success = false;
186
+ let httpStatus;
187
+ try {
188
+ // ── v1.x request interceptors ──────────────────────────────────────
189
+ for (const interceptor of this.requestInterceptors) {
190
+ ctx.init = await interceptor(ctx.url, ctx.init);
191
+ }
192
+ // ── Determine effective throwOnError ───────────────────────────────
193
+ const shouldThrow = init?.throwOnError !== undefined
194
+ ? init.throwOnError
195
+ : this.clientThrowOnError;
196
+ // ── Innermost middleware: actual HTTP request ───────────────────────
197
+ const coreMiddleware = async (ctx, next) => {
198
+ const doFetch = async () => {
199
+ // fastFetch handles retries + dedup + cache internally
200
+ const response = await fastFetch(ctx.url, ctx.init);
201
+ if (!response.ok && shouldThrow) {
202
+ throw new HttpError(response);
203
+ }
204
+ return response;
205
+ };
206
+ let response;
207
+ if (this._circuitBreaker && this._queue) {
208
+ response = await this._queue.enqueue(() => this._circuitBreaker.execute(doFetch), "normal");
209
+ }
210
+ else if (this._circuitBreaker) {
211
+ response = await this._circuitBreaker.execute(doFetch);
212
+ }
213
+ else if (this._queue) {
214
+ response = await this._queue.enqueue(doFetch, "normal");
215
+ }
216
+ else {
217
+ response = await doFetch();
218
+ }
219
+ ctx.response = response;
220
+ httpStatus = response.status;
221
+ success = response.ok;
222
+ ctx.duration = Date.now() - start;
223
+ await next(); // nothing beyond this in the chain
224
+ };
225
+ // ── Compose and run pipeline ───────────────────────────────────────
226
+ const pipeline = compose([...this.middlewares, coreMiddleware]);
227
+ await pipeline(ctx);
228
+ // ── v1.x response interceptors ─────────────────────────────────────
229
+ let finalResponse = ctx.response;
230
+ for (const interceptor of this.responseInterceptors) {
231
+ finalResponse = await interceptor(finalResponse);
232
+ }
233
+ return finalResponse;
234
+ }
235
+ catch (err) {
236
+ ctx.duration = ctx.duration ?? Date.now() - start;
237
+ success = false;
238
+ // ── v1.x error interceptors ────────────────────────────────────────
239
+ let handledErr = err;
240
+ for (const interceptor of this.errorInterceptors) {
241
+ handledErr = await interceptor(handledErr);
242
+ }
243
+ throw handledErr;
244
+ }
245
+ finally {
246
+ this._metrics.record(url, ctx.duration ?? Date.now() - start, success, httpStatus);
247
+ }
248
+ }
249
+ // ── Typed JSON helper ────────────────────────────────────────────────────
250
+ /**
251
+ * Fetch and automatically parse the response body as JSON.
252
+ * Returns a typed `Promise<T>`.
253
+ *
254
+ * @example
255
+ * ```ts
256
+ * const user = await api.json<User>('/users/1');
257
+ * const posts = await api.json<Post[]>('/posts?limit=10');
258
+ * ```
259
+ */
260
+ async json(path, init) {
261
+ const res = await this.fetch(path, {
262
+ ...init,
263
+ headers: {
264
+ Accept: "application/json",
265
+ ...init?.headers,
266
+ },
267
+ });
268
+ return res.json();
269
+ }
270
+ // ── SSE streaming ─────────────────────────────────────────────────────────
271
+ /**
272
+ * Open a Server-Sent Events stream and call `handler` for each event.
273
+ *
274
+ * The method resolves when the stream ends or the provided `AbortSignal` fires.
275
+ * Automatically sets `Accept: text/event-stream` and `Cache-Control: no-cache`.
276
+ *
277
+ * @example
278
+ * ```ts
279
+ * const ctrl = new AbortController();
280
+ * await api.stream('/api/ai/generate', (event) => {
281
+ * process.stdout.write(event.data);
282
+ * }, { signal: ctrl.signal });
283
+ * ```
284
+ */
285
+ async stream(path, handler, init) {
286
+ const res = await this.fetch(path, {
287
+ ...init,
288
+ // SSE streams should always throw on error
289
+ ...{ "throwOnError": true },
290
+ headers: {
291
+ Accept: "text/event-stream",
292
+ "Cache-Control": "no-cache",
293
+ ...init?.headers,
294
+ },
295
+ });
296
+ if (!res.body) {
297
+ throw new NetworkError("Response body is null — cannot open SSE stream. " +
298
+ "Ensure the server returns a `Content-Type: text/event-stream` response.");
299
+ }
300
+ const reader = res.body.getReader();
301
+ await consumeSSE(reader, handler, init?.signal);
302
+ }
303
+ // ── HTTP method shorthands ───────────────────────────────────────────────
304
+ /** GET request — returns raw `Response`. Use `.json<T>()` for parsed data. */
305
+ get(path, init) {
306
+ return this.fetch(path, { ...init, method: "GET" });
307
+ }
308
+ /**
309
+ * POST request. Body is automatically serialised:
310
+ * - Plain objects / arrays → JSON (+ `Content-Type: application/json`)
311
+ * - FormData / URLSearchParams / string → passed through as-is
312
+ */
313
+ post(path, body, init) {
314
+ const headers = init?.headers;
315
+ const serialized = this.autoSerializeBody(body, headers ?? {});
316
+ return this.fetch(path, {
317
+ ...init,
318
+ method: "POST",
319
+ body: serialized.body,
320
+ headers: serialized.headers,
321
+ });
322
+ }
323
+ /**
324
+ * PUT request with auto body serialisation.
325
+ */
326
+ put(path, body, init) {
327
+ const headers = init?.headers;
328
+ const serialized = this.autoSerializeBody(body, headers ?? {});
329
+ return this.fetch(path, {
330
+ ...init,
331
+ method: "PUT",
332
+ body: serialized.body,
333
+ headers: serialized.headers,
334
+ });
335
+ }
336
+ /**
337
+ * PATCH request with auto body serialisation.
338
+ */
339
+ patch(path, body, init) {
340
+ const headers = init?.headers;
341
+ const serialized = this.autoSerializeBody(body, headers ?? {});
342
+ return this.fetch(path, {
343
+ ...init,
344
+ method: "PATCH",
345
+ body: serialized.body,
346
+ headers: serialized.headers,
347
+ });
348
+ }
349
+ /** DELETE request. */
350
+ delete(path, init) {
351
+ return this.fetch(path, { ...init, method: "DELETE" });
352
+ }
353
+ }
354
+ // ---------------------------------------------------------------------------
355
+ // Factory
356
+ // ---------------------------------------------------------------------------
357
+ /**
358
+ * Create a pre-configured `FastFetchClient` instance.
359
+ *
360
+ * @example
361
+ * ```ts
362
+ * import { createClient } from 'fastfetch-api-fetch-enhancer';
363
+ *
364
+ * const api = createClient({
365
+ * baseURL: 'https://api.example.com/v1',
366
+ * headers: { Authorization: `Bearer ${token}` },
367
+ * retries: 3,
368
+ * timeout: 8_000,
369
+ * circuitBreaker: { threshold: 5, timeout: 30_000 },
370
+ * maxConcurrent: 10,
371
+ * offlineQueue: true,
372
+ * });
373
+ *
374
+ * // Register middleware
375
+ * api.use(async (ctx, next) => {
376
+ * ctx.init.headers['X-Request-Id'] = crypto.randomUUID();
377
+ * await next();
378
+ * console.log(`${ctx.method} ${ctx.url} → ${ctx.response?.status} in ${ctx.duration}ms`);
379
+ * });
380
+ *
381
+ * const user = await api.json<User>('/users/1');
382
+ * const posts = await api.json<Post[]>('/posts');
383
+ * await api.post('/users', { name: 'Alice', role: 'admin' }); // auto-JSON
384
+ * await api.stream('/events', (e) => console.log(e.data));
385
+ *
386
+ * console.table(api.metrics.snapshot().byEndpoint);
387
+ * ```
388
+ */
389
+ export function createClient(options) {
390
+ return new FastFetchClient(options);
391
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * FastFetch typed error hierarchy.
3
+ *
4
+ * All errors extend `FastFetchError` so callers can do:
5
+ * catch (e) { if (e instanceof HttpError) { ... } }
6
+ */
7
+ export declare class FastFetchError extends Error {
8
+ constructor(message: string, cause?: unknown);
9
+ }
10
+ /**
11
+ * Thrown when the server returns a non-2xx response and `throwOnError` is
12
+ * enabled. Provides direct access to the original Response so callers can
13
+ * still read the body.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * try {
18
+ * await api.json<User>('/users/99');
19
+ * } catch (e) {
20
+ * if (e instanceof HttpError) {
21
+ * console.error(e.status, e.statusText);
22
+ * const body = await e.response.json(); // still accessible
23
+ * }
24
+ * }
25
+ * ```
26
+ */
27
+ export declare class HttpError extends FastFetchError {
28
+ readonly status: number;
29
+ readonly statusText: string;
30
+ /** The original Response — body is still consumable. */
31
+ readonly response: Response;
32
+ constructor(response: Response);
33
+ }
34
+ /**
35
+ * Thrown when a request is aborted by FastFetch's own timeout mechanism
36
+ * (not a user-supplied `AbortSignal`).
37
+ */
38
+ export declare class TimeoutError extends FastFetchError {
39
+ readonly timeout: number;
40
+ constructor(timeoutMs: number);
41
+ }
42
+ /**
43
+ * Thrown on network-level failures where no HTTP response was received.
44
+ * Wraps the underlying `TypeError` or `DOMException` as `cause`.
45
+ */
46
+ export declare class NetworkError extends FastFetchError {
47
+ constructor(message: string, cause?: unknown);
48
+ }
49
+ /**
50
+ * Thrown immediately when the circuit breaker is in the OPEN state.
51
+ * No network request is made, protecting the downstream service.
52
+ */
53
+ export declare class CircuitOpenError extends FastFetchError {
54
+ constructor();
55
+ }
56
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAU7C;AAMD;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,SAAU,SAAQ,cAAc;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,wDAAwD;IACxD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;gBAEhB,QAAQ,EAAE,QAAQ;CAO/B;AAMD;;;GAGG;AACH,qBAAa,YAAa,SAAQ,cAAc;IAC9C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAEb,SAAS,EAAE,MAAM;CAK9B;AAMD;;;GAGG;AACH,qBAAa,YAAa,SAAQ,cAAc;gBAClC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAI7C;AAMD;;;GAGG;AACH,qBAAa,gBAAiB,SAAQ,cAAc;;CAQnD"}
package/dist/errors.js ADDED
@@ -0,0 +1,91 @@
1
+ /**
2
+ * FastFetch typed error hierarchy.
3
+ *
4
+ * All errors extend `FastFetchError` so callers can do:
5
+ * catch (e) { if (e instanceof HttpError) { ... } }
6
+ */
7
+ // ---------------------------------------------------------------------------
8
+ // Base
9
+ // ---------------------------------------------------------------------------
10
+ export class FastFetchError extends Error {
11
+ constructor(message, cause) {
12
+ super(message);
13
+ this.name = "FastFetchError";
14
+ if (cause !== undefined) {
15
+ this.cause = cause;
16
+ }
17
+ if (Error.captureStackTrace) {
18
+ Error.captureStackTrace(this, this.constructor);
19
+ }
20
+ }
21
+ }
22
+ // ---------------------------------------------------------------------------
23
+ // HTTP-level error (response received, status not ok)
24
+ // ---------------------------------------------------------------------------
25
+ /**
26
+ * Thrown when the server returns a non-2xx response and `throwOnError` is
27
+ * enabled. Provides direct access to the original Response so callers can
28
+ * still read the body.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * try {
33
+ * await api.json<User>('/users/99');
34
+ * } catch (e) {
35
+ * if (e instanceof HttpError) {
36
+ * console.error(e.status, e.statusText);
37
+ * const body = await e.response.json(); // still accessible
38
+ * }
39
+ * }
40
+ * ```
41
+ */
42
+ export class HttpError extends FastFetchError {
43
+ constructor(response) {
44
+ super(`HTTP ${response.status} ${response.statusText}`);
45
+ this.name = "HttpError";
46
+ this.status = response.status;
47
+ this.statusText = response.statusText;
48
+ this.response = response;
49
+ }
50
+ }
51
+ // ---------------------------------------------------------------------------
52
+ // Timeout error (AbortController fired our timer)
53
+ // ---------------------------------------------------------------------------
54
+ /**
55
+ * Thrown when a request is aborted by FastFetch's own timeout mechanism
56
+ * (not a user-supplied `AbortSignal`).
57
+ */
58
+ export class TimeoutError extends FastFetchError {
59
+ constructor(timeoutMs) {
60
+ super(`Request timed out after ${timeoutMs}ms`);
61
+ this.name = "TimeoutError";
62
+ this.timeout = timeoutMs;
63
+ }
64
+ }
65
+ // ---------------------------------------------------------------------------
66
+ // Network error (no response at all — DNS, CORS, connection refused…)
67
+ // ---------------------------------------------------------------------------
68
+ /**
69
+ * Thrown on network-level failures where no HTTP response was received.
70
+ * Wraps the underlying `TypeError` or `DOMException` as `cause`.
71
+ */
72
+ export class NetworkError extends FastFetchError {
73
+ constructor(message, cause) {
74
+ super(message, cause);
75
+ this.name = "NetworkError";
76
+ }
77
+ }
78
+ // ---------------------------------------------------------------------------
79
+ // Circuit-open error (circuit breaker is OPEN, request immediately rejected)
80
+ // ---------------------------------------------------------------------------
81
+ /**
82
+ * Thrown immediately when the circuit breaker is in the OPEN state.
83
+ * No network request is made, protecting the downstream service.
84
+ */
85
+ export class CircuitOpenError extends FastFetchError {
86
+ constructor() {
87
+ super("Circuit breaker is OPEN — request rejected to protect the downstream service. " +
88
+ "The circuit will attempt a probe request after the configured timeout.");
89
+ this.name = "CircuitOpenError";
90
+ }
91
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Options for controlling FastFetch behavior.
3
+ */
4
+ export interface FastFetchOptions {
5
+ /** Number of retries on failure (default: 0). */
6
+ retries?: number;
7
+ /**
8
+ * Base delay in ms before next retry — applied with exponential backoff (default: 1000).
9
+ * Ignored when a `Retry-After` header is present on a 429 response.
10
+ */
11
+ retryDelay?: number;
12
+ /** Deduplicate identical in-flight requests (default: true). */
13
+ deduplicate?: boolean;
14
+ /**
15
+ * Custom retry predicate. Return `true` to retry.
16
+ * Receives the raw Response (for HTTP errors) or an Error (for network failures),
17
+ * plus the current attempt number (1-indexed).
18
+ * Overrides the built-in smart retry logic when provided.
19
+ */
20
+ shouldRetry?: (errorOrResponse: any, attempt: number) => boolean;
21
+ /**
22
+ * Abort the request after this many milliseconds.
23
+ * Throws a `TimeoutError` when the request is aborted by FastFetch's own timer.
24
+ */
25
+ timeout?: number;
26
+ /**
27
+ * Throw an `HttpError` instead of returning a non-ok Response (default: false).
28
+ * `FastFetchClient` sets this to `true` by default.
29
+ */
30
+ throwOnError?: boolean;
31
+ /**
32
+ * Cache successful GET responses in memory (default: false).
33
+ * Only applies to GET requests.
34
+ */
35
+ fastCache?: boolean;
36
+ /** How long (ms) to keep a cached response fresh (default: 30 000 ms = 30 s). */
37
+ cacheTTL?: number;
38
+ /**
39
+ * Called each time a retry is about to happen.
40
+ * @param attempt The attempt number that just failed (1-indexed).
41
+ * @param error The error/Response that caused the retry.
42
+ * @param delay How long (ms) FastFetch will wait before the next attempt.
43
+ */
44
+ onRetry?: (attempt: number, error: any, delay: number) => void;
45
+ /** Enable verbose debug logging to console (default: false). */
46
+ debug?: boolean;
47
+ }
48
+ /**
49
+ * FastFetch — A smarter `fetch()` wrapper.
50
+ *
51
+ * Features:
52
+ * - **Retry** with exponential backoff (`retries`, `retryDelay`, `shouldRetry`, `onRetry`)
53
+ * - **Smart default retry** — automatically retries 5xx and 429 responses
54
+ * - **Retry-After** — respects the `Retry-After` header on 429 responses
55
+ * - **Timeout** — aborts the request after `timeout` ms
56
+ * - **Deduplication** — merges identical in-flight requests
57
+ * - **TTL Cache** — caches successful GET responses for `cacheTTL` ms
58
+ * - **Debug logging** — optional verbose logging via `debug: true`
59
+ */
60
+ export declare function fastFetch(input: RequestInfo, init?: RequestInit & FastFetchOptions): Promise<Response>;
61
+ /**
62
+ * Manually clear all TTL-cached responses, or a single entry by URL.
63
+ */
64
+ export declare function clearCache(url?: string): void;
65
+ //# sourceMappingURL=fastFetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fastFetch.d.ts","sourceRoot":"","sources":["../src/fastFetch.ts"],"names":[],"mappings":"AAWA;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iDAAiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,CAAC,eAAe,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACjE;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/D,gEAAgE;IAChE,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAsFD;;;;;;;;;;;GAWG;AACH,wBAAsB,SAAS,CAC7B,KAAK,EAAE,WAAW,EAClB,IAAI,CAAC,EAAE,WAAW,GAAG,gBAAgB,GACpC,OAAO,CAAC,QAAQ,CAAC,CAmJnB;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAS7C"}