@fetchkit/ffetch 3.4.1
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/LICENSE +21 -0
- package/README.md +176 -0
- package/dist/index.cjs +423 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +72 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.js +391 -0
- package/dist/index.js.map +1 -0
- package/dist/index.min.js +2 -0
- package/dist/index.min.js.map +1 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 gkoos
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+

|
|
2
|
+

|
|
3
|
+

|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
# @gkoos/ffetch
|
|
13
|
+
|
|
14
|
+
**A production-ready TypeScript-first drop-in replacement for native fetch, or any fetch-compatible implementation.**
|
|
15
|
+
|
|
16
|
+
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.
|
|
17
|
+
|
|
18
|
+
**Key Features:**
|
|
19
|
+
|
|
20
|
+
- **Timeouts** – per-request or global
|
|
21
|
+
- **Retries** – exponential backoff + jitter
|
|
22
|
+
- **Circuit breaker** – automatic failure protection
|
|
23
|
+
- **Hooks** – logging, auth, metrics, request/response transformation
|
|
24
|
+
- **Pending requests** – real-time monitoring of active requests
|
|
25
|
+
- **Per-request overrides** – customize behavior on a per-request basis
|
|
26
|
+
- **Universal** – Node.js, Browser, Cloudflare Workers, React Native
|
|
27
|
+
- **Zero runtime deps** – ships as dual ESM/CJS
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
### Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install @gkoos/ffetch
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Basic Usage
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import createClient from '@gkoos/ffetch'
|
|
41
|
+
|
|
42
|
+
// Create a client with timeout and retries
|
|
43
|
+
const api = createClient({
|
|
44
|
+
timeout: 5000,
|
|
45
|
+
retries: 3,
|
|
46
|
+
retryDelay: ({ attempt }) => 2 ** attempt * 100 + Math.random() * 100,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Make requests
|
|
50
|
+
const response = await api('https://api.example.com/users')
|
|
51
|
+
const data = await response.json()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Using a Custom fetchHandler (SSR, metaframeworks, or polyfills)
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// Example: SvelteKit, Next.js, Nuxt, or node-fetch
|
|
58
|
+
import createClient from '@gkoos/ffetch'
|
|
59
|
+
|
|
60
|
+
// Pass your framework's fetch implementation
|
|
61
|
+
const api = createClient({
|
|
62
|
+
fetchHandler: fetch, // SvelteKit/Next.js/Nuxt provide their own fetch
|
|
63
|
+
timeout: 5000,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// Or use node-fetch/undici in Node.js
|
|
67
|
+
import nodeFetch from 'node-fetch'
|
|
68
|
+
const apiNode = createClient({ fetchHandler: nodeFetch })
|
|
69
|
+
|
|
70
|
+
// All ffetch features work identically
|
|
71
|
+
const response = await api('/api/data')
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Advanced Example
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
// Production-ready client with error handling and monitoring
|
|
78
|
+
const client = createClient({
|
|
79
|
+
timeout: 10000,
|
|
80
|
+
retries: 2,
|
|
81
|
+
circuit: { threshold: 5, reset: 30000 },
|
|
82
|
+
fetchHandler: fetch, // Use custom fetch if needed
|
|
83
|
+
hooks: {
|
|
84
|
+
before: async (req) => console.log('→', req.url),
|
|
85
|
+
after: async (req, res) => console.log('←', res.status),
|
|
86
|
+
onError: async (req, err) => console.error('Error:', err.message),
|
|
87
|
+
onCircuitOpen: (req) => console.warn('Circuit opened due to:', req.url),
|
|
88
|
+
onCircuitClose: (req) => console.info('Circuit closed after:', req.url),
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const response = await client('/api/data')
|
|
94
|
+
|
|
95
|
+
// Check HTTP status manually (like native fetch)
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
console.log('HTTP error:', response.status)
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const data = await response.json()
|
|
102
|
+
console.log('Active requests:', client.pendingRequests.length)
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (err instanceof TimeoutError) {
|
|
105
|
+
console.log('Request timed out')
|
|
106
|
+
} else if (err instanceof RetryLimitError) {
|
|
107
|
+
console.log('Request failed after retries')
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Documentation
|
|
113
|
+
|
|
114
|
+
| Topic | Description |
|
|
115
|
+
| --------------------------------------------- | ------------------------------------------------------------------------- |
|
|
116
|
+
| **[Complete Documentation](./docs/index.md)** | **Start here** - Documentation index and overview |
|
|
117
|
+
| **[API Reference](./docs/api.md)** | Complete API documentation and configuration options |
|
|
118
|
+
| **[Advanced Features](./docs/advanced.md)** | Per-request overrides, pending requests, circuit breakers, custom errors |
|
|
119
|
+
| **[Hooks & Transformation](./docs/hooks.md)** | Lifecycle hooks, authentication, logging, request/response transformation |
|
|
120
|
+
| **[Usage Examples](./docs/examples.md)** | Real-world patterns: REST clients, GraphQL, file uploads, microservices |
|
|
121
|
+
| **[Compatibility](./docs/compatibility.md)** | Browser/Node.js support, polyfills, framework integration |
|
|
122
|
+
|
|
123
|
+
## Environment Requirements
|
|
124
|
+
|
|
125
|
+
`ffetch` requires modern AbortSignal APIs:
|
|
126
|
+
|
|
127
|
+
- **Node.js 20.6+** (for AbortSignal.any)
|
|
128
|
+
- **Modern browsers** (Chrome 117+, Firefox 117+, Safari 17+, Edge 117+)
|
|
129
|
+
|
|
130
|
+
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.
|
|
131
|
+
|
|
132
|
+
**Custom fetch support:**
|
|
133
|
+
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.
|
|
134
|
+
|
|
135
|
+
#### "AbortSignal.any is not a function"
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
Solution: Install a polyfill for AbortSignal.any
|
|
139
|
+
npm install abort-controller-x
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## CDN Usage
|
|
143
|
+
|
|
144
|
+
```html
|
|
145
|
+
<script type="module">
|
|
146
|
+
import createClient from 'https://unpkg.com/@gkoos/ffetch/dist/index.min.js'
|
|
147
|
+
|
|
148
|
+
const api = createClient({ timeout: 5000 })
|
|
149
|
+
const data = await api('/api/data').then((r) => r.json())
|
|
150
|
+
</script>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Fetch vs. Axios vs. `ffetch`
|
|
154
|
+
|
|
155
|
+
| Feature | Native Fetch | Axios | ffetch |
|
|
156
|
+
| -------------------- | ------------------------- | -------------------- | -------------------------------------------------------------------------------------- |
|
|
157
|
+
| Timeouts | ❌ Manual AbortController | ✅ Built-in | ✅ Built-in with fallbacks |
|
|
158
|
+
| Retries | ❌ Manual implementation | ❌ Manual or plugins | ✅ Smart exponential backoff |
|
|
159
|
+
| Circuit Breaker | ❌ Not available | ❌ Manual or plugins | ✅ Automatic failure protection |
|
|
160
|
+
| Request Monitoring | ❌ Manual tracking | ❌ Manual tracking | ✅ Built-in pending requests |
|
|
161
|
+
| Error Types | ❌ Generic errors | ⚠️ HTTP errors only | ✅ Specific error classes |
|
|
162
|
+
| TypeScript | ⚠️ Basic types | ⚠️ Basic types | ✅ Full type safety |
|
|
163
|
+
| Hooks/Middleware | ❌ Not available | ✅ Interceptors | ✅ Comprehensive lifecycle hooks |
|
|
164
|
+
| Bundle Size | ✅ Native (0kb) | ❌ ~13kb minified | ✅ ~3kb minified |
|
|
165
|
+
| Modern APIs | ✅ Web standards | ❌ XMLHttpRequest | ✅ Fetch + modern features |
|
|
166
|
+
| Custom Fetch Support | ❌ No (global only) | ❌ No | ✅ Yes (wrap any fetch-compatible implementation, including framework or custom fetch) |
|
|
167
|
+
|
|
168
|
+
## Contributing
|
|
169
|
+
|
|
170
|
+
- **Issues**: [GitHub Issues](https://github.com/gkoos/ffetch/issues)
|
|
171
|
+
- **Pull Requests**: [GitHub PRs](https://github.com/gkoos/ffetch/pulls)
|
|
172
|
+
- **Documentation**: Found in `./docs/` - PRs welcome!
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT © 2025 gkoos
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AbortError: () => AbortError,
|
|
24
|
+
CircuitOpenError: () => CircuitOpenError,
|
|
25
|
+
NetworkError: () => NetworkError,
|
|
26
|
+
RetryLimitError: () => RetryLimitError,
|
|
27
|
+
TimeoutError: () => TimeoutError,
|
|
28
|
+
createClient: () => createClient,
|
|
29
|
+
default: () => index_default
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(index_exports);
|
|
32
|
+
|
|
33
|
+
// src/retry.ts
|
|
34
|
+
var defaultDelay = (ctx) => {
|
|
35
|
+
const retryAfter = ctx.response?.headers.get("Retry-After");
|
|
36
|
+
if (retryAfter) {
|
|
37
|
+
const seconds = parseInt(retryAfter, 10);
|
|
38
|
+
if (!isNaN(seconds)) return seconds * 1e3;
|
|
39
|
+
const date = Date.parse(retryAfter);
|
|
40
|
+
if (!isNaN(date)) return Math.max(0, date - Date.now());
|
|
41
|
+
}
|
|
42
|
+
return 2 ** ctx.attempt * 200 + Math.random() * 100;
|
|
43
|
+
};
|
|
44
|
+
async function retry(fn, retries, delay, shouldRetry2 = () => true, request) {
|
|
45
|
+
let lastErr;
|
|
46
|
+
let lastRes;
|
|
47
|
+
for (let i = 0; i <= retries; i++) {
|
|
48
|
+
const ctx = {
|
|
49
|
+
attempt: i + 1,
|
|
50
|
+
request,
|
|
51
|
+
response: lastRes,
|
|
52
|
+
error: lastErr
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
lastRes = await fn();
|
|
56
|
+
ctx.response = lastRes;
|
|
57
|
+
ctx.error = void 0;
|
|
58
|
+
if (i < retries && shouldRetry2(ctx)) {
|
|
59
|
+
const wait = typeof delay === "function" ? delay(ctx) : delay;
|
|
60
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
return lastRes;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
lastErr = err;
|
|
66
|
+
ctx.error = err;
|
|
67
|
+
if (i === retries || !shouldRetry2(ctx)) throw err;
|
|
68
|
+
const wait = typeof delay === "function" ? delay(ctx) : delay;
|
|
69
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
throw lastErr;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/error.ts
|
|
76
|
+
var BaseError = class extends Error {
|
|
77
|
+
constructor(name, message, cause) {
|
|
78
|
+
super(message);
|
|
79
|
+
this.name = name;
|
|
80
|
+
if (cause !== void 0) this.cause = cause;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var TimeoutError = class extends BaseError {
|
|
84
|
+
constructor(message = "Request timed out", cause) {
|
|
85
|
+
super("TimeoutError", message, cause);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
var CircuitOpenError = class extends BaseError {
|
|
89
|
+
constructor(message = "Circuit is open", cause) {
|
|
90
|
+
super("CircuitOpenError", message, cause);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
var AbortError = class extends BaseError {
|
|
94
|
+
constructor(message = "Request was aborted", cause) {
|
|
95
|
+
super("AbortError", message, cause);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
var RetryLimitError = class extends BaseError {
|
|
99
|
+
constructor(message = "Retry limit reached", cause) {
|
|
100
|
+
super("RetryLimitError", message, cause);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
var NetworkError = class extends BaseError {
|
|
104
|
+
constructor(message = "Network error occurred", cause) {
|
|
105
|
+
super("NetworkError", message, cause);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// src/should-retry.ts
|
|
110
|
+
function shouldRetry(ctx) {
|
|
111
|
+
const { error, response } = ctx;
|
|
112
|
+
if (error instanceof AbortError || error instanceof CircuitOpenError || error instanceof TimeoutError)
|
|
113
|
+
return false;
|
|
114
|
+
if (!response) return true;
|
|
115
|
+
return response.status >= 500 || response.status === 429;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/circuit.ts
|
|
119
|
+
var CircuitBreaker = class {
|
|
120
|
+
constructor(threshold, resetTimeout) {
|
|
121
|
+
this.threshold = threshold;
|
|
122
|
+
this.resetTimeout = resetTimeout;
|
|
123
|
+
this.failures = 0;
|
|
124
|
+
this.nextAttempt = 0;
|
|
125
|
+
this.isOpen = false;
|
|
126
|
+
}
|
|
127
|
+
// Returns true if the circuit breaker is currently open (blocking requests).
|
|
128
|
+
get open() {
|
|
129
|
+
return this.isOpen;
|
|
130
|
+
}
|
|
131
|
+
// Call this after each request to record the result and update circuit state.
|
|
132
|
+
// Returns true if the result was counted as a failure.
|
|
133
|
+
recordResult(response, error, req) {
|
|
134
|
+
if (error && !(error instanceof RetryLimitError)) {
|
|
135
|
+
this.setLastOpenRequest(req);
|
|
136
|
+
this.onFailure();
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
if (response && (response.status >= 500 || response.status === 429)) {
|
|
140
|
+
this.setLastOpenRequest(req);
|
|
141
|
+
this.onFailure();
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
if (req) this.setLastSuccessRequest(req);
|
|
145
|
+
this.onSuccess();
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
setHooks(hooks) {
|
|
149
|
+
this.hooks = hooks;
|
|
150
|
+
}
|
|
151
|
+
setLastOpenRequest(req) {
|
|
152
|
+
this.lastOpenRequest = req;
|
|
153
|
+
}
|
|
154
|
+
setLastSuccessRequest(req) {
|
|
155
|
+
this.lastSuccessRequest = req;
|
|
156
|
+
}
|
|
157
|
+
async invoke(fn) {
|
|
158
|
+
if (Date.now() < this.nextAttempt)
|
|
159
|
+
throw new CircuitOpenError("Circuit is open");
|
|
160
|
+
try {
|
|
161
|
+
const res = await fn();
|
|
162
|
+
this.onSuccess();
|
|
163
|
+
return res;
|
|
164
|
+
} catch (err) {
|
|
165
|
+
this.onFailure();
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
onSuccess() {
|
|
170
|
+
const wasOpen = this.isOpen;
|
|
171
|
+
this.failures = 0;
|
|
172
|
+
if (wasOpen) {
|
|
173
|
+
this.isOpen = false;
|
|
174
|
+
if (this.hooks?.onCircuitClose && this.lastSuccessRequest) {
|
|
175
|
+
this.hooks.onCircuitClose(this.lastSuccessRequest);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
this.lastSuccessRequest = void 0;
|
|
179
|
+
}
|
|
180
|
+
onFailure() {
|
|
181
|
+
this.failures++;
|
|
182
|
+
if (this.failures >= this.threshold) {
|
|
183
|
+
this.nextAttempt = Date.now() + this.resetTimeout;
|
|
184
|
+
this.isOpen = true;
|
|
185
|
+
if (this.hooks?.onCircuitOpen && this.lastOpenRequest) {
|
|
186
|
+
this.hooks.onCircuitOpen(this.lastOpenRequest);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// src/client.ts
|
|
193
|
+
function createClient(opts = {}) {
|
|
194
|
+
const {
|
|
195
|
+
timeout: clientDefaultTimeout = 5e3,
|
|
196
|
+
retries: clientDefaultRetries = 0,
|
|
197
|
+
retryDelay: clientDefaultRetryDelay = defaultDelay,
|
|
198
|
+
shouldRetry: clientDefaultShouldRetry = shouldRetry,
|
|
199
|
+
hooks: clientDefaultHooks = {},
|
|
200
|
+
circuit: clientDefaultCircuit,
|
|
201
|
+
fetchHandler
|
|
202
|
+
} = opts;
|
|
203
|
+
const breaker = clientDefaultCircuit ? new CircuitBreaker(
|
|
204
|
+
clientDefaultCircuit.threshold,
|
|
205
|
+
clientDefaultCircuit.reset
|
|
206
|
+
) : null;
|
|
207
|
+
if (breaker && (clientDefaultHooks.onCircuitClose || clientDefaultHooks.onCircuitOpen)) {
|
|
208
|
+
breaker.setHooks({
|
|
209
|
+
onCircuitClose: clientDefaultHooks.onCircuitClose,
|
|
210
|
+
onCircuitOpen: clientDefaultHooks.onCircuitOpen
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
const pendingRequests = [];
|
|
214
|
+
function abortAll() {
|
|
215
|
+
for (const entry of pendingRequests) {
|
|
216
|
+
entry.controller?.abort();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const client = async (input, init = {}) => {
|
|
220
|
+
let request = new Request(input, init);
|
|
221
|
+
const effectiveHooks = { ...clientDefaultHooks, ...init.hooks || {} };
|
|
222
|
+
if (effectiveHooks.transformRequest) {
|
|
223
|
+
request = await effectiveHooks.transformRequest(request);
|
|
224
|
+
}
|
|
225
|
+
await effectiveHooks.before?.(request);
|
|
226
|
+
function createTimeoutSignal(timeout) {
|
|
227
|
+
if (typeof AbortSignal?.timeout === "function") {
|
|
228
|
+
return AbortSignal.timeout(timeout);
|
|
229
|
+
}
|
|
230
|
+
const controller2 = new AbortController();
|
|
231
|
+
const timeoutId = setTimeout(() => controller2.abort(), timeout);
|
|
232
|
+
controller2.signal.addEventListener(
|
|
233
|
+
"abort",
|
|
234
|
+
() => clearTimeout(timeoutId),
|
|
235
|
+
{ once: true }
|
|
236
|
+
);
|
|
237
|
+
return controller2.signal;
|
|
238
|
+
}
|
|
239
|
+
const effectiveTimeout = init.timeout ?? clientDefaultTimeout;
|
|
240
|
+
const userSignal = init.signal;
|
|
241
|
+
const transformedSignal = request.signal;
|
|
242
|
+
let timeoutSignal = void 0;
|
|
243
|
+
let combinedSignal = void 0;
|
|
244
|
+
let controller = void 0;
|
|
245
|
+
if (effectiveTimeout > 0) {
|
|
246
|
+
timeoutSignal = createTimeoutSignal(effectiveTimeout);
|
|
247
|
+
}
|
|
248
|
+
const signals = [];
|
|
249
|
+
if (userSignal) signals.push(userSignal);
|
|
250
|
+
if (transformedSignal && transformedSignal !== userSignal) {
|
|
251
|
+
signals.push(transformedSignal);
|
|
252
|
+
}
|
|
253
|
+
if (timeoutSignal) signals.push(timeoutSignal);
|
|
254
|
+
if (signals.length === 1) {
|
|
255
|
+
combinedSignal = signals[0];
|
|
256
|
+
controller = new AbortController();
|
|
257
|
+
} else {
|
|
258
|
+
if (typeof AbortSignal.any !== "function") {
|
|
259
|
+
throw new Error(
|
|
260
|
+
"AbortSignal.any is required for combining multiple signals. Please install a polyfill for environments that do not support it."
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
combinedSignal = AbortSignal.any(signals);
|
|
264
|
+
controller = new AbortController();
|
|
265
|
+
}
|
|
266
|
+
const retryWithHooks = async () => {
|
|
267
|
+
const effectiveRetries = init.retries ?? clientDefaultRetries;
|
|
268
|
+
const effectiveRetryDelay = typeof init.retryDelay !== "undefined" ? init.retryDelay : clientDefaultRetryDelay;
|
|
269
|
+
const effectiveShouldRetry = init.shouldRetry ?? clientDefaultShouldRetry;
|
|
270
|
+
let attempt = 0;
|
|
271
|
+
const shouldRetryWithHook = (ctx) => {
|
|
272
|
+
attempt = ctx.attempt;
|
|
273
|
+
const retrying = effectiveShouldRetry(ctx);
|
|
274
|
+
if (retrying && attempt <= effectiveRetries) {
|
|
275
|
+
effectiveHooks.onRetry?.(
|
|
276
|
+
request,
|
|
277
|
+
attempt - 1,
|
|
278
|
+
ctx.error,
|
|
279
|
+
ctx.response
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
return retrying;
|
|
283
|
+
};
|
|
284
|
+
function mapToCustomError(err) {
|
|
285
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
286
|
+
if (timeoutSignal?.aborted && (!userSignal || !userSignal.aborted)) {
|
|
287
|
+
return new TimeoutError("signal timed out", err);
|
|
288
|
+
} else {
|
|
289
|
+
return new AbortError("Request was aborted", err);
|
|
290
|
+
}
|
|
291
|
+
} else if (err instanceof TypeError && /NetworkError|network error|failed to fetch|lost connection|NetworkError when attempting to fetch resource/i.test(
|
|
292
|
+
err.message
|
|
293
|
+
)) {
|
|
294
|
+
return new NetworkError(err.message, err);
|
|
295
|
+
}
|
|
296
|
+
return err;
|
|
297
|
+
}
|
|
298
|
+
async function handleError(err) {
|
|
299
|
+
err = mapToCustomError(err);
|
|
300
|
+
if (userSignal?.aborted) {
|
|
301
|
+
const abortErr = new AbortError("Request was aborted by user");
|
|
302
|
+
await effectiveHooks.onAbort?.(request);
|
|
303
|
+
await effectiveHooks.onError?.(request, abortErr);
|
|
304
|
+
await effectiveHooks.onComplete?.(request, void 0, abortErr);
|
|
305
|
+
throw abortErr;
|
|
306
|
+
}
|
|
307
|
+
if (err instanceof TimeoutError || err instanceof NetworkError || err instanceof AbortError) {
|
|
308
|
+
if (err instanceof TimeoutError) {
|
|
309
|
+
await effectiveHooks.onTimeout?.(request);
|
|
310
|
+
}
|
|
311
|
+
if (err instanceof AbortError) {
|
|
312
|
+
await effectiveHooks.onAbort?.(request);
|
|
313
|
+
}
|
|
314
|
+
await effectiveHooks.onError?.(request, err);
|
|
315
|
+
await effectiveHooks.onComplete?.(request, void 0, err);
|
|
316
|
+
throw err;
|
|
317
|
+
}
|
|
318
|
+
const retryErr = new RetryLimitError(
|
|
319
|
+
typeof err === "object" && err && "message" in err && typeof err.message === "string" ? err.message : "Retry limit reached"
|
|
320
|
+
);
|
|
321
|
+
await effectiveHooks.onError?.(request, retryErr);
|
|
322
|
+
await effectiveHooks.onComplete?.(request, void 0, retryErr);
|
|
323
|
+
throw retryErr;
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
let res = await retry(
|
|
327
|
+
async () => {
|
|
328
|
+
if (typeof combinedSignal?.throwIfAborted === "function") {
|
|
329
|
+
combinedSignal.throwIfAborted();
|
|
330
|
+
} else if (combinedSignal?.aborted) {
|
|
331
|
+
throw new AbortError("Request was aborted");
|
|
332
|
+
}
|
|
333
|
+
const reqWithSignal = new Request(request, {
|
|
334
|
+
signal: combinedSignal
|
|
335
|
+
});
|
|
336
|
+
try {
|
|
337
|
+
const handler = fetchHandler ?? fetch;
|
|
338
|
+
const response = await handler(reqWithSignal);
|
|
339
|
+
if (breaker) {
|
|
340
|
+
if (breaker.recordResult(response, void 0, request)) {
|
|
341
|
+
throw new Error(`HTTP error: ${response.status}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return response;
|
|
345
|
+
} catch (err) {
|
|
346
|
+
if (breaker) breaker.recordResult(void 0, err, request);
|
|
347
|
+
throw mapToCustomError(err);
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
effectiveRetries,
|
|
351
|
+
effectiveRetryDelay,
|
|
352
|
+
shouldRetryWithHook,
|
|
353
|
+
request
|
|
354
|
+
);
|
|
355
|
+
if (effectiveHooks.transformResponse) {
|
|
356
|
+
res = await effectiveHooks.transformResponse(res, request);
|
|
357
|
+
}
|
|
358
|
+
await effectiveHooks.after?.(request, res);
|
|
359
|
+
await effectiveHooks.onComplete?.(request, res, void 0);
|
|
360
|
+
return res;
|
|
361
|
+
} catch (err) {
|
|
362
|
+
await handleError(err);
|
|
363
|
+
throw new Error("Unreachable: handleError should always throw");
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
const promise = breaker ? breaker.invoke(retryWithHooks).catch(async (err) => {
|
|
367
|
+
if (err instanceof CircuitOpenError) {
|
|
368
|
+
await effectiveHooks.onCircuitOpen?.(request);
|
|
369
|
+
await effectiveHooks.onError?.(request, err);
|
|
370
|
+
await effectiveHooks.onComplete?.(request, void 0, err);
|
|
371
|
+
} else {
|
|
372
|
+
await effectiveHooks.onError?.(request, err);
|
|
373
|
+
await effectiveHooks.onComplete?.(request, void 0, err);
|
|
374
|
+
}
|
|
375
|
+
throw err;
|
|
376
|
+
}) : retryWithHooks();
|
|
377
|
+
const entry = {
|
|
378
|
+
promise,
|
|
379
|
+
request,
|
|
380
|
+
controller
|
|
381
|
+
};
|
|
382
|
+
pendingRequests.push(entry);
|
|
383
|
+
return promise.finally(() => {
|
|
384
|
+
const index = pendingRequests.indexOf(entry);
|
|
385
|
+
if (index > -1) {
|
|
386
|
+
pendingRequests.splice(index, 1);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
};
|
|
390
|
+
Object.defineProperty(client, "pendingRequests", {
|
|
391
|
+
get() {
|
|
392
|
+
return pendingRequests;
|
|
393
|
+
},
|
|
394
|
+
enumerable: false,
|
|
395
|
+
configurable: false
|
|
396
|
+
});
|
|
397
|
+
Object.defineProperty(client, "abortAll", {
|
|
398
|
+
value: abortAll,
|
|
399
|
+
writable: false,
|
|
400
|
+
enumerable: false,
|
|
401
|
+
configurable: false
|
|
402
|
+
});
|
|
403
|
+
Object.defineProperty(client, "circuitOpen", {
|
|
404
|
+
get() {
|
|
405
|
+
return breaker ? breaker.open : false;
|
|
406
|
+
},
|
|
407
|
+
enumerable: true
|
|
408
|
+
});
|
|
409
|
+
return client;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/index.ts
|
|
413
|
+
var index_default = createClient;
|
|
414
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
415
|
+
0 && (module.exports = {
|
|
416
|
+
AbortError,
|
|
417
|
+
CircuitOpenError,
|
|
418
|
+
NetworkError,
|
|
419
|
+
RetryLimitError,
|
|
420
|
+
TimeoutError,
|
|
421
|
+
createClient
|
|
422
|
+
});
|
|
423
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/retry.ts","../src/error.ts","../src/should-retry.ts","../src/circuit.ts","../src/client.ts"],"sourcesContent":["export type { FFetch, FFetchOptions } from './types'\nexport type { Hooks } from './hooks'\n\nimport { createClient } from './client'\nexport { createClient } from './client'\n\nexport {\n TimeoutError,\n CircuitOpenError,\n AbortError,\n RetryLimitError,\n NetworkError,\n} from './error'\n\nexport default createClient\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\nexport async function retry(\n fn: () => Promise<Response>,\n retries: number,\n delay: RetryDelay,\n shouldRetry: (ctx: RetryContext) => boolean = () => true,\n request: Request\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 ctx.response = lastRes\n ctx.error = undefined\n if (i < retries && shouldRetry(ctx)) {\n const wait = typeof delay === 'function' ? delay(ctx) : delay\n await new Promise((r) => setTimeout(r, wait))\n continue\n }\n return lastRes\n } catch (err) {\n lastErr = err\n ctx.error = err\n if (i === retries || !shouldRetry(ctx)) throw err\n const wait = typeof delay === 'function' ? delay(ctx) : delay\n await new Promise((r) => setTimeout(r, wait))\n }\n }\n throw lastErr\n}\n","// 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 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","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 { CircuitOpenError, RetryLimitError } from './error.js'\n\nexport class CircuitBreaker {\n private failures = 0\n private nextAttempt = 0\n private isOpen = false\n\n // Returns true if the circuit breaker is currently open (blocking requests).\n get open(): boolean {\n return this.isOpen\n }\n private hooks?: {\n onCircuitOpen?: (req: Request) => void | Promise<void>\n onCircuitClose?: (req: Request) => void | Promise<void>\n }\n private lastSuccessRequest?: Request\n private lastOpenRequest?: Request\n\n constructor(\n private threshold: number,\n private resetTimeout: number\n ) {}\n\n // Call this after each request to record the result and update circuit state.\n // Returns true if the result was counted as a failure.\n recordResult(response?: Response, error?: unknown, req?: Request): boolean {\n // Count thrown errors (network, abort, timeout) as failures\n if (error && !(error instanceof RetryLimitError)) {\n this.setLastOpenRequest(req!)\n this.onFailure()\n return true\n }\n // Count HTTP 5xx and 429 responses as failures\n if (response && (response.status >= 500 || response.status === 429)) {\n this.setLastOpenRequest(req!)\n this.onFailure()\n return true\n }\n // Otherwise, count as success\n if (req) this.setLastSuccessRequest(req)\n this.onSuccess()\n return false\n }\n\n setHooks(hooks: {\n onCircuitOpen?: (req: Request) => void | Promise<void>\n onCircuitClose?: (req: Request) => void | Promise<void>\n }) {\n this.hooks = hooks\n }\n setLastOpenRequest(req: Request) {\n this.lastOpenRequest = req\n }\n\n setLastSuccessRequest(req: Request) {\n this.lastSuccessRequest = req\n }\n\n async invoke<T>(fn: () => Promise<T>): Promise<T> {\n if (Date.now() < this.nextAttempt)\n throw new CircuitOpenError('Circuit is open')\n try {\n const res = await fn()\n this.onSuccess()\n return res\n } catch (err) {\n this.onFailure()\n throw err\n }\n }\n\n private onSuccess() {\n const wasOpen = this.isOpen\n this.failures = 0\n if (wasOpen) {\n this.isOpen = false\n if (this.hooks?.onCircuitClose && this.lastSuccessRequest) {\n this.hooks.onCircuitClose(this.lastSuccessRequest)\n }\n }\n this.lastSuccessRequest = undefined\n }\n\n private onFailure() {\n this.failures++\n if (this.failures >= this.threshold) {\n this.nextAttempt = Date.now() + this.resetTimeout\n this.isOpen = true\n if (this.hooks?.onCircuitOpen && this.lastOpenRequest) {\n this.hooks.onCircuitOpen(this.lastOpenRequest)\n }\n }\n }\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 { CircuitBreaker } from './circuit.js'\nimport {\n TimeoutError,\n CircuitOpenError,\n AbortError,\n RetryLimitError,\n NetworkError,\n} from './error.js'\n\nexport function createClient(opts: FFetchOptions = {}): FFetch {\n const {\n timeout: clientDefaultTimeout = 5_000,\n retries: clientDefaultRetries = 0,\n retryDelay: clientDefaultRetryDelay = defaultDelay,\n shouldRetry: clientDefaultShouldRetry = defaultShouldRetry,\n hooks: clientDefaultHooks = {},\n circuit: clientDefaultCircuit,\n fetchHandler,\n } = opts\n\n const breaker = clientDefaultCircuit\n ? new CircuitBreaker(\n clientDefaultCircuit.threshold,\n clientDefaultCircuit.reset\n )\n : null\n\n if (\n breaker &&\n (clientDefaultHooks.onCircuitClose || clientDefaultHooks.onCircuitOpen)\n ) {\n breaker.setHooks({\n onCircuitClose: clientDefaultHooks.onCircuitClose,\n onCircuitOpen: clientDefaultHooks.onCircuitOpen,\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 = async (\n input: RequestInfo | URL,\n init: FFetchRequestInit = {}\n ) => {\n // No longer require AbortSignal.timeout - we'll implement it manually if needed\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 // 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\n // Manual implementation for older environments\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), timeout)\n\n // Clean up timeout if signal is aborted early by other means\n controller.signal.addEventListener(\n 'abort',\n () => clearTimeout(timeoutId),\n { once: true }\n )\n\n return controller.signal\n }\n\n // AbortSignal.timeout/any logic ---\n const effectiveTimeout = init.timeout ?? clientDefaultTimeout\n const userSignal = init.signal\n const transformedSignal = request.signal // Extract signal from transformed request\n let timeoutSignal: AbortSignal | undefined = undefined\n let combinedSignal: AbortSignal | undefined = undefined\n let controller: AbortController | undefined = undefined\n\n if (effectiveTimeout > 0) {\n timeoutSignal = createTimeoutSignal(effectiveTimeout)\n }\n\n // Collect all signals that need to be combined\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\n // Use AbortSignal.any for signal combination. Requires native support or a polyfill.\n // If not available, instruct users to install a polyfill for environments lacking AbortSignal.any.\n // there are always 1 or more signals\n if (signals.length === 1) {\n combinedSignal = signals[0]\n controller = new AbortController()\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 controller = new AbortController()\n }\n const retryWithHooks = async () => {\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 // Wrap shouldRetry to call onRetry hook\n let attempt = 0\n const shouldRetryWithHook = (ctx: import('./types').RetryContext) => {\n attempt = ctx.attempt\n const retrying = effectiveShouldRetry(ctx)\n if (retrying && attempt <= effectiveRetries) {\n effectiveHooks.onRetry?.(\n request,\n attempt - 1,\n ctx.error,\n ctx.response\n )\n }\n return retrying\n }\n\n function mapToCustomError(err: unknown): unknown {\n if (err instanceof DOMException && err.name === 'AbortError') {\n if (timeoutSignal?.aborted && (!userSignal || !userSignal.aborted)) {\n return new TimeoutError('signal timed out', err)\n } else {\n return new AbortError('Request was aborted', err)\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 return new NetworkError(err.message, err)\n }\n return err\n }\n\n async function handleError(err: unknown): Promise<never> {\n err = mapToCustomError(err)\n // If user aborted, always throw AbortError, not RetryLimitError\n if (userSignal?.aborted) {\n const abortErr = new AbortError('Request was aborted by user')\n await effectiveHooks.onAbort?.(request)\n await effectiveHooks.onError?.(request, abortErr)\n await effectiveHooks.onComplete?.(request, undefined, abortErr)\n throw abortErr\n }\n // If the error is a custom error, re-throw it directly (do not wrap)\n if (\n err instanceof TimeoutError ||\n err instanceof NetworkError ||\n err instanceof AbortError\n ) {\n if (err instanceof TimeoutError) {\n await effectiveHooks.onTimeout?.(request)\n }\n if (err instanceof AbortError) {\n await effectiveHooks.onAbort?.(request)\n }\n await effectiveHooks.onError?.(request, err)\n await effectiveHooks.onComplete?.(request, undefined, err)\n throw err\n }\n // Otherwise, throw RetryLimitError after all retries are exhausted\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 )\n await effectiveHooks.onError?.(request, retryErr)\n await effectiveHooks.onComplete?.(request, undefined, retryErr)\n throw retryErr\n }\n\n try {\n let res = await retry(\n async () => {\n // Use AbortSignal.throwIfAborted() before starting fetch\n if (typeof combinedSignal?.throwIfAborted === 'function') {\n combinedSignal.throwIfAborted()\n } else if (combinedSignal?.aborted) {\n throw new AbortError('Request was aborted')\n }\n const reqWithSignal = new Request(request, {\n signal: combinedSignal,\n })\n try {\n const handler = fetchHandler ?? fetch\n const response = await handler(reqWithSignal)\n // Circuit breaker: record result\n if (breaker) {\n if (breaker.recordResult(response, undefined, request)) {\n throw new Error(`HTTP error: ${response.status}`)\n }\n }\n return response\n } catch (err) {\n if (breaker) breaker.recordResult(undefined, err, request)\n throw mapToCustomError(err)\n }\n },\n effectiveRetries,\n effectiveRetryDelay,\n shouldRetryWithHook,\n request\n )\n if (effectiveHooks.transformResponse) {\n res = await effectiveHooks.transformResponse(res, request)\n }\n await effectiveHooks.after?.(request, res)\n await effectiveHooks.onComplete?.(request, res, undefined)\n return res\n } catch (err: unknown) {\n await handleError(err)\n throw new Error('Unreachable: handleError should always throw')\n }\n }\n\n const promise = breaker\n ? breaker.invoke(retryWithHooks).catch(async (err: unknown) => {\n if (err instanceof CircuitOpenError) {\n await effectiveHooks.onCircuitOpen?.(request)\n await effectiveHooks.onError?.(request, err)\n await effectiveHooks.onComplete?.(request, undefined, err)\n } else {\n await effectiveHooks.onError?.(request, err)\n await effectiveHooks.onComplete?.(request, undefined, err)\n }\n throw err\n })\n : retryWithHooks()\n\n const entry: PendingRequest = {\n promise,\n request,\n controller,\n }\n pendingRequests.push(entry)\n\n return promise.finally(() => {\n const index = pendingRequests.indexOf(entry)\n if (index > -1) {\n pendingRequests.splice(index, 1)\n }\n })\n }\n\n // Add pendingRequests property to the client function (read-only)\n Object.defineProperty(client, 'pendingRequests', {\n get() {\n return pendingRequests\n },\n enumerable: false,\n configurable: false,\n })\n\n // Add abortAll method to the client function (read-only)\n Object.defineProperty(client, 'abortAll', {\n value: abortAll,\n writable: false,\n enumerable: false,\n configurable: false,\n })\n\n // Expose circuit breaker open state\n Object.defineProperty(client, 'circuitOpen', {\n get() {\n return breaker ? breaker.open : false\n },\n enumerable: true,\n })\n\n return client as FFetch\n}\n"],"mappings":"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","names":["shouldRetry","controller"]}
|