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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FastFetch offline request queue.
|
|
3
|
+
*
|
|
4
|
+
* When the environment reports `navigator.onLine === false`, mutating requests
|
|
5
|
+
* (POST / PUT / PATCH / DELETE) are held in memory and replayed automatically
|
|
6
|
+
* once the `"online"` event fires.
|
|
7
|
+
*
|
|
8
|
+
* Notes:
|
|
9
|
+
* - Only mutating HTTP methods are queued; GET / HEAD / OPTIONS are always
|
|
10
|
+
* passed through so read operations fail fast with a network error.
|
|
11
|
+
* - This is intentionally an in-memory queue. For durable offline support
|
|
12
|
+
* (e.g. across page refreshes) consider combining with Service Workers or
|
|
13
|
+
* IndexedDB persistence on top of this class.
|
|
14
|
+
* - In non-browser environments (Node.js, Deno) the queue is always disabled
|
|
15
|
+
* (`isOffline` returns false, `start()` is a no-op).
|
|
16
|
+
*/
|
|
17
|
+
import fetch from "cross-fetch";
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Types
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// OfflineQueue
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
export class OfflineQueue {
|
|
26
|
+
constructor() {
|
|
27
|
+
this._queue = [];
|
|
28
|
+
this._listening = false;
|
|
29
|
+
}
|
|
30
|
+
// ── Lifecycle ──────────────────────────────────────────────────────────
|
|
31
|
+
/**
|
|
32
|
+
* Start listening for browser `online` / `offline` events.
|
|
33
|
+
* Safe to call multiple times (idempotent).
|
|
34
|
+
*/
|
|
35
|
+
start() {
|
|
36
|
+
if (this._listening || !this.isBrowser)
|
|
37
|
+
return;
|
|
38
|
+
this._listening = true;
|
|
39
|
+
this._boundOnline = () => void this.replay();
|
|
40
|
+
window.addEventListener("online", this._boundOnline);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Stop listening and discard all queued requests.
|
|
44
|
+
* Call this when you no longer need the queue (e.g. component unmount).
|
|
45
|
+
*/
|
|
46
|
+
stop() {
|
|
47
|
+
if (this._boundOnline) {
|
|
48
|
+
if (typeof window !== "undefined") {
|
|
49
|
+
window.removeEventListener("online", this._boundOnline);
|
|
50
|
+
}
|
|
51
|
+
this._boundOnline = undefined;
|
|
52
|
+
}
|
|
53
|
+
this._listening = false;
|
|
54
|
+
this._queue = [];
|
|
55
|
+
}
|
|
56
|
+
// ── Status ─────────────────────────────────────────────────────────────
|
|
57
|
+
get isBrowser() {
|
|
58
|
+
return typeof window !== "undefined" && typeof navigator !== "undefined";
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* `true` when the environment reports no network connectivity.
|
|
62
|
+
* Always `false` in non-browser environments (Node.js / Deno).
|
|
63
|
+
*/
|
|
64
|
+
get isOffline() {
|
|
65
|
+
return this.isBrowser && !navigator.onLine;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Returns `true` if this request should be queued (mutating method + offline).
|
|
69
|
+
*/
|
|
70
|
+
shouldQueue(method) {
|
|
71
|
+
return this.isOffline && MUTATING_METHODS.has(method.toUpperCase());
|
|
72
|
+
}
|
|
73
|
+
// ── Queue management ───────────────────────────────────────────────────
|
|
74
|
+
/** Add a request to the offline queue. */
|
|
75
|
+
enqueue(url, init) {
|
|
76
|
+
this._queue.push({ url, init, queuedAt: Date.now() });
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Replay all queued requests in chronological order.
|
|
80
|
+
* Requests that fail are returned in the result with `success: false`.
|
|
81
|
+
*/
|
|
82
|
+
async replay() {
|
|
83
|
+
const pending = this._queue.splice(0); // atomic drain
|
|
84
|
+
const results = [];
|
|
85
|
+
for (const req of pending) {
|
|
86
|
+
const method = (req.init.method ?? "GET").toUpperCase();
|
|
87
|
+
try {
|
|
88
|
+
const res = await fetch(req.url, req.init);
|
|
89
|
+
results.push({
|
|
90
|
+
url: req.url,
|
|
91
|
+
method,
|
|
92
|
+
success: res.ok,
|
|
93
|
+
status: res.status,
|
|
94
|
+
});
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
// Non-2xx — don't re-queue, just report
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
// Network error during replay — re-enqueue at front
|
|
101
|
+
this._queue.unshift(req);
|
|
102
|
+
results.push({
|
|
103
|
+
url: req.url,
|
|
104
|
+
method,
|
|
105
|
+
success: false,
|
|
106
|
+
error: err instanceof Error ? err.message : String(err),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return results;
|
|
111
|
+
}
|
|
112
|
+
/** Number of requests currently queued. */
|
|
113
|
+
get size() {
|
|
114
|
+
return this._queue.length;
|
|
115
|
+
}
|
|
116
|
+
/** Snapshot of queued requests (read-only). */
|
|
117
|
+
get queue() {
|
|
118
|
+
return this._queue;
|
|
119
|
+
}
|
|
120
|
+
}
|
package/dist/queue.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FastFetch concurrency-limited async request queue with priority support.
|
|
3
|
+
*
|
|
4
|
+
* Ensures at most `maxConcurrent` requests are in-flight at any time.
|
|
5
|
+
* Excess requests are queued and dispatched as slots become free.
|
|
6
|
+
*/
|
|
7
|
+
export type QueuePriority = "high" | "normal" | "low";
|
|
8
|
+
export declare class RequestQueue {
|
|
9
|
+
private readonly max;
|
|
10
|
+
private _active;
|
|
11
|
+
private readonly waiting;
|
|
12
|
+
/**
|
|
13
|
+
* @param maxConcurrent Maximum number of concurrently running requests.
|
|
14
|
+
* @throws RangeError if `maxConcurrent` < 1.
|
|
15
|
+
*/
|
|
16
|
+
constructor(maxConcurrent: number);
|
|
17
|
+
/**
|
|
18
|
+
* Enqueue a request factory function. If a slot is free it runs immediately;
|
|
19
|
+
* otherwise it waits until one becomes available.
|
|
20
|
+
*
|
|
21
|
+
* @param fn A zero-argument factory that returns a Promise.
|
|
22
|
+
* @param priority Execution priority. High-priority jobs skip ahead of normal/low ones.
|
|
23
|
+
*/
|
|
24
|
+
enqueue<T>(fn: () => Promise<T>, priority?: QueuePriority): Promise<T>;
|
|
25
|
+
private drain;
|
|
26
|
+
/** Number of requests currently queued (not yet started). */
|
|
27
|
+
get pending(): number;
|
|
28
|
+
/** Number of requests currently running. */
|
|
29
|
+
get active(): number;
|
|
30
|
+
/** Configured maximum concurrency. */
|
|
31
|
+
get concurrency(): number;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=queue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queue.d.ts","sourceRoot":"","sources":["../src/queue.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;AAmBtD,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,OAAO,CAAK;IAEpB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IAEjD;;;OAGG;gBACS,aAAa,EAAE,MAAM;IAOjC;;;;;;OAMG;IACH,OAAO,CAAC,CAAC,EACP,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,QAAQ,GAAE,aAAwB,GACjC,OAAO,CAAC,CAAC,CAAC;IAgBb,OAAO,CAAC,KAAK;IAwBb,6DAA6D;IAC7D,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,4CAA4C;IAC5C,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,sCAAsC;IACtC,IAAI,WAAW,IAAI,MAAM,CAExB;CACF"}
|
package/dist/queue.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FastFetch concurrency-limited async request queue with priority support.
|
|
3
|
+
*
|
|
4
|
+
* Ensures at most `maxConcurrent` requests are in-flight at any time.
|
|
5
|
+
* Excess requests are queued and dispatched as slots become free.
|
|
6
|
+
*/
|
|
7
|
+
const PRIORITY_WEIGHT = {
|
|
8
|
+
high: 0,
|
|
9
|
+
normal: 1,
|
|
10
|
+
low: 2,
|
|
11
|
+
};
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// RequestQueue
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
export class RequestQueue {
|
|
16
|
+
/**
|
|
17
|
+
* @param maxConcurrent Maximum number of concurrently running requests.
|
|
18
|
+
* @throws RangeError if `maxConcurrent` < 1.
|
|
19
|
+
*/
|
|
20
|
+
constructor(maxConcurrent) {
|
|
21
|
+
this._active = 0;
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
this.waiting = [];
|
|
24
|
+
if (maxConcurrent < 1) {
|
|
25
|
+
throw new RangeError(`maxConcurrent must be ≥ 1, got ${maxConcurrent}`);
|
|
26
|
+
}
|
|
27
|
+
this.max = maxConcurrent;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Enqueue a request factory function. If a slot is free it runs immediately;
|
|
31
|
+
* otherwise it waits until one becomes available.
|
|
32
|
+
*
|
|
33
|
+
* @param fn A zero-argument factory that returns a Promise.
|
|
34
|
+
* @param priority Execution priority. High-priority jobs skip ahead of normal/low ones.
|
|
35
|
+
*/
|
|
36
|
+
enqueue(fn, priority = "normal") {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
this.waiting.push({ fn, priority, resolve, reject });
|
|
39
|
+
// Sort descending by weight so index 0 is the highest-priority entry.
|
|
40
|
+
// Use a stable insertion to avoid reordering equal-priority entries.
|
|
41
|
+
this.waiting.sort((a, b) => PRIORITY_WEIGHT[a.priority] - PRIORITY_WEIGHT[b.priority]);
|
|
42
|
+
this.drain();
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
// ── Internal ─────────────────────────────────────────────────────────────
|
|
46
|
+
drain() {
|
|
47
|
+
while (this._active < this.max && this.waiting.length > 0) {
|
|
48
|
+
const entry = this.waiting.shift();
|
|
49
|
+
this._active++;
|
|
50
|
+
entry
|
|
51
|
+
.fn()
|
|
52
|
+
.then((value) => {
|
|
53
|
+
this._active--;
|
|
54
|
+
entry.resolve(value);
|
|
55
|
+
this.drain();
|
|
56
|
+
}, (err) => {
|
|
57
|
+
this._active--;
|
|
58
|
+
entry.reject(err);
|
|
59
|
+
this.drain();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// ── Observability ────────────────────────────────────────────────────────
|
|
64
|
+
/** Number of requests currently queued (not yet started). */
|
|
65
|
+
get pending() {
|
|
66
|
+
return this.waiting.length;
|
|
67
|
+
}
|
|
68
|
+
/** Number of requests currently running. */
|
|
69
|
+
get active() {
|
|
70
|
+
return this._active;
|
|
71
|
+
}
|
|
72
|
+
/** Configured maximum concurrency. */
|
|
73
|
+
get concurrency() {
|
|
74
|
+
return this.max;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FastFetch Server-Sent Events (SSE) streaming support.
|
|
3
|
+
*
|
|
4
|
+
* Consumes a `ReadableStream` from a `fetch()` Response body and calls
|
|
5
|
+
* `handler` for each well-formed SSE event received.
|
|
6
|
+
*
|
|
7
|
+
* SSE spec reference: https://html.spec.whatwg.org/#server-sent-events
|
|
8
|
+
*/
|
|
9
|
+
export interface SSEEvent {
|
|
10
|
+
/** Event type field (defaults to `"message"` if not specified by server). */
|
|
11
|
+
type: string;
|
|
12
|
+
/** The `data` field value. Multi-line data fields are joined with `\n`. */
|
|
13
|
+
data: string;
|
|
14
|
+
/** Optional `id` field from the server. */
|
|
15
|
+
id?: string;
|
|
16
|
+
/** Optional `retry` field in milliseconds. */
|
|
17
|
+
retry?: number;
|
|
18
|
+
}
|
|
19
|
+
export type SSEHandler = (event: SSEEvent) => void | Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Consume a Server-Sent Events stream and invoke `handler` for each event.
|
|
22
|
+
*
|
|
23
|
+
* The function resolves when the stream ends (or `signal` is aborted).
|
|
24
|
+
* The underlying reader is always released in the `finally` block.
|
|
25
|
+
*
|
|
26
|
+
* @param reader A `ReadableStreamDefaultReader<Uint8Array>` from `response.body.getReader()`.
|
|
27
|
+
* @param handler Called for each complete SSE event.
|
|
28
|
+
* @param signal Optional `AbortSignal` to stop consuming early.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* const res = await fetch('/api/stream');
|
|
33
|
+
* if (!res.body) throw new Error('No body');
|
|
34
|
+
* await consumeSSE(res.body.getReader(), (event) => {
|
|
35
|
+
* console.log(event.type, event.data);
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare function consumeSSE(reader: ReadableStreamDefaultReader<Uint8Array>, handler: SSEHandler, signal?: AbortSignal): Promise<void>;
|
|
40
|
+
//# sourceMappingURL=streaming.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streaming.d.ts","sourceRoot":"","sources":["../src/streaming.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,MAAM,WAAW,QAAQ;IACvB,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,2CAA2C;IAC3C,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,8CAA8C;IAC9C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAMnE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,2BAA2B,CAAC,UAAU,CAAC,EAC/C,OAAO,EAAE,UAAU,EACnB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAyEf"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FastFetch Server-Sent Events (SSE) streaming support.
|
|
3
|
+
*
|
|
4
|
+
* Consumes a `ReadableStream` from a `fetch()` Response body and calls
|
|
5
|
+
* `handler` for each well-formed SSE event received.
|
|
6
|
+
*
|
|
7
|
+
* SSE spec reference: https://html.spec.whatwg.org/#server-sent-events
|
|
8
|
+
*/
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// consumeSSE
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
/**
|
|
13
|
+
* Consume a Server-Sent Events stream and invoke `handler` for each event.
|
|
14
|
+
*
|
|
15
|
+
* The function resolves when the stream ends (or `signal` is aborted).
|
|
16
|
+
* The underlying reader is always released in the `finally` block.
|
|
17
|
+
*
|
|
18
|
+
* @param reader A `ReadableStreamDefaultReader<Uint8Array>` from `response.body.getReader()`.
|
|
19
|
+
* @param handler Called for each complete SSE event.
|
|
20
|
+
* @param signal Optional `AbortSignal` to stop consuming early.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const res = await fetch('/api/stream');
|
|
25
|
+
* if (!res.body) throw new Error('No body');
|
|
26
|
+
* await consumeSSE(res.body.getReader(), (event) => {
|
|
27
|
+
* console.log(event.type, event.data);
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export async function consumeSSE(reader, handler, signal) {
|
|
32
|
+
const decoder = new TextDecoder("utf-8");
|
|
33
|
+
let buffer = "";
|
|
34
|
+
function parseAndEmit(block) {
|
|
35
|
+
// SSE field defaults
|
|
36
|
+
let eventType = "message";
|
|
37
|
+
let data = "";
|
|
38
|
+
let id;
|
|
39
|
+
let retry;
|
|
40
|
+
for (const line of block.split("\n")) {
|
|
41
|
+
const colon = line.indexOf(":");
|
|
42
|
+
if (colon === -1)
|
|
43
|
+
continue; // ignore malformed lines
|
|
44
|
+
const field = line.slice(0, colon).trim();
|
|
45
|
+
// A space immediately after ":" is part of the spec and must be stripped.
|
|
46
|
+
const value = line.slice(colon + 1).replace(/^ /, "");
|
|
47
|
+
switch (field) {
|
|
48
|
+
case "event":
|
|
49
|
+
eventType = value;
|
|
50
|
+
break;
|
|
51
|
+
case "data":
|
|
52
|
+
data = data ? `${data}\n${value}` : value;
|
|
53
|
+
break;
|
|
54
|
+
case "id":
|
|
55
|
+
id = value;
|
|
56
|
+
break;
|
|
57
|
+
case "retry": {
|
|
58
|
+
const ms = parseInt(value, 10);
|
|
59
|
+
if (!isNaN(ms))
|
|
60
|
+
retry = ms;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
// "comment" lines (field === "") are silently ignored per spec
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Dispatch only if a data field was present (spec §9.2.6)
|
|
67
|
+
if (data !== "") {
|
|
68
|
+
void handler({ type: eventType, data, id, retry });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
while (true) {
|
|
73
|
+
if (signal?.aborted)
|
|
74
|
+
break;
|
|
75
|
+
const { done, value } = await reader.read();
|
|
76
|
+
if (done)
|
|
77
|
+
break;
|
|
78
|
+
buffer += decoder.decode(value, { stream: true });
|
|
79
|
+
// Events are separated by two newlines (\n\n or \r\n\r\n)
|
|
80
|
+
// Split on double-newline — keep trailing incomplete chunk in buffer.
|
|
81
|
+
const parts = buffer.split(/\n\n|\r\n\r\n/);
|
|
82
|
+
buffer = parts.pop() ?? "";
|
|
83
|
+
for (const block of parts) {
|
|
84
|
+
const trimmed = block.trim();
|
|
85
|
+
if (trimmed) {
|
|
86
|
+
parseAndEmit(trimmed);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Flush any remaining data in the buffer
|
|
91
|
+
if (buffer.trim()) {
|
|
92
|
+
parseAndEmit(buffer.trim());
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
reader.releaseLock();
|
|
97
|
+
}
|
|
98
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// FastFetch core options
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
export interface FastFetchOptions {
|
|
6
|
+
/** Number of retries on failure (default: 0). */
|
|
7
|
+
retries?: number;
|
|
8
|
+
/**
|
|
9
|
+
* Base delay in ms before next retry — applied with exponential backoff (default: 1000).
|
|
10
|
+
* Ignored when a `Retry-After` header is present on a 429 response.
|
|
11
|
+
*/
|
|
12
|
+
retryDelay?: number;
|
|
13
|
+
/** Deduplicate identical in-flight requests (default: true). */
|
|
14
|
+
deduplicate?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Custom retry predicate. Return `true` to retry.
|
|
17
|
+
* Receives the raw Response (for HTTP errors) or an Error (for network failures),
|
|
18
|
+
* plus the current attempt count (1-indexed).
|
|
19
|
+
* Overrides the built-in smart retry logic when provided.
|
|
20
|
+
*/
|
|
21
|
+
shouldRetry?: (errorOrResponse: any, attempt: number) => boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Abort the request after this many milliseconds.
|
|
24
|
+
* Throws a `DOMException` with name `"AbortError"` on timeout.
|
|
25
|
+
*/
|
|
26
|
+
timeout?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Cache successful GET responses in memory (default: false).
|
|
29
|
+
* Only applies to GET requests.
|
|
30
|
+
*/
|
|
31
|
+
cache?: boolean;
|
|
32
|
+
/** How long (ms) to keep a cached response fresh (default: 30 000). */
|
|
33
|
+
cacheTTL?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Called each time a retry is about to happen.
|
|
36
|
+
* @param attempt The attempt number that just failed (1-indexed).
|
|
37
|
+
* @param error The error or Response that triggered the retry.
|
|
38
|
+
* @param delay How long (ms) FastFetch will wait before the next attempt.
|
|
39
|
+
*/
|
|
40
|
+
onRetry?: (attempt: number, error: any, delay: number) => void;
|
|
41
|
+
/** Enable verbose debug logging to console (default: false). */
|
|
42
|
+
debug?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The main fastFetch function — a smarter fetch() wrapper. */
|
|
46
|
+
export declare function fastFetch(
|
|
47
|
+
input: RequestInfo,
|
|
48
|
+
init?: RequestInit & FastFetchOptions,
|
|
49
|
+
): Promise<Response>;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Clear all TTL-cached responses, or only entries matching a specific URL.
|
|
53
|
+
* @param url If supplied, only cache entries whose key contains this string are removed.
|
|
54
|
+
*/
|
|
55
|
+
export declare function clearCache(url?: string): void;
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// createClient — interceptors & types
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
/** Mutates and returns the request init before it is sent. */
|
|
62
|
+
export type RequestInterceptor = (
|
|
63
|
+
url: string,
|
|
64
|
+
init: RequestInit & FastFetchOptions,
|
|
65
|
+
) => (RequestInit & FastFetchOptions) | Promise<RequestInit & FastFetchOptions>;
|
|
66
|
+
|
|
67
|
+
/** Transforms the Response after a successful fetch. */
|
|
68
|
+
export type ResponseInterceptor = (
|
|
69
|
+
response: Response,
|
|
70
|
+
) => Response | Promise<Response>;
|
|
71
|
+
|
|
72
|
+
/** Called when a request ultimately fails (after all retries). */
|
|
73
|
+
export type ErrorInterceptor = (error: unknown) => unknown | Promise<unknown>;
|
|
74
|
+
|
|
75
|
+
/** Options passed to `createClient()`. */
|
|
76
|
+
export interface ClientOptions extends FastFetchOptions {
|
|
77
|
+
/**
|
|
78
|
+
* Base URL prepended to relative paths.
|
|
79
|
+
* @example `'https://api.example.com/v1'`
|
|
80
|
+
*/
|
|
81
|
+
baseURL?: string;
|
|
82
|
+
/** Default headers merged into every request. */
|
|
83
|
+
headers?: Record<string, string>;
|
|
84
|
+
interceptors?: {
|
|
85
|
+
/** Run before the request — inject auth, signing, tracing headers, etc. */
|
|
86
|
+
request?: RequestInterceptor[];
|
|
87
|
+
/** Run after a successful response — unwrap envelopes, log, transform, etc. */
|
|
88
|
+
response?: ResponseInterceptor[];
|
|
89
|
+
/** Run when a request ultimately fails — centralise error handling. */
|
|
90
|
+
error?: ErrorInterceptor[];
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A pre-configured FastFetch client with base URL, default headers,
|
|
96
|
+
* interceptors, and typed HTTP method shorthands.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* const api = createClient({
|
|
101
|
+
* baseURL: 'https://api.example.com',
|
|
102
|
+
* headers: { Authorization: 'Bearer token' },
|
|
103
|
+
* retries: 3,
|
|
104
|
+
* timeout: 8000,
|
|
105
|
+
* });
|
|
106
|
+
*
|
|
107
|
+
* const user = await api.json<User>('/users/1');
|
|
108
|
+
* const posts = await api.json<Post[]>('/posts');
|
|
109
|
+
* await api.post('/users', { name: 'Alice' });
|
|
110
|
+
* await api.delete('/users/42');
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export declare class FastFetchClient {
|
|
114
|
+
constructor(options?: ClientOptions);
|
|
115
|
+
|
|
116
|
+
/** Core fetch — resolves URL, runs interceptors, returns raw Response. */
|
|
117
|
+
fetch(path: string, init?: RequestInit & FastFetchOptions): Promise<Response>;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Fetch and auto-parse JSON. Returns typed `Promise<T>`.
|
|
121
|
+
* @example `const user = await api.json<User>('/users/1');`
|
|
122
|
+
*/
|
|
123
|
+
json<T = unknown>(
|
|
124
|
+
path: string,
|
|
125
|
+
init?: RequestInit & FastFetchOptions,
|
|
126
|
+
): Promise<T>;
|
|
127
|
+
|
|
128
|
+
get(path: string, init?: RequestInit & FastFetchOptions): Promise<Response>;
|
|
129
|
+
|
|
130
|
+
post<T = unknown>(
|
|
131
|
+
path: string,
|
|
132
|
+
body?: T,
|
|
133
|
+
init?: RequestInit & FastFetchOptions,
|
|
134
|
+
): Promise<Response>;
|
|
135
|
+
|
|
136
|
+
put<T = unknown>(
|
|
137
|
+
path: string,
|
|
138
|
+
body?: T,
|
|
139
|
+
init?: RequestInit & FastFetchOptions,
|
|
140
|
+
): Promise<Response>;
|
|
141
|
+
|
|
142
|
+
patch<T = unknown>(
|
|
143
|
+
path: string,
|
|
144
|
+
body?: T,
|
|
145
|
+
init?: RequestInit & FastFetchOptions,
|
|
146
|
+
): Promise<Response>;
|
|
147
|
+
|
|
148
|
+
delete(path: string, init?: RequestInit & FastFetchOptions): Promise<Response>;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Create a pre-configured `FastFetchClient` instance.
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```ts
|
|
156
|
+
* const api = createClient({
|
|
157
|
+
* baseURL: 'https://api.example.com/v1',
|
|
158
|
+
* headers: { Authorization: `Bearer ${token}` },
|
|
159
|
+
* retries: 3,
|
|
160
|
+
* timeout: 8000,
|
|
161
|
+
* interceptors: {
|
|
162
|
+
* request: [(url, init) => { init.headers['X-Trace'] = 'abc'; return init; }],
|
|
163
|
+
* },
|
|
164
|
+
* });
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
export declare function createClient(options?: ClientOptions): FastFetchClient;
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
preset: "ts-jest/presets/default-esm",
|
|
3
|
+
testEnvironment: "node",
|
|
4
|
+
transform: {
|
|
5
|
+
"^.+\\.tsx?$": [
|
|
6
|
+
"ts-jest",
|
|
7
|
+
{
|
|
8
|
+
useESM: true,
|
|
9
|
+
},
|
|
10
|
+
],
|
|
11
|
+
},
|
|
12
|
+
moduleNameMapper: {
|
|
13
|
+
"^(\\.{1,2}/.*)\\.js$": "$1",
|
|
14
|
+
},
|
|
15
|
+
extensionsToTreatAsEsm: [".ts"],
|
|
16
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fastfetch-api-fetch-enhancer",
|
|
3
|
+
"displayName": "FastFetch – Smart API Fetcher",
|
|
4
|
+
"version": "2.0.0",
|
|
5
|
+
"description": "Production-grade HTTP client with circuit breakers, concurrency queues, metrics, SSE streaming, and typed errors.",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"test": "jest --coverage",
|
|
18
|
+
"prepublishOnly": "npm run build",
|
|
19
|
+
"demo": "node __tests__/demo.js",
|
|
20
|
+
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,css,scss,md,html}\""
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"fetch",
|
|
24
|
+
"http-client",
|
|
25
|
+
"circuit-breaker",
|
|
26
|
+
"middleware",
|
|
27
|
+
"concurrency-queue",
|
|
28
|
+
"sse",
|
|
29
|
+
"telemetry",
|
|
30
|
+
"offline-queue",
|
|
31
|
+
"retry",
|
|
32
|
+
"axios-alternative"
|
|
33
|
+
],
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/softsideof/FastFetch-API-Fetch-Enhancer.git"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/softsideof/FastFetch-API-Fetch-Enhancer/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/softsideof/FastFetch-API-Fetch-Enhancer#readme",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"author": "softsideof",
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/jest": "^29.0.0",
|
|
46
|
+
"jest": "^29.0.0",
|
|
47
|
+
"prettier": "^3.5.3",
|
|
48
|
+
"ts-jest": "^29.0.0",
|
|
49
|
+
"ts-node": "^10.9.2",
|
|
50
|
+
"typescript": "^5.0.0"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"cross-fetch": "^4.1.0"
|
|
54
|
+
}
|
|
55
|
+
}
|