Skip to content

Commit 1428df6

Browse files
committed
Documentation errors fixed
1 parent 6b761f5 commit 1428df6

7 files changed

Lines changed: 80 additions & 62 deletions

File tree

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,12 +156,12 @@ Native `fetch`'s controversial behavior of not throwing errors for HTTP error st
156156

157157
## Environment Requirements
158158

159-
`ffetch` requires modern AbortSignal APIs:
159+
`ffetch` works best with native `AbortSignal.any` support:
160160

161-
- **Node.js 20.6+** (for AbortSignal.any)
162-
- **Modern browsers** (Chrome 117+, Firefox 117+, Safari 17+, Edge 117+)
161+
- **Node.js 20.6+** (native `AbortSignal.any`)
162+
- **Modern browsers with `AbortSignal.any`** (for example: Chrome 117+, Firefox 117+, Safari 17+, Edge 117+)
163163

164-
If your environment does not support `AbortSignal.any` (Node.js < 20.6, older browsers), you **must install a polyfill** before using ffetch. See the [compatibility guide](./docs/compatibility.md) for instructions.
164+
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.
165165

166166
**Custom fetch support:**
167167
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.

docs/advanced.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ You can provide a function for `retryDelay` that receives a context object:
126126
```typescript
127127
const client = createClient({
128128
retryDelay: ({ attempt, request, response, error }) => {
129-
// attempt: number (starts at 2 for first retry)
129+
// attempt: number (starts at 1 for first retry decision)
130130
// request: Request
131131
// response: Response | undefined
132132
// error: unknown
@@ -198,13 +198,14 @@ This is useful for:
198198
- Implementing custom fallback or degraded mode logic
199199
- Integrating with dashboards or metrics
200200

201-
> **Note:** If the client is not configured with a circuit breaker (`circuit` option omitted), `client.circuitOpen` will always be `false` and the property is inert.
201+
> **Note:** `client.circuitOpen` is provided by `circuitPlugin`. If that plugin is not installed, this extension is not available on the client.
202202
203203
### How it Works
204204

205205
- When the number of consecutive failures reaches the `threshold`, the circuit "opens" and all further requests fail fast with a `CircuitOpenError`
206206
- After the `reset` period (in milliseconds), the circuit "closes" and requests are allowed again
207207
- If a request succeeds, the failure count resets
208+
- If `onCircuitOpen` is configured, it runs both when the circuit opens and when requests are blocked while it is already open
208209

209210
### Configuration
210211

docs/api.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ See [plugins.md](./plugins.md) for full lifecycle, ordering, extensions, and adv
116116

117117
### Removed Legacy Options (Breaking)
118118

119-
The following top-level options were removed and now throw a migration error if passed:
119+
The following top-level options were removed:
120120

121121
- `dedupe`
122122
- `dedupeHashFn`
@@ -164,5 +164,5 @@ type FFetch = {
164164
### Notes
165165
166166
- Signal combination (user, timeout, transformRequest) requires `AbortSignal.any`.
167-
- The first retry attempt uses `attempt = 2`.
167+
- The first retry decision uses `attempt = 1`.
168168
- Plugin order is deterministic: first by `order`, then by registration order.

docs/compatibility.md

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Prerequisites
44

5-
`ffetch` requires modern AbortSignal APIs, specifically `AbortSignal.timeout` and **AbortSignal.any**. Signal combination requires `AbortSignal.any` (native or polyfill).
5+
`ffetch` can work without native `AbortSignal.timeout` because it has an internal timeout fallback. Signal combination requires **`AbortSignal.any`** (native or polyfill).
66

77
## Node.js Support
88

@@ -13,7 +13,8 @@
1313

1414
### Polyfills for Older Versions
1515

16-
For older Node.js versions, you must install a polyfill for both `AbortSignal.timeout` and `AbortSignal.any`:
16+
For older Node.js versions, you must install a polyfill for `AbortSignal.any`.
17+
`AbortSignal.timeout` polyfill is optional because `ffetch` has an internal timeout fallback:
1718

1819
```bash
1920
npm install abortcontroller-polyfill abort-controller-x
@@ -22,10 +23,10 @@ npm install abortcontroller-polyfill abort-controller-x
2223
Then ensure the APIs are available globally before importing `ffetch`:
2324

