@fetchkit/ffetch 5.6.0 → 5.6.2

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 CHANGED
@@ -1,417 +1,418 @@
1
- ![npm](https://img.shields.io/npm/v/@fetchkit/ffetch)
2
- ![Downloads](https://img.shields.io/npm/dm/@fetchkit/ffetch)
3
- ![GitHub stars](https://img.shields.io/github/stars/fetch-kit/ffetch?style=social)
4
-
5
- ![Build](https://github.com/fetch-kit/ffetch/actions/workflows/ci.yml/badge.svg)
6
- ![codecov](https://codecov.io/gh/fetch-kit/ffetch/branch/main/graph/badge.svg)
7
- [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/fetch-kit/ffetch/badge)](https://scorecard.dev/viewer/?uri=github.com/fetch-kit/ffetch)
8
-
9
- ![MIT](https://img.shields.io/npm/l/@fetchkit/ffetch)
10
- ![bundlephobia](https://badgen.net/bundlephobia/minzip/@fetchkit/ffetch)
11
- ![Types](https://img.shields.io/npm/types/@fetchkit/ffetch)
12
-
13
- # @fetchkit/ffetch
1
+ ![npm](https://img.shields.io/npm/v/@fetchkit/ffetch)
2
+ ![Downloads](https://img.shields.io/npm/dm/@fetchkit/ffetch)
3
+ ![GitHub stars](https://img.shields.io/github/stars/fetch-kit/ffetch?style=social)
14
4
 
5
+ ![Build](https://github.com/fetch-kit/ffetch/actions/workflows/ci.yml/badge.svg)
6
+ ![codecov](https://codecov.io/gh/fetch-kit/ffetch/branch/main/graph/badge.svg)
7
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/fetch-kit/ffetch/badge)](https://scorecard.dev/viewer/?uri=github.com/fetch-kit/ffetch)
8
+ [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/14166/badge)](https://www.bestpractices.dev/projects/14166)
9
+
10
+ ![MIT](https://img.shields.io/npm/l/@fetchkit/ffetch)
11
+ ![bundlephobia](https://badgen.net/bundlephobia/minzip/@fetchkit/ffetch)
12
+ ![Types](https://img.shields.io/npm/types/@fetchkit/ffetch)
13
+
14
+ # @fetchkit/ffetch
15
15
 
16
16
  > Part of the **[fetch-kit ecosystem](https://fetchkit.org)** - production-ready fetch utilities and chaos-testing tools.
17
-
18
- **A production-ready TypeScript-first drop-in replacement for native fetch, or any fetch-compatible implementation.**
19
-
20
- ffetch can wrap any fetch-compatible implementation (native fetch, node-fetch, undici, or framework-provided fetch), making it flexible for SSR, edge, and custom environments.
21
-
22
- ffetch uses a plugin architecture for optional features, so you only include what you need.
23
-
24
- ## Why ffetch
25
-
26
- - Keep native fetch ergonomics, add production safety (timeouts, retries, error strategy).
27
- - Keep your runtime flexibility (use global fetch or any fetch-compatible handler).
28
- - Keep your bundle lean – **~3kb minified** (optional plugins, zero runtime dependencies).
29
-
30
- ## Table of Contents
31
-
32
- - [@fetchkit/ffetch](#fetchkitffetch)
33
- - [Why ffetch](#why-ffetch)
34
- - [Table of Contents](#table-of-contents)
35
- - [Key Features](#key-features)
36
- - [Built-in Plugins at a Glance](#built-in-plugins-at-a-glance)
37
- - [What Problems Does ffetch Solve?](#what-problems-does-ffetch-solve)
38
- - [Quick Start](#quick-start)
39
- - [Install](#install)
40
- - [Basic Setup](#basic-setup)
41
- - [Production Setup with Plugins](#production-setup-with-plugins)
42
- - [Why not only native fetch?](#why-not-only-native-fetch)
43
- - [Common Recipes](#common-recipes)
44
- - [Using a Custom fetchHandler (SSR, metaframeworks, or polyfills)](#using-a-custom-fetchhandler-ssr-metaframeworks-or-polyfills)
45
- - [Advanced Example](#advanced-example)
46
- - [Custom Error Handling with `throwOnHttpError`](#custom-error-handling-with-throwonhttperror)
47
- - [Documentation](#documentation)
48
- - [Environment Requirements](#environment-requirements)
49
- - ["AbortSignal.any is not a function"](#abortsignalany-is-not-a-function)
50
- - [CDN Usage](#cdn-usage)
51
- - [Deduplication Limitations](#deduplication-limitations)
52
- - [Fetch vs. Axios vs. ky vs. `ffetch`](#fetch-vs-axios-vs-ky-vs-ffetch)
53
- - [Try ffetch in Action](#try-ffetch-in-action)
54
- - [Join the Community](#join-the-community)
55
- - [Security](#security)
56
- - [Contributing](#contributing)
57
- - [License](#license)
58
-
59
- ## Key Features
60
-
61
- - **Timeouts** – per-request or global
62
- - **Retries** – exponential backoff + jitter
63
- - **Abort-aware retries** – aborting during backoff cancels immediately
64
- - **Plugin architecture** – extensible lifecycle-based plugins for optional behavior
65
- - **Hooks** – logging, auth, metrics, request/response transformation
66
- - **Pending requests** – real-time monitoring of active requests
67
- - **Per-request overrides** – customize behavior on a per-request basis
68
- - **Universal** – Node.js, Browser, Cloudflare Workers, React Native
69
- - **Zero runtime deps** – ships as dual ESM/CJS
70
- - **Configurable error handling** – custom error types and `throwOnHttpError` flag to throw on HTTP errors
71
- - **Bulkhead plugin (optional, prebuilt)** – cap concurrency and queue depth per client instance
72
- - **Circuit breaker plugin (optional, prebuilt)** – automatic failure protection
73
- - **Hedge plugin (optional, prebuilt)** – race parallel attempts to reduce tail latency
74
- - **Context ID plugin (optional, prebuilt)** – inject a stable context ID header across retries/hedges for correlation
75
- - **Deduplication plugin (optional, prebuilt)** – automatic deduping of in-flight identical requests
76
- - **Request shortcuts plugin (optional, prebuilt)** – call `client.get(url)` / `.post()` / `.put()` / `.patch()` / `.delete()` directly on the client
77
- - **Response shortcuts plugin (optional, prebuilt)** – call `client(url).json()` / `.text()` / `.blob()` directly on the request promise
78
- - **Download progress plugin (optional, prebuilt)** – stream download progress callbacks with bytes transferred and percentage
79
-
80
- **Built-in error classes:** `TimeoutError`, `RetryLimitError`, `CircuitOpenError`, `BulkheadFullError`, `HttpError`, `NetworkError`, `AbortError`
81
-
82
- ### Built-in Plugins at a Glance
83
-
84
- All plugins are tree-shakeable — import only what you use.
85
-
86
- - **dedupePlugin (optional)**: dedupe in-flight identical requests.
87
- - **bulkheadPlugin (optional)**: cap in-flight concurrency with optional queue backpressure.
88
- - **hedgePlugin (optional)**: race multiple attempts and cancel losers when a winner is found.
89
- - **circuitPlugin (optional)**: fail fast after repeated failures.
90
- - **contextIdPlugin (optional)**: inject a stable request context ID (for example in `x-context-id`) across retries and hedges.
91
- - **requestShortcutsPlugin (optional)**: HTTP method shortcuts on the client (`.get()` / `.post()` / `.put()` / `.patch()` / `.delete()` / `.head()` / `.options()`).
92
- - **responseShortcutsPlugin (optional)**: use `client(url).json()` / `.text()` / `.blob()` style parsing.
93
- - **downloadProgressPlugin (optional)**: stream download progress via `onProgress(progress, chunk)` callback.
94
-
95
- ## What Problems Does ffetch Solve?
96
-
97
- ffetch is ideal for:
98
-
99
- - **Microservices and REST APIs** with retry requirements and timeout control
100
- - **High-traffic client applications** that need in-flight deduplication and circuit breaker protection
101
- - **SSR and metaframework apps** that require runtime flexibility (custom fetch handlers for different environments)
102
- - **Type-safe request handling** with strong TypeScript support and zero runtime dependencies
103
-
104
- ## Quick Start
105
-
106
- Migrating from v4? Start with the [migration guide](./docs/migration.md) before applying the examples below.
107
-
108
- ### Install
109
-
110
- ```bash
111
- # npm
112
- npm install @fetchkit/ffetch
113
-
114
- # yarn
115
- yarn add @fetchkit/ffetch
116
-
117
- # pnpm
118
- pnpm add @fetchkit/ffetch
119
-
120
- # bun
121
- bun add @fetchkit/ffetch
122
- ```
123
-
124
- ### Basic Setup
125
-
126
- ```typescript
127
- import { createClient } from '@fetchkit/ffetch'
128
-
129
- type User = { id: number; name: string }
130
-
131
- const api = createClient({ timeout: 5000, retries: 2 })
132
- const response = await api('https://api.example.com/users')
133
-
134
- if (!response.ok) {
135
- throw new Error(`Request failed: ${response.status}`)
136
- }
137
-
138
- const users = (await response.json()) as User[]
139
- ```
140
-
141
- ### Production Setup with Plugins
142
-
143
- ```typescript
144
- import { createClient } from '@fetchkit/ffetch'
145
- import { dedupePlugin } from '@fetchkit/ffetch/plugins/dedupe'
146
- import { circuitPlugin } from '@fetchkit/ffetch/plugins/circuit'
147
- import { contextIdPlugin } from '@fetchkit/ffetch/plugins/context-id'
148
- import { requestShortcutsPlugin } from '@fetchkit/ffetch/plugins/request-shortcuts'
149
- import { responseShortcutsPlugin } from '@fetchkit/ffetch/plugins/response-shortcuts'
150
-
151
- const api = createClient({
152
- timeout: 10_000,
153
- retries: 2,
154
- plugins: [
155
- // 1) Optional: dedupe identical in-flight requests
156
- dedupePlugin({ ttl: 30_000, sweepInterval: 5_000 }),
157
- // 2) Optional: open the circuit after repeated failures
158
- circuitPlugin({ threshold: 5, reset: 30_000 }),
159
- // 3) Optional: inject stable correlation context IDs
160
- contextIdPlugin(),
161
- // 4) Optional: enable request-promise parsing shortcuts
162
- responseShortcutsPlugin(),
163
- // 5) Optional: enable client HTTP method shortcuts
164
- requestShortcutsPlugin(),
165
- ],
166
- })
167
-
168
- const users = await api
169
- .get('https://api.example.com/users')
170
- .json<Array<{ id: number; name: string }>>()
171
-
172
- const p1 = api('https://api.example.com/data')
173
- const p2 = api('https://api.example.com/data')
174
- const [res1, res2] = await Promise.all([p1, p2])
175
- ```
176
-
177
- What this setup gives you:
178
-
179
- - **Operational safety**: retries with timeout defaults.
180
- - **Lower duplicate traffic (optional)**: concurrent identical requests share one in-flight call.
181
- - **Faster failure recovery (optional)**: circuit breaker blocks repeated failing calls.
182
- - **Better observability correlation (optional)**: stable request context IDs across retries and hedges.
183
- - **Cleaner request ergonomics (optional)**: `client.get(url)` / `.post(url, init)` style shortcuts.
184
- - **Cleaner parsing (optional)**: `client(url).json()` style shortcuts.
185
-
186
- ### Why not only native fetch?
187
-
188
- - Native fetch is a great baseline, but production apps usually need retries and timeout control.
189
- - ffetch keeps the fetch model and adds optional resilience features.
190
- - You can keep strict native behavior and only opt into plugins you need.
191
-
192
- ### Common Recipes
193
-
194
- ```typescript
195
- // Throw on non-2xx/429 once retries are exhausted
196
- const strict = createClient({ throwOnHttpError: true })
197
-
198
- // Use a custom fetch implementation (SSR/framework/runtime)
199
- import nodeFetch from 'node-fetch'
200
- const apiWithCustomHandler = createClient({ fetchHandler: nodeFetch })
201
-
202
- // Keep native Response flow (works with or without plugins)
203
- const plainApi = createClient({ timeout: 5000 })
204
- const response = await plainApi('https://api.example.com/health')
205
- const text = await response.text()
206
- ```
207
-
208
- ### Using a Custom fetchHandler (SSR, metaframeworks, or polyfills)
209
-
210
- ```typescript
211
- // Why this exists:
212
- // ffetch wraps whatever fetch-compatible function you provide.
213
- // This is useful when your runtime has a scoped/framework fetch,
214
- // or when Node needs an explicit fetch implementation.
215
-
216
- import { createClient } from '@fetchkit/ffetch'
217
- import nodeFetch from 'node-fetch'
218
-
219
- // Node.js example: provide node-fetch explicitly
220
- const apiNode = createClient({
221
- fetchHandler: nodeFetch,
222
- timeout: 5000,
223
- })
224
- const nodeResponse = await apiNode('https://api.example.com/data')
225
-
226
- // Framework example: pass the framework-scoped fetch
227
- // (e.g. the fetch passed into a request handler)
228
- async function loadData(frameworkFetch: typeof fetch) {
229
- const api = createClient({
230
- fetchHandler: frameworkFetch,
231
- timeout: 5000,
232
- })
233
-
234
- const response = await api('/internal/data')
235
- return response.json()
236
- }
237
- ```
238
-
239
- All ffetch features (timeouts, retries, plugins, hooks) behave the same with a custom `fetchHandler`.
240
-
241
- With `responseShortcutsPlugin()` enabled, request-promise shortcuts like `api(url).json()` also work the same.
242
-
243
- ### Advanced Example
244
-
245
- ```typescript
246
- // Production-ready client with error handling and monitoring
247
- import { createClient } from '@fetchkit/ffetch'
248
- import { dedupePlugin } from '@fetchkit/ffetch/plugins/dedupe'
249
- import { circuitPlugin } from '@fetchkit/ffetch/plugins/circuit'
250
-
251
- const client = createClient({
252
- timeout: 10000,
253
- retries: 2,
254
- fetchHandler: fetch, // Use custom fetch if needed
255
- plugins: [
256
- dedupePlugin({
257
- hashFn: (params) => `${params.method}|${params.url}|${params.body}`,
258
- ttl: 30_000,
259
- sweepInterval: 5_000,
260
- }),
261
- circuitPlugin({
262
- threshold: 5,
263
- reset: 30_000,
264
- onCircuitOpen: ({ request, reason }) =>
265
- console.warn('Circuit opened due to:', request.url, reason.type),
266
- onCircuitClose: ({ request, response }) =>
267
- console.info('Circuit closed after:', request.url, response.status),
268
- }),
269
- ],
270
- hooks: {
271
- before: async (req) => console.log('→', req.url),
272
- after: async (req, res) => console.log('←', res.status),
273
- onError: async (req, err) => console.error('Error:', err.message),
274
- },
275
- })
276
-
277
- try {
278
- const response = await client('/api/data')
279
-
280
- // Check HTTP status manually (like native fetch)
281
- if (!response.ok) {
282
- console.log('HTTP error:', response.status)
283
- return
284
- }
285
-
286
- const data = await response.json()
287
- console.log('Active requests:', client.pendingRequests.length)
288
- } catch (err) {
289
- if (err instanceof TimeoutError) {
290
- console.log('Request timed out')
291
- } else if (err instanceof RetryLimitError) {
292
- console.log('Request failed after retries')
293
- }
294
- }
295
- ```
296
-
297
- ### Custom Error Handling with `throwOnHttpError`
298
-
299
- Native `fetch`'s controversial behavior of not throwing errors for HTTP error status codes (4xx, 5xx) can lead to overlooked errors in applications. By default, `ffetch` follows this same pattern, returning a `Response` object regardless of the HTTP status code. However, with the `throwOnHttpError` flag, developers can configure `ffetch` to throw an `HttpError` for HTTP error responses, making error handling more explicit and robust. Note that this behavior is affected by retries and the circuit breaker - full details are explained in the [Error Handling documentation](./docs/errorhandling.md).
300
-
301
- ## Documentation
302
-
303
- | Topic | Description |
304
- | ------------------------------------------------------------ | ------------------------------------------------------------------------- |
305
- | **[Complete Documentation](./docs/index.md)** | **Start here** - Documentation index and overview |
306
- | **[API Reference](./docs/api.md)** | Complete API documentation and configuration options |
307
- | **[Plugin Architecture](./docs/plugins.md)** | Plugin lifecycle, custom plugin authoring, and integration patterns |
308
- | **[Deduplication](./docs/deduplication.md)** | How deduplication works, hash config, optional TTL cleanup, limitations |
309
- | **[Error Handling](./docs/errorhandling.md)** | Strategies for managing errors, including `throwOnHttpError` |
310
- | **[Advanced Features](./docs/advanced.md)** | Per-request overrides, pending requests, circuit breakers, custom errors |
311
- | **[Production Operations](./docs/production-operations.md)** | Pre-deploy checklist, alerting baseline, and incident playbook |
312
- | **[Hooks & Transformation](./docs/hooks.md)** | Lifecycle hooks, authentication, logging, request/response transformation |
313
- | **[Usage Examples](./docs/examples.md)** | Real-world patterns: REST clients, GraphQL, file uploads, microservices |
314
- | **[Compatibility](./docs/compatibility.md)** | Browser/Node.js support, polyfills, framework integration |
315
-
316
- ## Environment Requirements
317
-
318
- `ffetch` works best with native `AbortSignal.any` support:
319
-
320
- - **Node.js 20.6+** (native `AbortSignal.any`)
321
- - **Modern browsers with `AbortSignal.any`** (for example: Chrome 117+, Firefox 117+, Safari 17+, Edge 117+)
322
-
323
- If your environment does not support `AbortSignal.any` (Node.js < 20.6, older browsers), you can still use ffetch by installing an `AbortSignal.any` polyfill. `AbortSignal.timeout` is optional because ffetch includes an internal timeout fallback. See the [compatibility guide](./docs/compatibility.md) for instructions.
324
-
325
- **Custom fetch support:**
326
- You can pass any fetch-compatible implementation (native fetch, node-fetch, undici, SvelteKit, Next.js, Nuxt, or a polyfill) via the `fetchHandler` option. This makes ffetch fully compatible with SSR, edge, metaframework environments, custom backends, and test runners.
327
-
328
- #### "AbortSignal.any is not a function"
329
-
330
- Solution: Install a polyfill for `AbortSignal.any`
331
-
332
- ```bash
333
- npm install abort-controller-x
334
- ```
335
-
336
- ## CDN Usage
337
-
338
- ```html
339
- <script type="module">
340
- import { createClient } from 'https://unpkg.com/@fetchkit/ffetch/dist/index.min.js'
341
-
342
- const api = createClient({ timeout: 5000 })
343
- const data = await api('/api/data').then((r) => r.json())
344
- </script>
345
- ```
346
-
347
- ## Deduplication Limitations
348
-
349
- - Deduplication is **off** by default. Enable it via `plugins: [dedupePlugin()]`.
350
- - The default hash function is `dedupeRequestHash`, which handles common body types and skips deduplication for streams and FormData.
351
- - Optional stale-entry cleanup: `dedupePlugin({ ttl, sweepInterval })` enables map-entry eviction. TTL eviction only removes dedupe keys; it does not reject already in-flight promises.
352
- - **Stream bodies** (`ReadableStream`, `FormData`): Deduplication is skipped for requests with these body types, as they cannot be reliably hashed or replayed.
353
- - **Non-idempotent requests**: Use deduplication with caution for non-idempotent methods (e.g., POST), as it may suppress multiple intended requests.
354
- - **Custom hash function**: Ensure your hash function uniquely identifies requests to avoid accidental deduplication.
355
-
356
- See [deduplication.md](./docs/deduplication.md) for full details.
357
-
358
- ## Fetch vs. Axios vs. ky vs. `ffetch`
359
-
360
- | Feature | Native Fetch | Axios | ky | ffetch |
361
- | -------------------- | ------------------------------------------------------- | ------------------------------ | --------------------------------------------- | -------------------------------------------------------------------------------------- |
362
- | Timeouts | ❌ Manual AbortController | ✅ Built-in | ✅ Built-in | ✅ Built-in with fallbacks |
363
- | Retries | ❌ Manual implementation | ❌ Manual or plugins | ✅ Built-in | ✅ Smart exponential backoff |
364
- | Response Parsing DX | ⚠️ Response methods only (`await fetch(...).then(...)`) | ✅ `response.data` convenience | ✅ `.json()/.text()/.blob()` on request chain | ✅ Optional `responseShortcutsPlugin()` (`.json()/.text()/.blob()` on request chain) |
365
- | Plugin Architecture | ❌ Not available | ⚠️ Interceptors only | ⚠️ Hook-based extensions | ✅ First-class plugin pipeline (optional built-in + custom plugins) |
366
- | Circuit Breaker | ❌ Not available | ❌ Manual or plugins | ❌ Manual | ✅ Automatic failure protection |
367
- | Deduplication | ❌ Not available | ❌ Not available | ❌ Not available | ✅ Optional via `dedupePlugin()` |
368
- | Bulkheading | ❌ Not available | ❌ Not available | ❌ Not available | ✅ Optional via `bulkheadPlugin()` |
369
- | Request Hedging | ❌ Not available | ❌ Not available | ❌ Not available | ✅ Optional via `hedgePlugin()` (tail latency reduction) |
370
- | Request Monitoring | ❌ Manual tracking | ❌ Manual tracking | ❌ Manual tracking | ✅ Built-in pending requests |
371
- | Error Types | ❌ Generic errors | ⚠️ HTTP errors only | ✅ Specific error classes | ✅ Specific error classes |
372
- | TypeScript | ⚠️ Basic types | ⚠️ Basic types | ✅ Strong types | ✅ Full type safety |
373
- | Hooks/Middleware | ❌ Not available | ✅ Interceptors | ✅ Hooks | ✅ Comprehensive lifecycle hooks |
374
- | Bundle Size | ✅ Native (0kb) | ❌ ~13kb minified | ✅ Lightweight (fetch-based) | ✅ ~3kb minified |
375
- | Modern APIs | ✅ Web standards | ❌ XMLHttpRequest | ✅ Fetch + modern APIs | ✅ Fetch + modern features |
376
- | Download Progress | ❌ Manual ReadableStream | ❌ Manual | ✅ `onDownloadProgress` callback | ✅ Optional via `downloadProgressPlugin()` |
377
- | Custom Fetch Support | ❌ No (global only) | ❌ No | ❌ No | ✅ Yes (wrap any fetch-compatible implementation, including framework or custom fetch) |
378
-
379
- Note: built-in plugins in ffetch are opt-in. Use `bulkheadPlugin()` for concurrency isolation and backpressure, `dedupePlugin()` for deduplication, `circuitPlugin()` for circuit breaking, `hedgePlugin()` for tail-latency racing, `requestShortcutsPlugin()` for client HTTP method shortcuts, `responseShortcutsPlugin()` for request-promise parsing shortcuts, and `downloadProgressPlugin()` for streaming download progress. Bundle size: ~3kb core, additional optional plugin imports are tree-shakeable.
380
-
381
- ## Try ffetch in Action
382
-
383
- Want to see these clients in practice? Check out [ffetch-demo](https://github.com/fetch-kit/ffetch-demo) for working examples and side-by-side comparisons of how ffetch simplifies common fetch patterns.
384
-
385
- 📰 Featured in [Node Weekly #594](https://nodeweekly.com/issues/594)
386
-
387
- ## Join the Community
388
-
389
- Got questions, want to discuss features, or share examples? Join the **Fetch-Kit Discord server**:
390
-
391
- [![Discord](https://img.shields.io/badge/Discord-Join_Fetch--Kit-7289DA?logo=discord&logoColor=white)](https://discord.gg/sdyPBPCDUg)
392
-
393
- ## Security
394
-
395
- `ffetch` is scored at **7.4/10** by the [OpenSSF Scorecard](https://securityscorecards.dev/), an automated security health check for open source projects. This score reflects the commitment to security best practices and continuous improvement. Key security features include:
396
-
397
- - ✅ Pinned GitHub Actions dependencies
398
- - ✅ CodeQL static analysis on every PR and push to main
399
- - ✅ Dependabot for dependency updates and security alerts
400
- - ✅ npm publish with OIDC provenance attestations
401
- - ✅ Security policy and private vulnerability reporting
402
- - ✅ Branch protection on `main`
403
- - ✅ SPDX SBOM attached to every release
404
-
405
- The score is capped below 10 due to being a solo-maintained project (no mandatory code review or multiple org contributors). [View the full breakdown](https://scorecard.dev/viewer/?uri=github.com/fetch-kit/ffetch).
406
-
407
- To report a security vulnerability, see [SECURITY.md](./SECURITY.md).
408
-
409
- ## Contributing
410
-
411
- - **Issues**: [GitHub Issues](https://github.com/fetch-kit/ffetch/issues)
412
- - **Pull Requests**: [GitHub PRs](https://github.com/fetch-kit/ffetch/pulls)
413
- - **Documentation**: Found in `./docs/` - PRs welcome!
414
-
415
- ## License
416
-
417
- MIT © 2025- gkoos
17
+
18
+ **A production-ready TypeScript-first drop-in replacement for native fetch, or any fetch-compatible implementation.**
19
+
20
+ ffetch can wrap any fetch-compatible implementation (native fetch, node-fetch, undici, or framework-provided fetch), making it flexible for SSR, edge, and custom environments.
21
+
22
+ ffetch uses a plugin architecture for optional features, so you only include what you need.
23
+
24
+ ## Why ffetch
25
+
26
+ - Keep native fetch ergonomics, add production safety (timeouts, retries, error strategy).
27
+ - Keep your runtime flexibility (use global fetch or any fetch-compatible handler).
28
+ - Keep your bundle lean – **~3kb minified** (optional plugins, zero runtime dependencies).
29
+
30
+ ## Table of Contents
31
+
32
+ - [@fetchkit/ffetch](#fetchkitffetch)
33
+ - [Why ffetch](#why-ffetch)
34
+ - [Table of Contents](#table-of-contents)
35
+ - [Key Features](#key-features)
36
+ - [Built-in Plugins at a Glance](#built-in-plugins-at-a-glance)
37
+ - [What Problems Does ffetch Solve?](#what-problems-does-ffetch-solve)
38
+ - [Quick Start](#quick-start)
39
+ - [Install](#install)
40
+ - [Basic Setup](#basic-setup)
41
+ - [Production Setup with Plugins](#production-setup-with-plugins)
42
+ - [Why not only native fetch?](#why-not-only-native-fetch)
43
+ - [Common Recipes](#common-recipes)
44
+ - [Using a Custom fetchHandler (SSR, metaframeworks, or polyfills)](#using-a-custom-fetchhandler-ssr-metaframeworks-or-polyfills)
45
+ - [Advanced Example](#advanced-example)
46
+ - [Custom Error Handling with `throwOnHttpError`](#custom-error-handling-with-throwonhttperror)
47
+ - [Documentation](#documentation)
48
+ - [Environment Requirements](#environment-requirements)
49
+ - ["AbortSignal.any is not a function"](#abortsignalany-is-not-a-function)
50
+ - [CDN Usage](#cdn-usage)
51
+ - [Deduplication Limitations](#deduplication-limitations)
52
+ - [Fetch vs. Axios vs. ky vs. `ffetch`](#fetch-vs-axios-vs-ky-vs-ffetch)
53
+ - [Try ffetch in Action](#try-ffetch-in-action)
54
+ - [Join the Community](#join-the-community)
55
+ - [Security](#security)
56
+ - [Contributing](#contributing)
57
+ - [License](#license)
58
+
59
+ ## Key Features
60
+
61
+ - **Timeouts** – per-request or global
62
+ - **Retries** – exponential backoff + jitter
63
+ - **Abort-aware retries** – aborting during backoff cancels immediately
64
+ - **Plugin architecture** – extensible lifecycle-based plugins for optional behavior
65
+ - **Hooks** – logging, auth, metrics, request/response transformation
66
+ - **Pending requests** – real-time monitoring of active requests
67
+ - **Per-request overrides** – customize behavior on a per-request basis
68
+ - **Universal** – Node.js, Browser, Cloudflare Workers, React Native
69
+ - **Zero runtime deps** – ships as dual ESM/CJS
70
+ - **Configurable error handling** – custom error types and `throwOnHttpError` flag to throw on HTTP errors
71
+ - **Bulkhead plugin (optional, prebuilt)** – cap concurrency and queue depth per client instance
72
+ - **Circuit breaker plugin (optional, prebuilt)** – automatic failure protection
73
+ - **Hedge plugin (optional, prebuilt)** – race parallel attempts to reduce tail latency
74
+ - **Context ID plugin (optional, prebuilt)** – inject a stable context ID header across retries/hedges for correlation
75
+ - **Deduplication plugin (optional, prebuilt)** – automatic deduping of in-flight identical requests
76
+ - **Request shortcuts plugin (optional, prebuilt)** – call `client.get(url)` / `.post()` / `.put()` / `.patch()` / `.delete()` directly on the client
77
+ - **Response shortcuts plugin (optional, prebuilt)** – call `client(url).json()` / `.text()` / `.blob()` directly on the request promise
78
+ - **Download progress plugin (optional, prebuilt)** – stream download progress callbacks with bytes transferred and percentage
79
+
80
+ **Built-in error classes:** `TimeoutError`, `RetryLimitError`, `CircuitOpenError`, `BulkheadFullError`, `HttpError`, `NetworkError`, `AbortError`
81
+
82
+ ### Built-in Plugins at a Glance
83
+
84
+ All plugins are tree-shakeable — import only what you use.
85
+
86
+ - **dedupePlugin (optional)**: dedupe in-flight identical requests.
87
+ - **bulkheadPlugin (optional)**: cap in-flight concurrency with optional queue backpressure.
88
+ - **hedgePlugin (optional)**: race multiple attempts and cancel losers when a winner is found.
89
+ - **circuitPlugin (optional)**: fail fast after repeated failures.
90
+ - **contextIdPlugin (optional)**: inject a stable request context ID (for example in `x-context-id`) across retries and hedges.
91
+ - **requestShortcutsPlugin (optional)**: HTTP method shortcuts on the client (`.get()` / `.post()` / `.put()` / `.patch()` / `.delete()` / `.head()` / `.options()`).
92
+ - **responseShortcutsPlugin (optional)**: use `client(url).json()` / `.text()` / `.blob()` style parsing.
93
+ - **downloadProgressPlugin (optional)**: stream download progress via `onProgress(progress, chunk)` callback.
94
+
95
+ ## What Problems Does ffetch Solve?
96
+
97
+ ffetch is ideal for:
98
+
99
+ - **Microservices and REST APIs** with retry requirements and timeout control
100
+ - **High-traffic client applications** that need in-flight deduplication and circuit breaker protection
101
+ - **SSR and metaframework apps** that require runtime flexibility (custom fetch handlers for different environments)
102
+ - **Type-safe request handling** with strong TypeScript support and zero runtime dependencies
103
+
104
+ ## Quick Start
105
+
106
+ Migrating from v4? Start with the [migration guide](./docs/migration.md) before applying the examples below.
107
+
108
+ ### Install
109
+
110
+ ```bash
111
+ # npm
112
+ npm install @fetchkit/ffetch
113
+
114
+ # yarn
115
+ yarn add @fetchkit/ffetch
116
+
117
+ # pnpm
118
+ pnpm add @fetchkit/ffetch
119
+
120
+ # bun
121
+ bun add @fetchkit/ffetch
122
+ ```
123
+
124
+ ### Basic Setup
125
+
126
+ ```typescript
127
+ import { createClient } from '@fetchkit/ffetch'
128
+
129
+ type User = { id: number; name: string }
130
+
131
+ const api = createClient({ timeout: 5000, retries: 2 })
132
+ const response = await api('https://api.example.com/users')
133
+
134
+ if (!response.ok) {
135
+ throw new Error(`Request failed: ${response.status}`)
136
+ }
137
+
138
+ const users = (await response.json()) as User[]
139
+ ```
140
+
141
+ ### Production Setup with Plugins
142
+
143
+ ```typescript
144
+ import { createClient } from '@fetchkit/ffetch'
145
+ import { dedupePlugin } from '@fetchkit/ffetch/plugins/dedupe'
146
+ import { circuitPlugin } from '@fetchkit/ffetch/plugins/circuit'
147
+ import { contextIdPlugin } from '@fetchkit/ffetch/plugins/context-id'
148
+ import { requestShortcutsPlugin } from '@fetchkit/ffetch/plugins/request-shortcuts'
149
+ import { responseShortcutsPlugin } from '@fetchkit/ffetch/plugins/response-shortcuts'
150
+
151
+ const api = createClient({
152
+ timeout: 10_000,
153
+ retries: 2,
154
+ plugins: [
155
+ // 1) Optional: dedupe identical in-flight requests
156
+ dedupePlugin({ ttl: 30_000, sweepInterval: 5_000 }),
157
+ // 2) Optional: open the circuit after repeated failures
158
+ circuitPlugin({ threshold: 5, reset: 30_000 }),
159
+ // 3) Optional: inject stable correlation context IDs
160
+ contextIdPlugin(),
161
+ // 4) Optional: enable request-promise parsing shortcuts
162
+ responseShortcutsPlugin(),
163
+ // 5) Optional: enable client HTTP method shortcuts
164
+ requestShortcutsPlugin(),
165
+ ],
166
+ })
167
+
168
+ const users = await api
169
+ .get('https://api.example.com/users')
170
+ .json<Array<{ id: number; name: string }>>()
171
+
172
+ const p1 = api('https://api.example.com/data')
173
+ const p2 = api('https://api.example.com/data')
174
+ const [res1, res2] = await Promise.all([p1, p2])
175
+ ```
176
+
177
+ What this setup gives you:
178
+
179
+ - **Operational safety**: retries with timeout defaults.
180
+ - **Lower duplicate traffic (optional)**: concurrent identical requests share one in-flight call.
181
+ - **Faster failure recovery (optional)**: circuit breaker blocks repeated failing calls.
182
+ - **Better observability correlation (optional)**: stable request context IDs across retries and hedges.
183
+ - **Cleaner request ergonomics (optional)**: `client.get(url)` / `.post(url, init)` style shortcuts.
184
+ - **Cleaner parsing (optional)**: `client(url).json()` style shortcuts.
185
+
186
+ ### Why not only native fetch?
187
+
188
+ - Native fetch is a great baseline, but production apps usually need retries and timeout control.
189
+ - ffetch keeps the fetch model and adds optional resilience features.
190
+ - You can keep strict native behavior and only opt into plugins you need.
191
+
192
+ ### Common Recipes
193
+
194
+ ```typescript
195
+ // Throw on non-2xx/429 once retries are exhausted
196
+ const strict = createClient({ throwOnHttpError: true })
197
+
198
+ // Use a custom fetch implementation (SSR/framework/runtime)
199
+ import nodeFetch from 'node-fetch'
200
+ const apiWithCustomHandler = createClient({ fetchHandler: nodeFetch })
201
+
202
+ // Keep native Response flow (works with or without plugins)
203
+ const plainApi = createClient({ timeout: 5000 })
204
+ const response = await plainApi('https://api.example.com/health')
205
+ const text = await response.text()
206
+ ```
207
+
208
+ ### Using a Custom fetchHandler (SSR, metaframeworks, or polyfills)
209
+
210
+ ```typescript
211
+ // Why this exists:
212
+ // ffetch wraps whatever fetch-compatible function you provide.
213
+ // This is useful when your runtime has a scoped/framework fetch,
214
+ // or when Node needs an explicit fetch implementation.
215
+
216
+ import { createClient } from '@fetchkit/ffetch'
217
+ import nodeFetch from 'node-fetch'
218
+
219
+ // Node.js example: provide node-fetch explicitly
220
+ const apiNode = createClient({
221
+ fetchHandler: nodeFetch,
222
+ timeout: 5000,
223
+ })
224
+ const nodeResponse = await apiNode('https://api.example.com/data')
225
+
226
+ // Framework example: pass the framework-scoped fetch
227
+ // (e.g. the fetch passed into a request handler)
228
+ async function loadData(frameworkFetch: typeof fetch) {
229
+ const api = createClient({
230
+ fetchHandler: frameworkFetch,
231
+ timeout: 5000,
232
+ })
233
+
234
+ const response = await api('/internal/data')
235
+ return response.json()
236
+ }
237
+ ```
238
+
239
+ All ffetch features (timeouts, retries, plugins, hooks) behave the same with a custom `fetchHandler`.
240
+
241
+ With `responseShortcutsPlugin()` enabled, request-promise shortcuts like `api(url).json()` also work the same.
242
+
243
+ ### Advanced Example
244
+
245
+ ```typescript
246
+ // Production-ready client with error handling and monitoring
247
+ import { createClient } from '@fetchkit/ffetch'
248
+ import { dedupePlugin } from '@fetchkit/ffetch/plugins/dedupe'
249
+ import { circuitPlugin } from '@fetchkit/ffetch/plugins/circuit'
250
+
251
+ const client = createClient({
252
+ timeout: 10000,
253
+ retries: 2,
254
+ fetchHandler: fetch, // Use custom fetch if needed
255
+ plugins: [
256
+ dedupePlugin({
257
+ hashFn: (params) => `${params.method}|${params.url}|${params.body}`,
258
+ ttl: 30_000,
259
+ sweepInterval: 5_000,
260
+ }),
261
+ circuitPlugin({
262
+ threshold: 5,
263
+ reset: 30_000,
264
+ onCircuitOpen: ({ request, reason }) =>
265
+ console.warn('Circuit opened due to:', request.url, reason.type),
266
+ onCircuitClose: ({ request, response }) =>
267
+ console.info('Circuit closed after:', request.url, response.status),
268
+ }),
269
+ ],
270
+ hooks: {
271
+ before: async (req) => console.log('→', req.url),
272
+ after: async (req, res) => console.log('←', res.status),
273
+ onError: async (req, err) => console.error('Error:', err.message),
274
+ },
275
+ })
276
+
277
+ try {
278
+ const response = await client('/api/data')
279
+
280
+ // Check HTTP status manually (like native fetch)
281
+ if (!response.ok) {
282
+ console.log('HTTP error:', response.status)
283
+ return
284
+ }
285
+
286
+ const data = await response.json()
287
+ console.log('Active requests:', client.pendingRequests.length)
288
+ } catch (err) {
289
+ if (err instanceof TimeoutError) {
290
+ console.log('Request timed out')
291
+ } else if (err instanceof RetryLimitError) {
292
+ console.log('Request failed after retries')
293
+ }
294
+ }
295
+ ```
296
+
297
+ ### Custom Error Handling with `throwOnHttpError`
298
+
299
+ Native `fetch`'s controversial behavior of not throwing errors for HTTP error status codes (4xx, 5xx) can lead to overlooked errors in applications. By default, `ffetch` follows this same pattern, returning a `Response` object regardless of the HTTP status code. However, with the `throwOnHttpError` flag, developers can configure `ffetch` to throw an `HttpError` for HTTP error responses, making error handling more explicit and robust. Note that this behavior is affected by retries and the circuit breaker - full details are explained in the [Error Handling documentation](./docs/errorhandling.md).
300
+
301
+ ## Documentation
302
+
303
+ | Topic | Description |
304
+ | ------------------------------------------------------------ | ------------------------------------------------------------------------- |
305
+ | **[Complete Documentation](./docs/index.md)** | **Start here** - Documentation index and overview |
306
+ | **[API Reference](./docs/api.md)** | Complete API documentation and configuration options |
307
+ | **[Plugin Architecture](./docs/plugins.md)** | Plugin lifecycle, custom plugin authoring, and integration patterns |
308
+ | **[Deduplication](./docs/deduplication.md)** | How deduplication works, hash config, optional TTL cleanup, limitations |
309
+ | **[Error Handling](./docs/errorhandling.md)** | Strategies for managing errors, including `throwOnHttpError` |
310
+ | **[Advanced Features](./docs/advanced.md)** | Per-request overrides, pending requests, circuit breakers, custom errors |
311
+ | **[Production Operations](./docs/production-operations.md)** | Pre-deploy checklist, alerting baseline, and incident playbook |
312
+ | **[Hooks & Transformation](./docs/hooks.md)** | Lifecycle hooks, authentication, logging, request/response transformation |
313
+ | **[Usage Examples](./docs/examples.md)** | Real-world patterns: REST clients, GraphQL, file uploads, microservices |
314
+ | **[Compatibility](./docs/compatibility.md)** | Browser/Node.js support, polyfills, framework integration |
315
+
316
+ ## Environment Requirements
317
+
318
+ `ffetch` works best with native `AbortSignal.any` support:
319
+
320
+ - **Node.js 20.6+** (native `AbortSignal.any`)
321
+ - **Modern browsers with `AbortSignal.any`** (for example: Chrome 117+, Firefox 117+, Safari 17+, Edge 117+)
322
+
323
+ If your environment does not support `AbortSignal.any` (Node.js < 20.6, older browsers), you can still use ffetch by installing an `AbortSignal.any` polyfill. `AbortSignal.timeout` is optional because ffetch includes an internal timeout fallback. See the [compatibility guide](./docs/compatibility.md) for instructions.
324
+
325
+ **Custom fetch support:**
326
+ You can pass any fetch-compatible implementation (native fetch, node-fetch, undici, SvelteKit, Next.js, Nuxt, or a polyfill) via the `fetchHandler` option. This makes ffetch fully compatible with SSR, edge, metaframework environments, custom backends, and test runners.
327
+
328
+ #### "AbortSignal.any is not a function"
329
+
330
+ Solution: Install a polyfill for `AbortSignal.any`
331
+
332
+ ```bash
333
+ npm install abort-controller-x
334
+ ```
335
+
336
+ ## CDN Usage
337
+
338
+ ```html
339
+ <script type="module">
340
+ import { createClient } from 'https://unpkg.com/@fetchkit/ffetch/dist/index.min.js'
341
+
342
+ const api = createClient({ timeout: 5000 })
343
+ const data = await api('/api/data').then((r) => r.json())
344
+ </script>
345
+ ```
346
+
347
+ ## Deduplication Limitations
348
+
349
+ - Deduplication is **off** by default. Enable it via `plugins: [dedupePlugin()]`.
350
+ - The default hash function is `dedupeRequestHash`, which handles common body types and skips deduplication for streams and FormData.
351
+ - Optional stale-entry cleanup: `dedupePlugin({ ttl, sweepInterval })` enables map-entry eviction. TTL eviction only removes dedupe keys; it does not reject already in-flight promises.
352
+ - **Stream bodies** (`ReadableStream`, `FormData`): Deduplication is skipped for requests with these body types, as they cannot be reliably hashed or replayed.
353
+ - **Non-idempotent requests**: Use deduplication with caution for non-idempotent methods (e.g., POST), as it may suppress multiple intended requests.
354
+ - **Custom hash function**: Ensure your hash function uniquely identifies requests to avoid accidental deduplication.
355
+
356
+ See [deduplication.md](./docs/deduplication.md) for full details.
357
+
358
+ ## Fetch vs. Axios vs. ky vs. `ffetch`
359
+
360
+ | Feature | Native Fetch | Axios | ky | ffetch |
361
+ | -------------------- | ------------------------------------------------------- | ------------------------------ | --------------------------------------------- | -------------------------------------------------------------------------------------- |
362
+ | Timeouts | ❌ Manual AbortController | ✅ Built-in | ✅ Built-in | ✅ Built-in with fallbacks |
363
+ | Retries | ❌ Manual implementation | ❌ Manual or plugins | ✅ Built-in | ✅ Smart exponential backoff |
364
+ | Response Parsing DX | ⚠️ Response methods only (`await fetch(...).then(...)`) | ✅ `response.data` convenience | ✅ `.json()/.text()/.blob()` on request chain | ✅ Optional `responseShortcutsPlugin()` (`.json()/.text()/.blob()` on request chain) |
365
+ | Plugin Architecture | ❌ Not available | ⚠️ Interceptors only | ⚠️ Hook-based extensions | ✅ First-class plugin pipeline (optional built-in + custom plugins) |
366
+ | Circuit Breaker | ❌ Not available | ❌ Manual or plugins | ❌ Manual | ✅ Automatic failure protection |
367
+ | Deduplication | ❌ Not available | ❌ Not available | ❌ Not available | ✅ Optional via `dedupePlugin()` |
368
+ | Bulkheading | ❌ Not available | ❌ Not available | ❌ Not available | ✅ Optional via `bulkheadPlugin()` |
369
+ | Request Hedging | ❌ Not available | ❌ Not available | ❌ Not available | ✅ Optional via `hedgePlugin()` (tail latency reduction) |
370
+ | Request Monitoring | ❌ Manual tracking | ❌ Manual tracking | ❌ Manual tracking | ✅ Built-in pending requests |
371
+ | Error Types | ❌ Generic errors | ⚠️ HTTP errors only | ✅ Specific error classes | ✅ Specific error classes |
372
+ | TypeScript | ⚠️ Basic types | ⚠️ Basic types | ✅ Strong types | ✅ Full type safety |
373
+ | Hooks/Middleware | ❌ Not available | ✅ Interceptors | ✅ Hooks | ✅ Comprehensive lifecycle hooks |
374
+ | Bundle Size | ✅ Native (0kb) | ❌ ~13kb minified | ✅ Lightweight (fetch-based) | ✅ ~3kb minified |
375
+ | Modern APIs | ✅ Web standards | ❌ XMLHttpRequest | ✅ Fetch + modern APIs | ✅ Fetch + modern features |
376
+ | Download Progress | ❌ Manual ReadableStream | ❌ Manual | ✅ `onDownloadProgress` callback | ✅ Optional via `downloadProgressPlugin()` |
377
+ | Custom Fetch Support | ❌ No (global only) | ❌ No | ❌ No | ✅ Yes (wrap any fetch-compatible implementation, including framework or custom fetch) |
378
+
379
+ Note: built-in plugins in ffetch are opt-in. Use `bulkheadPlugin()` for concurrency isolation and backpressure, `dedupePlugin()` for deduplication, `circuitPlugin()` for circuit breaking, `hedgePlugin()` for tail-latency racing, `requestShortcutsPlugin()` for client HTTP method shortcuts, `responseShortcutsPlugin()` for request-promise parsing shortcuts, and `downloadProgressPlugin()` for streaming download progress. Bundle size: ~3kb core, additional optional plugin imports are tree-shakeable.
380
+
381
+ ## Try ffetch in Action
382
+
383
+ Want to see these clients in practice? Check out [ffetch-demo](https://github.com/fetch-kit/ffetch-demo) for working examples and side-by-side comparisons of how ffetch simplifies common fetch patterns.
384
+
385
+ 📰 Featured in [Node Weekly #594](https://nodeweekly.com/issues/594)
386
+
387
+ ## Join the Community
388
+
389
+ Got questions, want to discuss features, or share examples? Join the **Fetch-Kit Discord server**:
390
+
391
+ [![Discord](https://img.shields.io/badge/Discord-Join_Fetch--Kit-7289DA?logo=discord&logoColor=white)](https://discord.gg/sdyPBPCDUg)
392
+
393
+ ## Security
394
+
395
+ `ffetch` is continuously evaluated by the [OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/fetch-kit/ffetch), an automated security health check for open source projects. The badge at the top of this README shows the latest score. Security measures include:
396
+
397
+ - ✅ Pinned GitHub Actions dependencies
398
+ - ✅ CodeQL static analysis on every PR and push to main
399
+ - ✅ Dependabot for dependency updates and security alerts
400
+ - ✅ npm publish with OIDC provenance attestations
401
+ - ✅ Security policy and private vulnerability reporting
402
+ - ✅ Branch protection on `main`
403
+ - ✅ SPDX SBOM attached to every release
404
+
405
+ Some checks, such as mandatory reviews and multiple organizational contributors, are intentionally not pursued for this solo-maintained project. [View the full breakdown](https://scorecard.dev/viewer/?uri=github.com/fetch-kit/ffetch).
406
+
407
+ To report a security vulnerability, see [SECURITY.md](./SECURITY.md).
408
+
409
+ ## Contributing
410
+
411
+ - **Issues**: [GitHub Issues](https://github.com/fetch-kit/ffetch/issues)
412
+ - **Contribution guide**: See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup and pull request requirements.
413
+ - **Code of Conduct**: See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
414
+ - **Documentation**: Found in `./docs/` - PRs welcome!
415
+
416
+ ## License
417
+
418
+ MIT © 2025- gkoos
package/dist/index.cjs CHANGED
@@ -3,8 +3,13 @@ var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __esm = (fn, res) => function __init() {
7
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ var __esm = (fn, res, err) => function __init() {
7
+ if (err) throw err[0];
8
+ try {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ } catch (e) {
11
+ throw err = [e], e;
12
+ }
8
13
  };
9
14
  var __export = (target, all) => {
10
15
  for (var name in all)
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/error.ts","../src/index.ts","../src/retry.ts","../src/should-retry.ts","../src/client.ts"],"sourcesContent":["// Base error class to reduce duplication\nclass BaseError extends Error {\n public cause?: unknown\n constructor(name: string, message: string, cause?: unknown) {\n super(message)\n this.name = name\n if (cause !== undefined) this.cause = cause\n }\n}\n\nexport class TimeoutError extends BaseError {\n constructor(message = 'Request timed out', cause?: unknown) {\n super('TimeoutError', message, cause)\n }\n}\n\nexport class CircuitOpenError extends BaseError {\n constructor(message = 'Circuit is open', cause?: unknown) {\n super('CircuitOpenError', message, cause)\n }\n}\n\nexport class BulkheadFullError extends BaseError {\n constructor(message = 'Bulkhead queue is full', cause?: unknown) {\n super('BulkheadFullError', message, cause)\n }\n}\n\nexport class AbortError extends BaseError {\n constructor(message = 'Request was aborted', cause?: unknown) {\n super('AbortError', message, cause)\n }\n}\n\nexport class RetryLimitError extends BaseError {\n constructor(message = 'Retry limit reached', cause?: unknown) {\n super('RetryLimitError', message, cause)\n }\n}\n\nexport class NetworkError extends BaseError {\n constructor(message = 'Network error occurred', cause?: unknown) {\n super('NetworkError', message, cause)\n }\n}\n\nexport class HttpError extends BaseError {\n constructor(message = 'HTTP error occurred', cause?: unknown) {\n super('HttpError', message, cause)\n }\n}\n","export type { FFetch, FFetchOptions } from './types'\nexport type { Hooks } from './hooks'\nexport type {\n ClientPlugin,\n PluginRequestContext,\n PluginDispatch,\n PluginSetupContext,\n} from './plugins'\n\nexport { createClient } from './client'\n\nexport {\n TimeoutError,\n CircuitOpenError,\n BulkheadFullError,\n AbortError,\n RetryLimitError,\n NetworkError,\n HttpError,\n} from './error'\n","import type { RetryContext } from './types.js'\n\nexport type RetryDelay = number | ((ctx: RetryContext) => number)\n\nexport const defaultDelay: RetryDelay = (ctx) => {\n const retryAfter = ctx.response?.headers.get('Retry-After')\n if (retryAfter) {\n const seconds = parseInt(retryAfter, 10)\n if (!isNaN(seconds)) return seconds * 1000\n const date = Date.parse(retryAfter)\n if (!isNaN(date)) return Math.max(0, date - Date.now())\n }\n return 2 ** ctx.attempt * 200 + Math.random() * 100\n}\n\nfunction waitForRetryDelay(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0) return Promise.resolve()\n return new Promise((resolve) => {\n if (!signal) {\n setTimeout(resolve, ms)\n return\n }\n\n if (signal.aborted) {\n resolve()\n return\n }\n\n const onAbort = () => {\n clearTimeout(timer)\n resolve()\n }\n\n const timer = setTimeout(() => {\n signal.removeEventListener('abort', onAbort)\n resolve()\n }, ms)\n\n signal.addEventListener('abort', onAbort, { once: true })\n })\n}\n\nexport async function retry(\n fn: () => Promise<Response>,\n retries: number,\n delay: RetryDelay,\n shouldRetry: (ctx: RetryContext) => boolean | Promise<boolean> = () => true,\n request: Request,\n signal?: AbortSignal\n): Promise<Response> {\n let lastErr: unknown\n let lastRes: Response | undefined\n\n for (let i = 0; i <= retries; i++) {\n const ctx: RetryContext = {\n attempt: i + 1,\n request,\n response: lastRes,\n error: lastErr,\n }\n try {\n lastRes = await fn()\n } catch (err) {\n lastErr = err\n ctx.error = err\n if (i === retries || !(await shouldRetry(ctx))) throw err\n const wait = typeof delay === 'function' ? delay(ctx) : delay\n await waitForRetryDelay(wait, signal)\n continue\n }\n\n ctx.response = lastRes\n ctx.error = undefined\n if (i < retries && (await shouldRetry(ctx))) {\n const wait = typeof delay === 'function' ? delay(ctx) : delay\n await waitForRetryDelay(wait, signal)\n continue\n }\n return lastRes\n }\n throw lastErr\n}\n","import { AbortError, CircuitOpenError, TimeoutError } from './error.js'\nimport type { RetryContext } from './types.js'\n\nexport function shouldRetry(ctx: RetryContext): boolean {\n const { error, response } = ctx\n if (\n error instanceof AbortError ||\n error instanceof CircuitOpenError ||\n error instanceof TimeoutError\n )\n return false\n if (!response) return true // network error\n return response.status >= 500 || response.status === 429\n}\n","import type {\n FFetchOptions,\n FFetch,\n FFetchRequestInit,\n PendingRequest,\n} from './types.js'\nimport { retry, defaultDelay } from './retry.js'\nimport { shouldRetry as defaultShouldRetry } from './should-retry.js'\nimport {\n type PluginDispatch,\n type PluginRequestContext,\n type PluginExtensions,\n type PluginRequestPromiseExtensions,\n type ClientPlugin,\n type PluginExtensionBase,\n type PluginRequestPromiseExtensionBase,\n} from './plugins.js'\nimport {\n TimeoutError,\n AbortError,\n RetryLimitError,\n NetworkError,\n} from './error.js'\n\nexport function createClient<\n TPlugins extends readonly ClientPlugin<\n PluginExtensionBase,\n PluginRequestPromiseExtensionBase\n >[] = readonly ClientPlugin<\n PluginExtensionBase,\n PluginRequestPromiseExtensionBase\n >[],\n>(\n opts: FFetchOptions<TPlugins> = {} as FFetchOptions<TPlugins>\n): FFetch<\n PluginExtensions<TPlugins>,\n PluginRequestPromiseExtensions<TPlugins>\n> {\n const {\n timeout: clientDefaultTimeout = 5_000,\n retries: clientDefaultRetries = 0,\n retryDelay: clientDefaultRetryDelay = defaultDelay,\n shouldRetry: clientDefaultShouldRetry = defaultShouldRetry,\n hooks: clientDefaultHooks = {},\n fetchHandler,\n plugins: inputPlugins = [] as unknown as TPlugins,\n } = opts\n\n const extensionDescriptors: PropertyDescriptorMap = Object.create(null)\n\n const plugins = inputPlugins\n .map((plugin, index) => ({ plugin, index }))\n .sort((a, b) => {\n const aOrder = a.plugin.order ?? 0\n const bOrder = b.plugin.order ?? 0\n if (aOrder !== bOrder) return aOrder - bOrder\n return a.index - b.index\n })\n .map((entry) => entry.plugin)\n\n for (const plugin of plugins) {\n plugin.setup?.({\n defineExtension: (key, descriptor) => {\n const propertyKey = key as PropertyKey\n if (propertyKey in extensionDescriptors) {\n throw new Error(\n `Plugin extension collision for property \"${String(propertyKey)}\"`\n )\n }\n if ('get' in descriptor) {\n extensionDescriptors[propertyKey] = {\n get: descriptor.get,\n enumerable: descriptor.enumerable ?? true,\n configurable: false,\n }\n return\n }\n extensionDescriptors[propertyKey] = {\n value: descriptor.value,\n writable: false,\n enumerable: descriptor.enumerable ?? true,\n configurable: false,\n }\n },\n })\n }\n\n const pendingRequests: PendingRequest[] = []\n\n // Helper to abort all pending requests\n function abortAll() {\n for (const entry of pendingRequests) {\n entry.controller?.abort()\n }\n }\n\n const client = (input: RequestInfo | URL, init: FFetchRequestInit = {}) => {\n const execute = async () => {\n let request = new Request(input, init)\n\n // Merge hooks: per-request hooks override client hooks, but fallback to client hooks\n const effectiveHooks = { ...clientDefaultHooks, ...(init.hooks || {}) }\n if (effectiveHooks.transformRequest) {\n request = await effectiveHooks.transformRequest(request)\n }\n await effectiveHooks.before?.(request)\n\n // Determine retry config (per-request overrides client default)\n const effectiveRetries = init.retries ?? clientDefaultRetries\n const effectiveRetryDelay =\n typeof init.retryDelay !== 'undefined'\n ? init.retryDelay\n : clientDefaultRetryDelay\n const effectiveShouldRetry = init.shouldRetry ?? clientDefaultShouldRetry\n\n // AbortSignal.timeout/any logic\n const effectiveTimeout = init.timeout ?? clientDefaultTimeout\n const userSignal = init.signal\n const transformedSignal = request.signal\n\n const pluginContext: PluginRequestContext = {\n request,\n init,\n state: Object.create(null),\n metadata: {\n startedAt: Date.now(),\n timeoutMs: effectiveTimeout,\n signals: {\n user:\n userSignal === undefined || userSignal === null\n ? undefined\n : userSignal,\n transformed: transformedSignal,\n },\n retry: {\n configuredRetries: effectiveRetries,\n configuredDelay: effectiveRetryDelay,\n attempt: 0,\n },\n },\n }\n\n for (const plugin of plugins) {\n await plugin.preRequest?.(pluginContext)\n }\n\n // Determine throwOnHttpError (per-request overrides client default)\n const effectiveThrowOnHttpError =\n typeof init.throwOnHttpError !== 'undefined'\n ? init.throwOnHttpError\n : (opts.throwOnHttpError ?? false)\n\n // Create timeout signal (manual implementation if AbortSignal.timeout not available)\n function createTimeoutSignal(timeout: number): AbortSignal {\n if (typeof AbortSignal?.timeout === 'function') {\n return AbortSignal.timeout(timeout)\n }\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), timeout)\n controller.signal.addEventListener(\n 'abort',\n () => clearTimeout(timeoutId),\n { once: true }\n )\n return controller.signal\n }\n\n let timeoutSignal: AbortSignal | undefined = undefined\n let combinedSignal: AbortSignal | undefined = undefined\n const controller = new AbortController()\n\n if (effectiveTimeout > 0) {\n timeoutSignal = createTimeoutSignal(effectiveTimeout)\n pluginContext.metadata.signals.timeout = timeoutSignal\n }\n\n const signals: AbortSignal[] = []\n if (userSignal) signals.push(userSignal)\n if (transformedSignal && transformedSignal !== userSignal) {\n signals.push(transformedSignal)\n }\n if (timeoutSignal) signals.push(timeoutSignal)\n signals.push(controller.signal)\n\n if (signals.length === 1) {\n combinedSignal = signals[0]\n } else {\n if (typeof AbortSignal.any !== 'function') {\n throw new Error(\n 'AbortSignal.any is required for combining multiple signals. Please install a polyfill for environments that do not support it.'\n )\n }\n combinedSignal = AbortSignal.any(signals)\n }\n pluginContext.metadata.signals.combined = combinedSignal\n\n let coreErrorReported = false\n const reportCoreError = async (error: unknown, hookRequest: Request) => {\n if (coreErrorReported) return\n if (\n !(error instanceof TimeoutError) &&\n !(error instanceof AbortError) &&\n !(error instanceof NetworkError) &&\n !(error instanceof RetryLimitError)\n ) {\n return\n }\n\n coreErrorReported = true\n if (error instanceof TimeoutError) {\n await effectiveHooks.onTimeout?.(hookRequest)\n } else if (error instanceof AbortError) {\n await effectiveHooks.onAbort?.(hookRequest)\n }\n await effectiveHooks.onError?.(hookRequest, error)\n }\n\n const retryWithHooks = async (\n dispatchCtx: PluginRequestContext,\n dispatchSignal: AbortSignal | undefined\n ) => {\n const requestForAttempt = dispatchCtx.request\n let attempt = 0\n const shouldRetryWithHook = async (\n ctx: import('./types').RetryContext\n ) => {\n attempt = ctx.attempt\n dispatchCtx.metadata.retry.attempt = attempt\n dispatchCtx.metadata.retry.lastError = ctx.error\n dispatchCtx.metadata.retry.lastResponse = ctx.response\n const retrying = effectiveShouldRetry(ctx)\n dispatchCtx.metadata.retry.shouldRetryResult = retrying\n if (retrying && attempt <= effectiveRetries) {\n await effectiveHooks.onRetry?.(\n requestForAttempt,\n attempt - 1,\n ctx.error,\n ctx.response\n )\n }\n return retrying\n }\n\n let lastResponse: Response | undefined = undefined\n try {\n let res = await retry(\n async () => {\n if (controller.signal.aborted) {\n throw new AbortError('Request was aborted')\n }\n if (userSignal?.aborted) {\n throw new AbortError('Request was aborted by user')\n }\n if (timeoutSignal?.aborted) {\n throw new TimeoutError('signal timed out')\n }\n if (dispatchSignal?.aborted) {\n if (userSignal?.aborted) {\n throw new AbortError('Request was aborted by user')\n } else if (timeoutSignal?.aborted) {\n throw new TimeoutError('signal timed out')\n } else {\n throw new AbortError(\n 'Request was aborted',\n dispatchSignal.reason\n )\n }\n }\n const reqWithSignal = new Request(requestForAttempt.clone(), {\n signal: dispatchSignal,\n })\n try {\n const handler = init.fetchHandler ?? fetchHandler ?? fetch\n const response = await handler(reqWithSignal)\n lastResponse = response\n dispatchCtx.metadata.retry.lastResponse = response\n return response\n } catch (err) {\n dispatchCtx.metadata.retry.lastError = err\n if (err instanceof DOMException && err.name === 'AbortError') {\n if (\n timeoutSignal?.aborted &&\n (!userSignal || !userSignal.aborted)\n ) {\n throw new TimeoutError('signal timed out', err)\n } else if (userSignal?.aborted) {\n throw new AbortError('Request was aborted by user')\n } else if (controller.signal.aborted) {\n throw new AbortError('Request was aborted', err)\n } else {\n throw new AbortError(\n 'Request was aborted',\n new DOMException('Aborted', 'AbortError')\n )\n }\n } else if (\n err instanceof TypeError &&\n /NetworkError|network error|failed to fetch|lost connection|NetworkError when attempting to fetch resource/i.test(\n err.message\n )\n ) {\n throw new NetworkError(err.message, err)\n }\n throw err\n }\n },\n effectiveRetries,\n effectiveRetryDelay,\n shouldRetryWithHook,\n requestForAttempt,\n dispatchSignal\n )\n if (effectiveHooks.transformResponse) {\n res = await effectiveHooks.transformResponse(res, requestForAttempt)\n }\n await effectiveHooks.after?.(requestForAttempt, res)\n if (\n effectiveThrowOnHttpError &&\n ((res.status >= 400 && res.status < 500 && res.status !== 429) ||\n res.status >= 500 ||\n res.status === 429)\n ) {\n const { HttpError } = await import('./error.js')\n throw new HttpError(\n `HTTP error: ${res.status} ${res.statusText}`,\n res\n )\n }\n return res\n } catch (err: unknown) {\n dispatchCtx.metadata.retry.lastError = err\n if (err instanceof TimeoutError) {\n if (dispatchCtx === pluginContext) {\n await reportCoreError(err, requestForAttempt)\n }\n throw err\n }\n if (err instanceof AbortError) {\n if (dispatchCtx === pluginContext) {\n await reportCoreError(err, requestForAttempt)\n }\n throw err\n }\n if (lastResponse) {\n const resp = lastResponse as Response\n if (\n effectiveThrowOnHttpError &&\n ((resp.status >= 400 &&\n resp.status < 500 &&\n resp.status !== 429) ||\n resp.status >= 500 ||\n resp.status === 429)\n ) {\n const { HttpError } = await import('./error.js')\n throw new HttpError(\n `HTTP error: ${resp.status} ${resp.statusText}`,\n resp\n )\n }\n return resp\n }\n if (err instanceof NetworkError) {\n if (dispatchCtx === pluginContext) {\n await reportCoreError(err, requestForAttempt)\n }\n throw err\n }\n const retryErr = new RetryLimitError(\n typeof err === 'object' &&\n err &&\n 'message' in err &&\n typeof (err as { message?: unknown }).message === 'string'\n ? (err as { message: string }).message\n : 'Retry limit reached',\n err\n )\n if (dispatchCtx === pluginContext) {\n await reportCoreError(retryErr, requestForAttempt)\n }\n throw retryErr\n }\n }\n\n const baseDispatch: PluginDispatch = async (ctx) => {\n const dispatchSignal =\n ctx === pluginContext ? combinedSignal : ctx.request.signal\n return retryWithHooks(ctx, dispatchSignal)\n }\n\n let dispatch = baseDispatch\n for (let i = plugins.length - 1; i >= 0; i--) {\n const plugin = plugins[i]\n if (plugin.wrapDispatch) {\n dispatch = plugin.wrapDispatch(dispatch)\n }\n }\n\n let completeCalled = false\n const callComplete = async (\n response: Response | undefined,\n error: unknown\n ) => {\n if (completeCalled) return\n completeCalled = true\n await effectiveHooks.onComplete?.(request, response, error)\n }\n\n const actualPromise = dispatch(pluginContext)\n .then(async (response) => {\n await callComplete(response, undefined)\n for (const plugin of plugins) {\n await plugin.onSuccess?.(pluginContext, response)\n }\n return response\n })\n .catch(async (err: unknown) => {\n await reportCoreError(err, request)\n await callComplete(undefined, err)\n for (const plugin of plugins) {\n await plugin.onError?.(pluginContext, err)\n }\n throw err\n })\n\n const pendingEntry: PendingRequest = {\n promise: actualPromise,\n request,\n controller,\n }\n pendingRequests.push(pendingEntry)\n\n return actualPromise.finally(async () => {\n for (const plugin of plugins) {\n await plugin.onFinally?.(pluginContext)\n }\n\n const index = pendingRequests.indexOf(pendingEntry)\n if (index > -1) {\n pendingRequests.splice(index, 1)\n }\n })\n }\n\n let promise = execute() as Promise<Response>\n for (const plugin of plugins) {\n if (plugin.decoratePromise) {\n promise = plugin.decoratePromise(promise)\n }\n }\n return promise as Promise<Response> &\n PluginRequestPromiseExtensions<TPlugins>\n }\n\n Object.defineProperty(client, 'pendingRequests', {\n get() {\n return pendingRequests\n },\n enumerable: false,\n configurable: false,\n })\n\n Object.defineProperty(client, 'abortAll', {\n value: abortAll,\n writable: false,\n enumerable: false,\n configurable: false,\n })\n\n Object.defineProperties(client, extensionDescriptors)\n\n return client as FFetch<\n PluginExtensions<TPlugins>,\n PluginRequestPromiseExtensions<TPlugins>\n >\n}\n"],"mappings":"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","names":["shouldRetry","controller","HttpError"]}
1
+ {"version":3,"sources":["../src/error.ts","../src/index.ts","../src/retry.ts","../src/should-retry.ts","../src/client.ts"],"sourcesContent":["// Base error class to reduce duplication\nclass BaseError extends Error {\n public cause?: unknown\n constructor(name: string, message: string, cause?: unknown) {\n super(message)\n this.name = name\n if (cause !== undefined) this.cause = cause\n }\n}\n\nexport class TimeoutError extends BaseError {\n constructor(message = 'Request timed out', cause?: unknown) {\n super('TimeoutError', message, cause)\n }\n}\n\nexport class CircuitOpenError extends BaseError {\n constructor(message = 'Circuit is open', cause?: unknown) {\n super('CircuitOpenError', message, cause)\n }\n}\n\nexport class BulkheadFullError extends BaseError {\n constructor(message = 'Bulkhead queue is full', cause?: unknown) {\n super('BulkheadFullError', message, cause)\n }\n}\n\nexport class AbortError extends BaseError {\n constructor(message = 'Request was aborted', cause?: unknown) {\n super('AbortError', message, cause)\n }\n}\n\nexport class RetryLimitError extends BaseError {\n constructor(message = 'Retry limit reached', cause?: unknown) {\n super('RetryLimitError', message, cause)\n }\n}\n\nexport class NetworkError extends BaseError {\n constructor(message = 'Network error occurred', cause?: unknown) {\n super('NetworkError', message, cause)\n }\n}\n\nexport class HttpError extends BaseError {\n constructor(message = 'HTTP error occurred', cause?: unknown) {\n super('HttpError', message, cause)\n }\n}\n","export type { FFetch, FFetchOptions } from './types'\nexport type { Hooks } from './hooks'\nexport type {\n ClientPlugin,\n PluginRequestContext,\n PluginDispatch,\n PluginSetupContext,\n} from './plugins'\n\nexport { createClient } from './client'\n\nexport {\n TimeoutError,\n CircuitOpenError,\n BulkheadFullError,\n AbortError,\n RetryLimitError,\n NetworkError,\n HttpError,\n} from './error'\n","import type { RetryContext } from './types.js'\n\nexport type RetryDelay = number | ((ctx: RetryContext) => number)\n\nexport const defaultDelay: RetryDelay = (ctx) => {\n const retryAfter = ctx.response?.headers.get('Retry-After')\n if (retryAfter) {\n const seconds = parseInt(retryAfter, 10)\n if (!isNaN(seconds)) return seconds * 1000\n const date = Date.parse(retryAfter)\n if (!isNaN(date)) return Math.max(0, date - Date.now())\n }\n return 2 ** ctx.attempt * 200 + Math.random() * 100\n}\n\nfunction waitForRetryDelay(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0) return Promise.resolve()\n return new Promise((resolve) => {\n if (!signal) {\n setTimeout(resolve, ms)\n return\n }\n\n if (signal.aborted) {\n resolve()\n return\n }\n\n const onAbort = () => {\n clearTimeout(timer)\n resolve()\n }\n\n const timer = setTimeout(() => {\n signal.removeEventListener('abort', onAbort)\n resolve()\n }, ms)\n\n signal.addEventListener('abort', onAbort, { once: true })\n })\n}\n\nexport async function retry(\n fn: () => Promise<Response>,\n retries: number,\n delay: RetryDelay,\n shouldRetry: (ctx: RetryContext) => boolean | Promise<boolean> = () => true,\n request: Request,\n signal?: AbortSignal\n): Promise<Response> {\n let lastErr: unknown\n let lastRes: Response | undefined\n\n for (let i = 0; i <= retries; i++) {\n const ctx: RetryContext = {\n attempt: i + 1,\n request,\n response: lastRes,\n error: lastErr,\n }\n try {\n lastRes = await fn()\n } catch (err) {\n lastErr = err\n ctx.error = err\n if (i === retries || !(await shouldRetry(ctx))) throw err\n const wait = typeof delay === 'function' ? delay(ctx) : delay\n await waitForRetryDelay(wait, signal)\n continue\n }\n\n ctx.response = lastRes\n ctx.error = undefined\n if (i < retries && (await shouldRetry(ctx))) {\n const wait = typeof delay === 'function' ? delay(ctx) : delay\n await waitForRetryDelay(wait, signal)\n continue\n }\n return lastRes\n }\n throw lastErr\n}\n","import { AbortError, CircuitOpenError, TimeoutError } from './error.js'\nimport type { RetryContext } from './types.js'\n\nexport function shouldRetry(ctx: RetryContext): boolean {\n const { error, response } = ctx\n if (\n error instanceof AbortError ||\n error instanceof CircuitOpenError ||\n error instanceof TimeoutError\n )\n return false\n if (!response) return true // network error\n return response.status >= 500 || response.status === 429\n}\n","import type {\n FFetchOptions,\n FFetch,\n FFetchRequestInit,\n PendingRequest,\n} from './types.js'\nimport { retry, defaultDelay } from './retry.js'\nimport { shouldRetry as defaultShouldRetry } from './should-retry.js'\nimport {\n type PluginDispatch,\n type PluginRequestContext,\n type PluginExtensions,\n type PluginRequestPromiseExtensions,\n type ClientPlugin,\n type PluginExtensionBase,\n type PluginRequestPromiseExtensionBase,\n} from './plugins.js'\nimport {\n TimeoutError,\n AbortError,\n RetryLimitError,\n NetworkError,\n} from './error.js'\n\nexport function createClient<\n TPlugins extends readonly ClientPlugin<\n PluginExtensionBase,\n PluginRequestPromiseExtensionBase\n >[] = readonly ClientPlugin<\n PluginExtensionBase,\n PluginRequestPromiseExtensionBase\n >[],\n>(\n opts: FFetchOptions<TPlugins> = {} as FFetchOptions<TPlugins>\n): FFetch<\n PluginExtensions<TPlugins>,\n PluginRequestPromiseExtensions<TPlugins>\n> {\n const {\n timeout: clientDefaultTimeout = 5_000,\n retries: clientDefaultRetries = 0,\n retryDelay: clientDefaultRetryDelay = defaultDelay,\n shouldRetry: clientDefaultShouldRetry = defaultShouldRetry,\n hooks: clientDefaultHooks = {},\n fetchHandler,\n plugins: inputPlugins = [] as unknown as TPlugins,\n } = opts\n\n const extensionDescriptors: PropertyDescriptorMap = Object.create(null)\n\n const plugins = inputPlugins\n .map((plugin, index) => ({ plugin, index }))\n .sort((a, b) => {\n const aOrder = a.plugin.order ?? 0\n const bOrder = b.plugin.order ?? 0\n if (aOrder !== bOrder) return aOrder - bOrder\n return a.index - b.index\n })\n .map((entry) => entry.plugin)\n\n for (const plugin of plugins) {\n plugin.setup?.({\n defineExtension: (key, descriptor) => {\n const propertyKey = key as PropertyKey\n if (propertyKey in extensionDescriptors) {\n throw new Error(\n `Plugin extension collision for property \"${String(propertyKey)}\"`\n )\n }\n if ('get' in descriptor) {\n extensionDescriptors[propertyKey] = {\n get: descriptor.get,\n enumerable: descriptor.enumerable ?? true,\n configurable: false,\n }\n return\n }\n extensionDescriptors[propertyKey] = {\n value: descriptor.value,\n writable: false,\n enumerable: descriptor.enumerable ?? true,\n configurable: false,\n }\n },\n })\n }\n\n const pendingRequests: PendingRequest[] = []\n\n // Helper to abort all pending requests\n function abortAll() {\n for (const entry of pendingRequests) {\n entry.controller?.abort()\n }\n }\n\n const client = (input: RequestInfo | URL, init: FFetchRequestInit = {}) => {\n const execute = async () => {\n let request = new Request(input, init)\n\n // Merge hooks: per-request hooks override client hooks, but fallback to client hooks\n const effectiveHooks = { ...clientDefaultHooks, ...(init.hooks || {}) }\n if (effectiveHooks.transformRequest) {\n request = await effectiveHooks.transformRequest(request)\n }\n await effectiveHooks.before?.(request)\n\n // Determine retry config (per-request overrides client default)\n const effectiveRetries = init.retries ?? clientDefaultRetries\n const effectiveRetryDelay =\n typeof init.retryDelay !== 'undefined'\n ? init.retryDelay\n : clientDefaultRetryDelay\n const effectiveShouldRetry = init.shouldRetry ?? clientDefaultShouldRetry\n\n // AbortSignal.timeout/any logic\n const effectiveTimeout = init.timeout ?? clientDefaultTimeout\n const userSignal = init.signal\n const transformedSignal = request.signal\n\n const pluginContext: PluginRequestContext = {\n request,\n init,\n state: Object.create(null),\n metadata: {\n startedAt: Date.now(),\n timeoutMs: effectiveTimeout,\n signals: {\n user:\n userSignal === undefined || userSignal === null\n ? undefined\n : userSignal,\n transformed: transformedSignal,\n },\n retry: {\n configuredRetries: effectiveRetries,\n configuredDelay: effectiveRetryDelay,\n attempt: 0,\n },\n },\n }\n\n for (const plugin of plugins) {\n await plugin.preRequest?.(pluginContext)\n }\n\n // Determine throwOnHttpError (per-request overrides client default)\n const effectiveThrowOnHttpError =\n typeof init.throwOnHttpError !== 'undefined'\n ? init.throwOnHttpError\n : (opts.throwOnHttpError ?? false)\n\n // Create timeout signal (manual implementation if AbortSignal.timeout not available)\n function createTimeoutSignal(timeout: number): AbortSignal {\n if (typeof AbortSignal?.timeout === 'function') {\n return AbortSignal.timeout(timeout)\n }\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), timeout)\n controller.signal.addEventListener(\n 'abort',\n () => clearTimeout(timeoutId),\n { once: true }\n )\n return controller.signal\n }\n\n let timeoutSignal: AbortSignal | undefined = undefined\n let combinedSignal: AbortSignal | undefined = undefined\n const controller = new AbortController()\n\n if (effectiveTimeout > 0) {\n timeoutSignal = createTimeoutSignal(effectiveTimeout)\n pluginContext.metadata.signals.timeout = timeoutSignal\n }\n\n const signals: AbortSignal[] = []\n if (userSignal) signals.push(userSignal)\n if (transformedSignal && transformedSignal !== userSignal) {\n signals.push(transformedSignal)\n }\n if (timeoutSignal) signals.push(timeoutSignal)\n signals.push(controller.signal)\n\n if (signals.length === 1) {\n combinedSignal = signals[0]\n } else {\n if (typeof AbortSignal.any !== 'function') {\n throw new Error(\n 'AbortSignal.any is required for combining multiple signals. Please install a polyfill for environments that do not support it.'\n )\n }\n combinedSignal = AbortSignal.any(signals)\n }\n pluginContext.metadata.signals.combined = combinedSignal\n\n let coreErrorReported = false\n const reportCoreError = async (error: unknown, hookRequest: Request) => {\n if (coreErrorReported) return\n if (\n !(error instanceof TimeoutError) &&\n !(error instanceof AbortError) &&\n !(error instanceof NetworkError) &&\n !(error instanceof RetryLimitError)\n ) {\n return\n }\n\n coreErrorReported = true\n if (error instanceof TimeoutError) {\n await effectiveHooks.onTimeout?.(hookRequest)\n } else if (error instanceof AbortError) {\n await effectiveHooks.onAbort?.(hookRequest)\n }\n await effectiveHooks.onError?.(hookRequest, error)\n }\n\n const retryWithHooks = async (\n dispatchCtx: PluginRequestContext,\n dispatchSignal: AbortSignal | undefined\n ) => {\n const requestForAttempt = dispatchCtx.request\n let attempt = 0\n const shouldRetryWithHook = async (\n ctx: import('./types').RetryContext\n ) => {\n attempt = ctx.attempt\n dispatchCtx.metadata.retry.attempt = attempt\n dispatchCtx.metadata.retry.lastError = ctx.error\n dispatchCtx.metadata.retry.lastResponse = ctx.response\n const retrying = effectiveShouldRetry(ctx)\n dispatchCtx.metadata.retry.shouldRetryResult = retrying\n if (retrying && attempt <= effectiveRetries) {\n await effectiveHooks.onRetry?.(\n requestForAttempt,\n attempt - 1,\n ctx.error,\n ctx.response\n )\n }\n return retrying\n }\n\n let lastResponse: Response | undefined = undefined\n try {\n let res = await retry(\n async () => {\n if (controller.signal.aborted) {\n throw new AbortError('Request was aborted')\n }\n if (userSignal?.aborted) {\n throw new AbortError('Request was aborted by user')\n }\n if (timeoutSignal?.aborted) {\n throw new TimeoutError('signal timed out')\n }\n if (dispatchSignal?.aborted) {\n if (userSignal?.aborted) {\n throw new AbortError('Request was aborted by user')\n } else if (timeoutSignal?.aborted) {\n throw new TimeoutError('signal timed out')\n } else {\n throw new AbortError(\n 'Request was aborted',\n dispatchSignal.reason\n )\n }\n }\n const reqWithSignal = new Request(requestForAttempt.clone(), {\n signal: dispatchSignal,\n })\n try {\n const handler = init.fetchHandler ?? fetchHandler ?? fetch\n const response = await handler(reqWithSignal)\n lastResponse = response\n dispatchCtx.metadata.retry.lastResponse = response\n return response\n } catch (err) {\n dispatchCtx.metadata.retry.lastError = err\n if (err instanceof DOMException && err.name === 'AbortError') {\n if (\n timeoutSignal?.aborted &&\n (!userSignal || !userSignal.aborted)\n ) {\n throw new TimeoutError('signal timed out', err)\n } else if (userSignal?.aborted) {\n throw new AbortError('Request was aborted by user')\n } else if (controller.signal.aborted) {\n throw new AbortError('Request was aborted', err)\n } else {\n throw new AbortError(\n 'Request was aborted',\n new DOMException('Aborted', 'AbortError')\n )\n }\n } else if (\n err instanceof TypeError &&\n /NetworkError|network error|failed to fetch|lost connection|NetworkError when attempting to fetch resource/i.test(\n err.message\n )\n ) {\n throw new NetworkError(err.message, err)\n }\n throw err\n }\n },\n effectiveRetries,\n effectiveRetryDelay,\n shouldRetryWithHook,\n requestForAttempt,\n dispatchSignal\n )\n if (effectiveHooks.transformResponse) {\n res = await effectiveHooks.transformResponse(res, requestForAttempt)\n }\n await effectiveHooks.after?.(requestForAttempt, res)\n if (\n effectiveThrowOnHttpError &&\n ((res.status >= 400 && res.status < 500 && res.status !== 429) ||\n res.status >= 500 ||\n res.status === 429)\n ) {\n const { HttpError } = await import('./error.js')\n throw new HttpError(\n `HTTP error: ${res.status} ${res.statusText}`,\n res\n )\n }\n return res\n } catch (err: unknown) {\n dispatchCtx.metadata.retry.lastError = err\n if (err instanceof TimeoutError) {\n if (dispatchCtx === pluginContext) {\n await reportCoreError(err, requestForAttempt)\n }\n throw err\n }\n if (err instanceof AbortError) {\n if (dispatchCtx === pluginContext) {\n await reportCoreError(err, requestForAttempt)\n }\n throw err\n }\n if (lastResponse) {\n const resp = lastResponse as Response\n if (\n effectiveThrowOnHttpError &&\n ((resp.status >= 400 &&\n resp.status < 500 &&\n resp.status !== 429) ||\n resp.status >= 500 ||\n resp.status === 429)\n ) {\n const { HttpError } = await import('./error.js')\n throw new HttpError(\n `HTTP error: ${resp.status} ${resp.statusText}`,\n resp\n )\n }\n return resp\n }\n if (err instanceof NetworkError) {\n if (dispatchCtx === pluginContext) {\n await reportCoreError(err, requestForAttempt)\n }\n throw err\n }\n const retryErr = new RetryLimitError(\n typeof err === 'object' &&\n err &&\n 'message' in err &&\n typeof (err as { message?: unknown }).message === 'string'\n ? (err as { message: string }).message\n : 'Retry limit reached',\n err\n )\n if (dispatchCtx === pluginContext) {\n await reportCoreError(retryErr, requestForAttempt)\n }\n throw retryErr\n }\n }\n\n const baseDispatch: PluginDispatch = async (ctx) => {\n const dispatchSignal =\n ctx === pluginContext ? combinedSignal : ctx.request.signal\n return retryWithHooks(ctx, dispatchSignal)\n }\n\n let dispatch = baseDispatch\n for (let i = plugins.length - 1; i >= 0; i--) {\n const plugin = plugins[i]\n if (plugin.wrapDispatch) {\n dispatch = plugin.wrapDispatch(dispatch)\n }\n }\n\n let completeCalled = false\n const callComplete = async (\n response: Response | undefined,\n error: unknown\n ) => {\n if (completeCalled) return\n completeCalled = true\n await effectiveHooks.onComplete?.(request, response, error)\n }\n\n const actualPromise = dispatch(pluginContext)\n .then(async (response) => {\n await callComplete(response, undefined)\n for (const plugin of plugins) {\n await plugin.onSuccess?.(pluginContext, response)\n }\n return response\n })\n .catch(async (err: unknown) => {\n await reportCoreError(err, request)\n await callComplete(undefined, err)\n for (const plugin of plugins) {\n await plugin.onError?.(pluginContext, err)\n }\n throw err\n })\n\n const pendingEntry: PendingRequest = {\n promise: actualPromise,\n request,\n controller,\n }\n pendingRequests.push(pendingEntry)\n\n return actualPromise.finally(async () => {\n for (const plugin of plugins) {\n await plugin.onFinally?.(pluginContext)\n }\n\n const index = pendingRequests.indexOf(pendingEntry)\n if (index > -1) {\n pendingRequests.splice(index, 1)\n }\n })\n }\n\n let promise = execute() as Promise<Response>\n for (const plugin of plugins) {\n if (plugin.decoratePromise) {\n promise = plugin.decoratePromise(promise)\n }\n }\n return promise as Promise<Response> &\n PluginRequestPromiseExtensions<TPlugins>\n }\n\n Object.defineProperty(client, 'pendingRequests', {\n get() {\n return pendingRequests\n },\n enumerable: false,\n configurable: false,\n })\n\n Object.defineProperty(client, 'abortAll', {\n value: abortAll,\n writable: false,\n enumerable: false,\n configurable: false,\n })\n\n Object.defineProperties(client, extensionDescriptors)\n\n return client as FFetch<\n PluginExtensions<TPlugins>,\n PluginRequestPromiseExtensions<TPlugins>\n >\n}\n"],"mappings":"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","names":["shouldRetry","controller","HttpError"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fetchkit/ffetch",
3
- "version": "5.6.0",
3
+ "version": "5.6.2",
4
4
  "description": "Fetch wrapper with configurable timeouts, retries, and TypeScript-first DX",
5
5
  "keywords": [
6
6
  "fetch",
@@ -17,6 +17,9 @@
17
17
  "homepage": "https://github.com/fetch-kit/ffetch#readme",
18
18
  "bugs": "https://github.com/fetch-kit/ffetch/issues",
19
19
  "author": "Gabor Koos <gabor@gaborkoos.com>",
20
+ "engines": {
21
+ "node": ">=20.6.0"
22
+ },
20
23
  "license": "MIT",
21
24
  "main": "./dist/index.cjs",
22
25
  "module": "./dist/index.js",
@@ -88,7 +91,7 @@
88
91
  "prepublishOnly": "npm run build && npm run test:ci"
89
92
  },
90
93
  "devDependencies": {
91
- "@changesets/cli": "^2.29.5",
94
+ "@changesets/cli": "^3.0.0",
92
95
  "@eslint/js": "^10.0.1",
93
96
  "@types/node": "^25.9.1",
94
97
  "@typescript-eslint/eslint-plugin": "^8.39.0",
@@ -103,5 +106,8 @@
103
106
  "tsup": "^8.5.0",
104
107
  "typescript": "^6.0.3",
105
108
  "vitest": "^4.1.8"
109
+ },
110
+ "overrides": {
111
+ "esbuild": "0.28.2"
106
112
  }
107
113
  }