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.
- package/.idea/FastFetch-Smart-API-Fetcher.iml +12 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/LICENSE +21 -0
- package/README.md +101 -0
- package/__tests__/demo.test.ts +216 -0
- package/__tests__/test_database.json +5002 -0
- package/coverage/clover.xml +463 -0
- package/coverage/coverage-final.json +11 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/circuit-breaker.ts.html +547 -0
- package/coverage/lcov-report/client.ts.html +1858 -0
- package/coverage/lcov-report/errors.ts.html +415 -0
- package/coverage/lcov-report/fastFetch.ts.html +1045 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +251 -0
- package/coverage/lcov-report/index.ts.html +241 -0
- package/coverage/lcov-report/metrics.ts.html +685 -0
- package/coverage/lcov-report/middleware.ts.html +403 -0
- package/coverage/lcov-report/offline-queue.ts.html +535 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/queue.ts.html +421 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +196 -0
- package/coverage/lcov-report/streaming.ts.html +466 -0
- package/coverage/lcov.info +908 -0
- package/dist/circuit-breaker.d.ts +61 -0
- package/dist/circuit-breaker.d.ts.map +1 -0
- package/dist/circuit-breaker.js +106 -0
- package/dist/client.d.ts +215 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +391 -0
- package/dist/errors.d.ts +56 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +91 -0
- package/dist/fastFetch.d.ts +65 -0
- package/dist/fastFetch.d.ts.map +1 -0
- package/dist/fastFetch.js +209 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/metrics.d.ts +71 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +131 -0
- package/dist/middleware.d.ts +66 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +45 -0
- package/dist/offline-queue.d.ts +65 -0
- package/dist/offline-queue.d.ts.map +1 -0
- package/dist/offline-queue.js +120 -0
- package/dist/queue.d.ts +33 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +76 -0
- package/dist/streaming.d.ts +40 -0
- package/dist/streaming.d.ts.map +1 -0
- package/dist/streaming.js +98 -0
- package/index.d.ts +167 -0
- package/jest.config.js +16 -0
- package/package.json +55 -0
- package/src/circuit-breaker.ts +154 -0
- package/src/client.ts +591 -0
- package/src/errors.ts +110 -0
- package/src/fastFetch.ts +320 -0
- package/src/index.ts +52 -0
- package/src/metrics.ts +200 -0
- package/src/middleware.ts +106 -0
- package/src/offline-queue.ts +150 -0
- package/src/queue.ts +112 -0
- package/src/streaming.ts +127 -0
- 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
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -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"}
|