2425
```javascript
25-
// Option 1: abortcontroller-polyfill (for AbortSignal.timeout)
26+
// Optional: abortcontroller-polyfill (for native-style AbortSignal.timeout)
2627
require('abortcontroller-polyfill/dist/polyfill-patch-fetch')
2728

28-
// Option 2: abort-controller-x (for AbortSignal.any)
29+
// Required when AbortSignal.any is missing
2930
import 'abort-controller-x/polyfill'
3031

3132
// Now you can use ffetch
@@ -43,50 +44,60 @@ await client('https://api.example.com') // Works
4344
await client('http://localhost:3000') // Works
4445
```
4546

46-
#### Custom Agents
47+
#### Custom Node Connection Agent
4748

4849
```javascript
4950
import https from 'https'
51+
import fetch from 'node-fetch'
5052

51-
const client = createClient()
52-
53-
// Use custom HTTPS agent
54-
await client('https://api.example.com', {
55-
agent: new https.Agent({
56-
keepAlive: true,
57-
timeout: 5000,
58-
}),
53+
const agent = new https.Agent({
54+
keepAlive: true,
55+
timeout: 5000,
5956
})
57+
58+
// Wrap your fetch implementation and inject transport-specific options there.
59+
const fetchWithAgent = (input, init) => fetch(input, { ...init, agent })
60+
61+
const client = createClient({ fetchHandler: fetchWithAgent })
62+
63+
await client('https://api.example.com')
6064
```
6165

66+
`agent` is fetch-implementation-specific and not part of standard `RequestInit`. Prefer configuring it inside `fetchHandler`.
67+
6268
#### Self-signed Certificates (Development)
6369

6470
```javascript
6571
// For development only - never use in production
6672
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
6773

68-
// Better approach: use custom agent
74+
// Better approach: configure a custom agent in fetchHandler
6975
import https from 'https'
76+
import fetch from 'node-fetch'
7077

7178
const agent = new https.Agent({
7279
rejectUnauthorized: false,
7380
})
7481

75-
await client('https://localhost:8443', { agent })
82+
const client = createClient({
83+
fetchHandler: (input, init) => fetch(input, { ...init, agent }),
84+
})
85+
86+
await client('https://localhost:8443')
7687
```
7788

7889
## Browser Support
7990

8091
### Modern Browsers (Recommended)
8192

82-
- **Chrome 88+**: Full support
83-
- **Firefox 89+**: Full support
84-
- **Safari 15.4+**: Full support
85-
- **Edge 88+**: Full support
93+
- **Chrome 117+**: Native support for required AbortSignal APIs
94+
- **Firefox 117+**: Native support for required AbortSignal APIs
95+
- **Safari 17+**: Native support for required AbortSignal APIs
96+
- **Edge 117+**: Native support for required AbortSignal APIs
8697

8798
### Legacy Browser Support
8899

89-
For older browsers, you need polyfills for `AbortSignal.timeout` and `AbortSignal.any`:
100+
Older browsers can still work when `AbortSignal.any` is polyfilled. `AbortSignal.timeout` polyfill is optional:
90101

91102
```html
92103
<!-- Include polyfills before your app -->
@@ -95,7 +106,7 @@ For older browsers, you need polyfills for `AbortSignal.timeout` and `AbortSigna
95106

96107
<!-- Your app -->
97108
<script type="module">
98-
import createClient from 'https://unpkg.com/@fetchkit/ffetch/dist/index.min.js'
109+
import { createClient } from 'https://unpkg.com/@fetchkit/ffetch/dist/index.min.js'
99110
// ... your code
100111
</script>
101112
```
@@ -118,8 +129,8 @@ self.addEventListener('fetch', async (event) => {
118129
#### Web Workers
119130

120131
```javascript
121-
// Works in web workers
122-
importScripts('https://unpkg.com/@fetchkit/ffetch/dist/index.min.js')
132+
// Use a module worker and import the ESM build
133+
import { createClient } from 'https://unpkg.com/@fetchkit/ffetch/dist/index.min.js'
123134

124135
const client = createClient()
125136
self.postMessage(await client('/api/data').then((r) => r.json()))
@@ -202,10 +213,6 @@ function checkCompatibility() {
202213
throw new Error('AbortSignal not supported. Please add a polyfill.')
203214
}
204215

205-
if (typeof AbortSignal.timeout !== 'function') {
206-
throw new Error('AbortSignal.timeout not supported. Please add a polyfill.')
207-
}
208-
209216
if (typeof AbortSignal.any !== 'function') {
210217
throw new Error(
211218
'AbortSignal.any is required for combining multiple signals. Please install a polyfill.'
@@ -234,22 +241,16 @@ const client = createClient({
234241
235242
```html
236243
<script type="module">
237-
import createClient from 'https://unpkg.com/@fetchkit/ffetch/dist/index.min.js'
244+
import { createClient } from 'https://unpkg.com/@fetchkit/ffetch/dist/index.min.js'
238245

239246
const client = createClient()
240247
const data = await client('/api/data').then((r) => r.json())
241248
</script>
242249
```
243250
244-
### UMD (Legacy)
251+
### UMD
245252
246-
```html
247-
<script src="https://unpkg.com/@fetchkit/ffetch/dist/index.umd.js"></script>
248-
<script>
249-
const client = FFetch.createClient()
250-
// ... use client
251-
</script>
252-
```
253+
`ffetch` does not currently publish a UMD build. Use the ESM build shown above (or import from the package in a bundler/runtime environment).
253254
254255
## Framework Integration
255256
@@ -355,7 +356,7 @@ onUnmounted(() => {
355356
356357
### SSR Frameworks: SvelteKit, Next.js, Nuxt
357358
358-
For SvelteKit, Next.js, and Nuxt, you must pass the exact fetch instance provided by the framework in your handler or context. This is not the global fetch, and the parameter name may vary (often `fetch`, but check your framework docs).
359+
For SvelteKit, Next.js, and Nuxt, it is recommended to pass the fetch instance provided by the framework in your handler or context for SSR-safe behavior. The parameter name may vary (often `fetch`, but check your framework docs).
359360
360361
**SvelteKit example:**
361362
@@ -392,7 +393,7 @@ export default async function handler(request) {
392393
}
393394
```
394395
395-
> Always use the fetch instance provided by the framework in your handler/context, not the global fetch. The parameter name may vary, but it is always context-specific.
396+
> Recommended: use the fetch instance provided by the framework in handler/context code. ffetch also supports any fetch-compatible implementation and falls back to global fetch when no `fetchHandler` is provided.
396397
397398
## Troubleshooting
398399
@@ -401,7 +402,9 @@ export default async function handler(request) {
401402
#### "AbortSignal.timeout is not a function"
402403
403404
```
404-
Solution: Add a polyfill for AbortSignal.timeout
405+
ffetch has an internal fallback, so this is usually non-fatal.
406+
407+
Optional: add a polyfill for AbortSignal.timeout
405408
npm install abortcontroller-polyfill
406409
```
407410

docs/examples.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ if (client.circuitOpen) {
2424
}
2525
```
2626

27-
// If the client is not configured with a circuit breaker, client.circuitOpen will always be false.
27+
// `client.circuitOpen` is available when `circuitPlugin(...)` is installed on the client.
2828

2929
### Simple HTTP Client
3030

docs/hooks.md

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,29 @@ Hooks allow you to observe, log, or modify the request/response lifecycle. All h
44

55
## Lifecycle Hooks
66

7+
In this document, **core hooks** means the callbacks under `createClient({ hooks: ... })`:
8+
9+
- `before`
10+
- `after`
11+
- `onError`
12+
- `onRetry`
13+
- `onTimeout`
14+
- `onAbort`
15+
- `onComplete`
16+
- `transformRequest`
17+
- `transformResponse`
18+
19+
These are separate from plugin lifecycle callbacks (`setup`, `preRequest`, `wrapDispatch`, `onSuccess`, `onError`, `onFinally`).
20+
721
### Available Hooks
822

923
- `before(req)`: Called before each request is sent
1024
- `after(req, res)`: Called after a successful response
11-
- `onError(req, err)`: Called when a request fails with any error
25+
- `onError(req, err)`: Called when core request execution fails (before plugin post-processing)
1226
- `onRetry(req, attempt, err, res)`: Called before each retry attempt
1327
- `onTimeout(req)`: Called when a request times out
1428
- `onAbort(req)`: Called when a request is aborted by the user
15-
- `onComplete(req, res, err)`: Called after every request, whether it succeeded or failed
29+
- `onComplete(req, res, err)`: Called when core request execution completes (last among core hooks)
1630

1731
### Basic Hooks Example
1832

@@ -256,10 +270,11 @@ const client = createClient({
256270
retries: 3,
257271
hooks: {
258272
onRetry: async (req, attempt, err, res) => {
259-
console.log(`Retry ${attempt - 1}/3 for ${req.url}`)
273+
// `attempt` is zero-based in onRetry (0 = first retry)
274+
console.log(`Retry ${attempt + 1}/3 for ${req.url}`)
260275

261276
// Add exponential backoff delay
262-
const delay = Math.min(1000 * Math.pow(2, attempt - 2), 10000)
277+
const delay = Math.min(1000 * Math.pow(2, attempt), 10000)
263278
console.log(`Waiting ${delay}ms before retry...`)
264279
await new Promise((resolve) => setTimeout(resolve, delay))
265280

@@ -276,7 +291,7 @@ const client = createClient({
276291

277292
### onCircuitOpen
278293

279-
Called when the circuit transitions to open after consecutive failures. Receives the request that triggered the open event.
294+
Called when the circuit opens after consecutive failures, and also when a request is blocked while the circuit is already open. Receives the request associated with that event.
280295

281296
Signature: `(req: Request) => void | Promise<void>`
282297

@@ -313,17 +328,20 @@ When a request is made, hooks execute in this order:
313328
3. **Request is sent**
314329
4. `transformResponse` - Modify the incoming response (if successful)
315330
5. `after` - Called after successful response
316-
6. `onComplete` - Always called last
331+
6. `onComplete` - Last among core hooks
317332

318333
If an error occurs or retry is needed:
319334

320-
1. `onError` - Called on any error
335+
1. `onError` - Called on core execution errors
321336
2. `onRetry` - Called before retry attempts
322337
3. `onTimeout` - Called on timeout errors
323338
4. `onAbort` - Called on abort errors
324-
5. Plugin `onCircuitOpen` callback - Called when circuit breaker transitions to open
339+
5. Plugin `onCircuitOpen` callback - Called when circuit opens, and on blocked requests while already open
325340
6. Plugin `onCircuitClose` callback - Called when circuit breaker transitions to closed
326-
7. `onComplete` - Always called last
341+
7. `onComplete` - Last among core hooks
342+
343+
After core hooks run, plugin lifecycle callbacks may still run (for example plugin `onSuccess`, `onError`, and `onFinally`).
344+
If a plugin throws after core completion, core `onError` is not re-fired and core `onComplete` may already have run with success arguments.
327345

328346
## Per-Request Hooks
329347

docs/migration.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,7 @@ const plugins = [circuitPlugin({ threshold: 5, reset: 30_000 })] as const
127127
const client = createClient({ plugins })
128128
```
129129

130-
## 6) Runtime Guard for Legacy Options
131-
132-
v5 throws a clear runtime error if removed options are still present in client options or per-request init.
133-
134-
## 7) Quick Upgrade Checklist
130+
## 6) Quick Upgrade Checklist
135131

136132
1. Replace default root import with named root import.
137133
2. Add plugin imports from:

0 commit comments

Comments
 (0)