@alwatr/fetch 6.0.16 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +102 -0
- package/README.md +147 -32
- package/dist/error.d.ts +37 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/main.cjs +3 -3
- package/dist/main.cjs.map +3 -3
- package/dist/main.d.ts +23 -22
- package/dist/main.d.ts.map +1 -1
- package/dist/main.mjs +3 -3
- package/dist/main.mjs.map +3 -3
- package/dist/type.d.ts +16 -0
- package/dist/type.d.ts.map +1 -1
- package/package.json +10 -8
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,108 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [7.0.0](https://github.com/Alwatr/nanolib/compare/@alwatr/fetch@6.0.17...@alwatr/fetch@7.0.0) (2025-11-06)
|
|
7
|
+
|
|
8
|
+
### ⚠ BREAKING CHANGES
|
|
9
|
+
|
|
10
|
+
* The `fetch` function no longer throws exceptions. Instead, it returns a **tuple** following the Go-style error handling pattern:
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
// Old behavior (v1.x)
|
|
14
|
+
type FetchResponse = Promise<Response>;
|
|
15
|
+
|
|
16
|
+
// New behavior (v2.x)
|
|
17
|
+
type FetchResponse = Promise<[Response, null] | [null, Error | FetchError]>;
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Why This Change?
|
|
21
|
+
|
|
22
|
+
1. **Explicit Error Handling**: Forces developers to handle errors at the call site
|
|
23
|
+
2. **Type Safety**: TypeScript can track whether you've handled errors
|
|
24
|
+
3. **No Try-Catch Boilerplate**: Cleaner, more readable code
|
|
25
|
+
4. **Better Error Context**: `FetchError` provides detailed error reasons and response data
|
|
26
|
+
5. **Consistent Patterns**: Aligns with modern error handling practices (Go, Rust Result types)
|
|
27
|
+
|
|
28
|
+
### Migration Guide
|
|
29
|
+
|
|
30
|
+
#### Before (v1.x)
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import {fetch} from '@alwatr/fetch';
|
|
34
|
+
|
|
35
|
+
async function getUser(id: string) {
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetch(`/api/users/${id}`);
|
|
38
|
+
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return await response.json();
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.error('Failed to fetch user:', error);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
#### After (v2.x)
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import {fetch, FetchError} from '@alwatr/fetch';
|
|
56
|
+
|
|
57
|
+
async function getUser(id: string) {
|
|
58
|
+
const [response, error] = await fetch(`/api/users/${id}`);
|
|
59
|
+
|
|
60
|
+
if (error) {
|
|
61
|
+
console.error('Failed to fetch user:', error.message, error.response);
|
|
62
|
+
return null; // or throw, or return a default value
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// response is guaranteed to be ok here
|
|
66
|
+
return await response.json();
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
* enhance error handling in README with Go-style tuple pattern and FetchError examples ([e1091ec](https://github.com/Alwatr/nanolib/commit/e1091eca2c27cf3aa03e046fed3ccfad6ce704ed))
|
|
71
|
+
|
|
72
|
+
### ✨ Features
|
|
73
|
+
|
|
74
|
+
* add custom FetchError class for enhanced error handling in fetch requests ([31891de](https://github.com/Alwatr/nanolib/commit/31891de09437ddb86fd2101124120bf78a9552eb))
|
|
75
|
+
* enhance FetchError handling with specific reasons for fetch failures ([cc6569d](https://github.com/Alwatr/nanolib/commit/cc6569de16c27f2adaecefe3bef2c76ead29ffb8))
|
|
76
|
+
* enhance FetchResponse type to include FetchError for improved error handling ([dd6a0ff](https://github.com/Alwatr/nanolib/commit/dd6a0ff31ddbcd6ccdfd6f65eccbbe83b9cce237))
|
|
77
|
+
|
|
78
|
+
### 🐛 Bug Fixes
|
|
79
|
+
|
|
80
|
+
* add 'cache_not_found' reason to FetchErrorReason type for improved error categorization ([14dddd5](https://github.com/Alwatr/nanolib/commit/14dddd5750140f60ed4305d21226eb348795c0a3))
|
|
81
|
+
* add @alwatr/has-own dependency and update tsconfig references ([1bb1c71](https://github.com/Alwatr/nanolib/commit/1bb1c71bb8e7f6c2ffb0d6a563893e37183ec54b))
|
|
82
|
+
* add missing type import from @alwatr/type-helper ([2326335](https://github.com/Alwatr/nanolib/commit/23263352c2698738c5a43a5deebdf1744268e8ce))
|
|
83
|
+
* export error handling types from error.js ([bb88521](https://github.com/Alwatr/nanolib/commit/bb8852197cf0878f3ca62b14d3bd046a031e52a1))
|
|
84
|
+
* improve error handling in fetch function to parse response body as JSON or fallback to text ([8e02ba8](https://github.com/Alwatr/nanolib/commit/8e02ba8b4733005e52095dc9833e1e36d1f3e94a))
|
|
85
|
+
* refine error handling for fetch timeout and abort scenarios ([b5ac722](https://github.com/Alwatr/nanolib/commit/b5ac7229d713897f4d39d0c406dd3839792de680))
|
|
86
|
+
* replace Object.hasOwn with hasOwn import and enhance FetchError handling for better error reporting ([c320420](https://github.com/Alwatr/nanolib/commit/c320420689543aab1eebd46fe7dd601bda281002))
|
|
87
|
+
* set default options for fetch function ([7bda786](https://github.com/Alwatr/nanolib/commit/7bda786a8754d876e49d42ea1e5e7379ad70170d))
|
|
88
|
+
* support nodejs ([fb6d993](https://github.com/Alwatr/nanolib/commit/fb6d993fe6af56a468c73fa31a960aa601279b75))
|
|
89
|
+
* timeout abort issue ([bb3845d](https://github.com/Alwatr/nanolib/commit/bb3845d2b4cec705a8021f5c65de658fefc51e21))
|
|
90
|
+
* update error handling in README to reference FetchError consistently ([1f6e240](https://github.com/Alwatr/nanolib/commit/1f6e240c946a07b7ce9c4489a509597fec8705f9))
|
|
91
|
+
* update fetch function to return a tuple and add options processing ([d05bfb5](https://github.com/Alwatr/nanolib/commit/d05bfb59260be5eae5aeab7bd816aa2f613dd643))
|
|
92
|
+
* update fetch function to return FetchResponse and handle FetchError for improved error reporting ([ddf47e0](https://github.com/Alwatr/nanolib/commit/ddf47e07510bb0cd38fa75c8921a3d64ed370afc))
|
|
93
|
+
* update FetchError data type to ensure consistent error handling ([954b79a](https://github.com/Alwatr/nanolib/commit/954b79a7ba3954565c7d09db6b188b79f1fd8fa2))
|
|
94
|
+
* update FetchResponse type to ensure consistent error handling ([8da0b3a](https://github.com/Alwatr/nanolib/commit/8da0b3a8ac2801494ffa214a99792215a403b16e))
|
|
95
|
+
|
|
96
|
+
### 🧹 Miscellaneous Chores
|
|
97
|
+
|
|
98
|
+
* reorder jest dependency in package.json ([a098ecf](https://github.com/Alwatr/nanolib/commit/a098ecf0489596104908627c759c8dcb092d2424))
|
|
99
|
+
|
|
100
|
+
### 🔗 Dependencies update
|
|
101
|
+
|
|
102
|
+
* add @jest/globals dependency and remove types from tsconfig ([47ee79a](https://github.com/Alwatr/nanolib/commit/47ee79a234a026ce28ab5671f84f72aea61d8508))
|
|
103
|
+
|
|
104
|
+
## [6.0.17](https://github.com/Alwatr/nanolib/compare/@alwatr/fetch@6.0.16...@alwatr/fetch@6.0.17) (2025-11-04)
|
|
105
|
+
|
|
106
|
+
**Note:** Version bump only for package @alwatr/fetch
|
|
107
|
+
|
|
6
108
|
## [6.0.16](https://github.com/Alwatr/nanolib/compare/@alwatr/fetch@6.0.15...@alwatr/fetch@6.0.16) (2025-10-06)
|
|
7
109
|
|
|
8
110
|
### 🔗 Dependencies update
|
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ It's designed to be a drop-in replacement for the standard `fetch` to instantly
|
|
|
8
8
|
|
|
9
9
|
## Key Features
|
|
10
10
|
|
|
11
|
+
- **Go-Style Error Handling**: Returns a tuple `[Response, null]` on success or `[null, FetchError]` on failure—no exceptions thrown.
|
|
11
12
|
- **Retry Pattern**: Automatically retries failed requests on timeouts or server errors (5xx).
|
|
12
13
|
- **Request Timeout**: Aborts requests that take too long to complete.
|
|
13
14
|
- **Duplicate Handling**: Prevents sending identical parallel requests, returning a single response for all callers.
|
|
@@ -32,34 +33,101 @@ pnpm add @alwatr/fetch
|
|
|
32
33
|
|
|
33
34
|
## Quick Start
|
|
34
35
|
|
|
35
|
-
Import the `fetch` function and use it
|
|
36
|
+
Import the `fetch` function and use it with tuple destructuring for elegant error handling. The function returns `[Response, null]` on success or `[null, FetchError]` on failure—no exceptions are thrown.
|
|
36
37
|
|
|
37
38
|
```typescript
|
|
38
39
|
import {fetch} from '@alwatr/fetch';
|
|
39
40
|
|
|
40
41
|
async function fetchProducts() {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const data = await response.json();
|
|
54
|
-
console.log('Products:', data);
|
|
55
|
-
} catch (error) {
|
|
56
|
-
console.error('Failed to fetch products:', error);
|
|
42
|
+
console.log('Fetching product list...');
|
|
43
|
+
|
|
44
|
+
const [response, error] = await fetch('/api/products', {
|
|
45
|
+
queryParams: {limit: 10, category: 'electronics'},
|
|
46
|
+
cacheStrategy: 'stale_while_revalidate',
|
|
47
|
+
timeout: '5s',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (error) {
|
|
51
|
+
console.error('Failed to fetch products:', error.message);
|
|
52
|
+
console.error('Error reason:', error.reason);
|
|
53
|
+
return;
|
|
57
54
|
}
|
|
55
|
+
|
|
56
|
+
// At this point, response is guaranteed to be valid and ok
|
|
57
|
+
const data = await response.json();
|
|
58
|
+
console.log('Products:', data);
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
fetchProducts();
|
|
61
62
|
```
|
|
62
63
|
|
|
64
|
+
## Error Handling
|
|
65
|
+
|
|
66
|
+
`@alwatr/fetch` uses a **Go-style tuple return pattern** instead of throwing exceptions. This provides explicit, type-safe error handling.
|
|
67
|
+
|
|
68
|
+
### Return Type
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
type FetchResponse = Promise<[Response, null] | [null, FetchError]>;
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- **Success**: `[Response, null]` - The response is guaranteed to have `response.ok === true`
|
|
75
|
+
- **Failure**: `[null, FetchError]` - Contains detailed information about what went wrong
|
|
76
|
+
|
|
77
|
+
### FetchError Class
|
|
78
|
+
|
|
79
|
+
All errors are returned as `FetchError` instances, which provide rich context about the failure:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
class FetchError extends Error {
|
|
83
|
+
reason: FetchErrorReason; // Specific error reason
|
|
84
|
+
response?: Response; // The HTTP response (if available)
|
|
85
|
+
data?: unknown; // Parsed response body (if available)
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Error Reasons
|
|
90
|
+
|
|
91
|
+
The `reason` property indicates why the request failed:
|
|
92
|
+
|
|
93
|
+
- `'http_error'`: HTTP error status (e.g., 404, 500)
|
|
94
|
+
- `'timeout'`: Request exceeded the timeout duration
|
|
95
|
+
- `'cache_not_found'`: Resource not found in cache (when using `cache_only`)
|
|
96
|
+
- `'network_error'`: Network-level error (e.g., DNS failure, connection refused)
|
|
97
|
+
- `'aborted'`: Request was aborted via AbortSignal
|
|
98
|
+
- `'unknown_error'`: Unspecified error
|
|
99
|
+
|
|
100
|
+
### Error Handling Example
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
const [response, error] = await fetch('/api/user/profile', {
|
|
104
|
+
bearerToken: 'jwt-token',
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (error) {
|
|
108
|
+
switch (error.reason) {
|
|
109
|
+
case 'http_error':
|
|
110
|
+
console.error(`HTTP ${error.response?.status}:`, error.data);
|
|
111
|
+
break;
|
|
112
|
+
case 'timeout':
|
|
113
|
+
console.error('Request timed out. Please try again.');
|
|
114
|
+
break;
|
|
115
|
+
case 'network_error':
|
|
116
|
+
console.error('Network error. Check your connection.');
|
|
117
|
+
break;
|
|
118
|
+
case 'cache_not_found':
|
|
119
|
+
console.error('Data not available offline.');
|
|
120
|
+
break;
|
|
121
|
+
default:
|
|
122
|
+
console.error('Request failed:', error.message);
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Safe to use response here
|
|
128
|
+
const userData = await response.json();
|
|
129
|
+
```
|
|
130
|
+
|
|
63
131
|
## API and Options
|
|
64
132
|
|
|
65
133
|
The `fetch` function takes a `url` string and an `options` object. The options object extends the standard `RequestInit` and adds several custom options for enhanced control.
|
|
@@ -91,14 +159,17 @@ The `fetch` function takes a `url` string and an `options` object. The options o
|
|
|
91
159
|
The `queryParams` option simplifies adding search parameters to your request URL.
|
|
92
160
|
|
|
93
161
|
```typescript
|
|
94
|
-
// This will make a GET request to:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
queryParams: {
|
|
98
|
-
page: 2,
|
|
99
|
-
sort: 'asc',
|
|
100
|
-
},
|
|
162
|
+
// This will make a GET request to: /api/users?page=2&sort=asc
|
|
163
|
+
const [response, error] = await fetch('/api/users', {
|
|
164
|
+
queryParams: {page: 2, sort: 'asc'},
|
|
101
165
|
});
|
|
166
|
+
|
|
167
|
+
if (error) {
|
|
168
|
+
console.error('Failed to fetch users:', error.message);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const users = await response.json();
|
|
102
173
|
```
|
|
103
174
|
|
|
104
175
|
### JSON Body
|
|
@@ -107,23 +178,38 @@ Use `bodyJson` to send a JavaScript object as a JSON payload. The `Content-Type`
|
|
|
107
178
|
|
|
108
179
|
```typescript
|
|
109
180
|
// This will make a POST request to /api/orders with a JSON body
|
|
110
|
-
const response = await fetch('/api/orders', {
|
|
181
|
+
const [response, error] = await fetch('/api/orders', {
|
|
111
182
|
method: 'POST',
|
|
112
183
|
bodyJson: {
|
|
113
184
|
productId: 'xyz-123',
|
|
114
185
|
quantity: 2,
|
|
115
186
|
},
|
|
116
187
|
});
|
|
188
|
+
|
|
189
|
+
if (error) {
|
|
190
|
+
console.error('Failed to create order:', error.message);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const order = await response.json();
|
|
195
|
+
console.log('Order created:', order);
|
|
117
196
|
```
|
|
118
197
|
|
|
119
198
|
### Timeout
|
|
120
199
|
|
|
121
|
-
Set a timeout for your requests. If the request takes longer than the specified duration, it will be aborted
|
|
200
|
+
Set a timeout for your requests. If the request takes longer than the specified duration, it will be aborted and return a `FetchError` with `reason: 'timeout'`.
|
|
122
201
|
|
|
123
202
|
```typescript
|
|
124
|
-
await fetch('/api/slow-endpoint', {
|
|
203
|
+
const [response, error] = await fetch('/api/slow-endpoint', {
|
|
125
204
|
timeout: '2.5s', // You can use duration strings
|
|
126
205
|
});
|
|
206
|
+
|
|
207
|
+
if (error) {
|
|
208
|
+
if (error.reason === 'timeout') {
|
|
209
|
+
console.error('Request timed out after 2.5 seconds');
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
127
213
|
```
|
|
128
214
|
|
|
129
215
|
### Retry Pattern
|
|
@@ -132,10 +218,17 @@ The fetch operation will automatically retry on server errors (5xx status codes)
|
|
|
132
218
|
|
|
133
219
|
```typescript
|
|
134
220
|
// Retry up to 5 times, with a 2-second delay between each attempt
|
|
135
|
-
await fetch('/api/flaky-service', {
|
|
221
|
+
const [response, error] = await fetch('/api/flaky-service', {
|
|
136
222
|
retry: 5,
|
|
137
223
|
retryDelay: '2s',
|
|
138
224
|
});
|
|
225
|
+
|
|
226
|
+
if (error) {
|
|
227
|
+
console.error('Request failed after 5 retries:', error.message);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const data = await response.json();
|
|
139
232
|
```
|
|
140
233
|
|
|
141
234
|
### Duplicate Request Handling
|
|
@@ -150,10 +243,14 @@ The `removeDuplicate` option prevents multiple identical requests from being sen
|
|
|
150
243
|
```typescript
|
|
151
244
|
// Both calls will result in only ONE network request.
|
|
152
245
|
// The second call will receive the response from the first.
|
|
153
|
-
const
|
|
246
|
+
const results = await Promise.all([
|
|
154
247
|
fetch('/api/data', {removeDuplicate: 'until_load'}),
|
|
155
248
|
fetch('/api/data', {removeDuplicate: 'until_load'}),
|
|
156
249
|
]);
|
|
250
|
+
|
|
251
|
+
// Both results will have the same response or error
|
|
252
|
+
const [response1, error1] = results[0];
|
|
253
|
+
const [response2, error2] = results[1];
|
|
157
254
|
```
|
|
158
255
|
|
|
159
256
|
### Cache Strategies
|
|
@@ -163,17 +260,26 @@ Leverage the browser's Cache API with `cacheStrategy`.
|
|
|
163
260
|
- `'network_only'` (default): Standard fetch behavior; no caching.
|
|
164
261
|
- `'cache_first'`: Serves from cache if available. Otherwise, fetches from the network and caches the result.
|
|
165
262
|
- `'network_first'`: Fetches from the network first. If the network fails, it falls back to the cache.
|
|
263
|
+
- `'cache_only'`: Only serves from cache; returns an error if not found.
|
|
264
|
+
- `'update_cache'`: Fetches from network and updates the cache.
|
|
166
265
|
- `'stale_while_revalidate'`: The fastest strategy. It serves stale content from the cache immediately while sending a network request in the background to update the cache for the next time.
|
|
167
266
|
|
|
168
267
|
```typescript
|
|
169
268
|
// Serve news from cache instantly, but update it in the background for the next visit.
|
|
170
|
-
const response = await fetch('/api/news', {
|
|
269
|
+
const [response, error] = await fetch('/api/news', {
|
|
171
270
|
cacheStrategy: 'stale_while_revalidate',
|
|
172
271
|
revalidateCallback: (freshResponse) => {
|
|
173
272
|
console.log('Cache updated with fresh data!');
|
|
174
273
|
// You can use freshResponse to update the UI if needed
|
|
175
274
|
},
|
|
176
275
|
});
|
|
276
|
+
|
|
277
|
+
if (error) {
|
|
278
|
+
console.error('Failed to load news:', error.message);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const news = await response.json();
|
|
177
283
|
```
|
|
178
284
|
|
|
179
285
|
### Authentication
|
|
@@ -182,12 +288,21 @@ Easily add authentication headers with `bearerToken` or the `alwatrAuth` scheme.
|
|
|
182
288
|
|
|
183
289
|
```typescript
|
|
184
290
|
// Using a Bearer Token
|
|
185
|
-
await fetch('/api/secure/data', {
|
|
291
|
+
const [response, error] = await fetch('/api/secure/data', {
|
|
186
292
|
bearerToken: 'your-jwt-token-here',
|
|
187
293
|
});
|
|
188
294
|
|
|
295
|
+
if (error) {
|
|
296
|
+
if (error.response?.status === 401) {
|
|
297
|
+
console.error('Authentication failed. Please log in again.');
|
|
298
|
+
}
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const data = await response.json();
|
|
303
|
+
|
|
189
304
|
// Using Alwatr's authentication scheme
|
|
190
|
-
await fetch('/api/secure/data', {
|
|
305
|
+
const [response2, error2] = await fetch('/api/secure/data', {
|
|
191
306
|
alwatrAuth: {
|
|
192
307
|
userId: 'user-id',
|
|
193
308
|
userToken: 'user-auth-token',
|
package/dist/error.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { FetchErrorReason } from "./type.js";
|
|
2
|
+
/**
|
|
3
|
+
* Custom error class for fetch-related failures.
|
|
4
|
+
*
|
|
5
|
+
* This error is thrown when a fetch request fails, either due to a network issue
|
|
6
|
+
* or an HTTP error status (i.e., `response.ok` is `false`). It enriches the
|
|
7
|
+
* standard `Error` object with the `response` and the parsed `data` from the
|
|
8
|
+
* response body, allowing for more detailed error handling.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const [response, error] = await fetch('/api/endpoint');
|
|
13
|
+
* if (error) {
|
|
14
|
+
* console.error(`Request failed with status ${error.response?.status}`);
|
|
15
|
+
* console.error('Server response:', error.data);
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare class FetchError extends Error {
|
|
20
|
+
/**
|
|
21
|
+
* The original `Response` object.
|
|
22
|
+
* This is useful for accessing headers and other response metadata.
|
|
23
|
+
* It will be `undefined` for non-HTTP errors like network failures or timeouts.
|
|
24
|
+
*/
|
|
25
|
+
response?: Response;
|
|
26
|
+
/**
|
|
27
|
+
* The parsed body of the error response, typically a JSON object.
|
|
28
|
+
* It will be `undefined` for non-HTTP errors.
|
|
29
|
+
*/
|
|
30
|
+
data?: JsonObject | string;
|
|
31
|
+
/**
|
|
32
|
+
* The specific reason for the fetch failure.
|
|
33
|
+
*/
|
|
34
|
+
reason: FetchErrorReason;
|
|
35
|
+
constructor(reason: FetchErrorReason, message: string, response?: Response, data?: JsonObject | string);
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAElD;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,UAAW,SAAQ,KAAK;IACnC;;;;OAIG;IACI,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAE3B;;;OAGG;IACI,IAAI,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;IAElC;;OAEG;IACI,MAAM,EAAE,gBAAgB,CAAC;gBAEpB,MAAM,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,UAAU,GAAG,MAAM;CAOvG"}
|
package/dist/main.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** 📦 @alwatr/fetch
|
|
2
|
-
__dev_mode__: console.debug("📦 @alwatr/fetch
|
|
3
|
-
"use strict";var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:true})};var __copyProps=(to,from,except,desc)=>{if(from&&typeof from==="object"||typeof from==="function"){for(let key of __getOwnPropNames(from))if(!__hasOwnProp.call(to,key)&&key!==except)__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable})}return to};var __toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:true}),mod);var main_exports={};__export(main_exports,{cacheSupported:()=>cacheSupported,fetch:()=>fetch});module.exports=__toCommonJS(main_exports);var import_delay=require("@alwatr/delay");var import_global_this=require("@alwatr/global-this");var import_http_primer=require("@alwatr/http-primer");var import_logger=require("@alwatr/logger");var import_parse_duration=require("@alwatr/parse-duration");var logger_=(0,import_logger.createLogger)("@alwatr/fetch");var globalThis_=(0,import_global_this.getGlobalThis)();var cacheSupported=
|
|
1
|
+
/** 📦 @alwatr/fetch v7.0.0 */
|
|
2
|
+
__dev_mode__: console.debug("📦 @alwatr/fetch v7.0.0");
|
|
3
|
+
"use strict";var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:true})};var __copyProps=(to,from,except,desc)=>{if(from&&typeof from==="object"||typeof from==="function"){for(let key of __getOwnPropNames(from))if(!__hasOwnProp.call(to,key)&&key!==except)__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable})}return to};var __toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:true}),mod);var main_exports={};__export(main_exports,{FetchError:()=>FetchError,cacheSupported:()=>cacheSupported,fetch:()=>fetch});module.exports=__toCommonJS(main_exports);var import_delay=require("@alwatr/delay");var import_global_this=require("@alwatr/global-this");var import_has_own=require("@alwatr/has-own");var import_http_primer=require("@alwatr/http-primer");var import_logger=require("@alwatr/logger");var import_parse_duration=require("@alwatr/parse-duration");var FetchError=class extends Error{constructor(reason,message,response,data){super(message);this.name="FetchError";this.reason=reason;this.response=response;this.data=data}};var logger_=(0,import_logger.createLogger)("@alwatr/fetch");var globalThis_=(0,import_global_this.getGlobalThis)();var cacheSupported=(0,import_has_own.hasOwn)(globalThis_,"caches");var duplicateRequestStorage_={};var defaultFetchOptions={method:"GET",headers:{},timeout:8e3,retry:3,retryDelay:1e3,removeDuplicate:"never",cacheStrategy:"network_only",cacheStorageName:"fetch_cache"};async function fetch(url,options={}){logger_.logMethodArgs?.("fetch",{url,options});const options_=_processOptions(url,options);try{const response=await handleCacheStrategy_(options_);if(!response.ok){throw new FetchError("http_error",`HTTP error! status: ${response.status} ${response.statusText}`,response)}return[response,null]}catch(err){let error;if(err instanceof FetchError){error=err;if(error.response!==void 0&&error.data===void 0){const bodyText=await error.response.text().catch(()=>"");if(bodyText.trim().length>0){try{error.data=JSON.parse(bodyText)}catch{error.data=bodyText}}}}else if(err instanceof Error){if(err.name==="AbortError"){error=new FetchError("aborted",err.message)}else{error=new FetchError("network_error",err.message)}}else{error=new FetchError("unknown_error",String(err??"unknown_error"))}logger_.error("fetch",error.reason,{error});return[null,error]}}function _processOptions(url,options){logger_.logMethodArgs?.("_processOptions",{url,options});const options_={...defaultFetchOptions,...options,url};options_.window??=null;if(options_.removeDuplicate==="auto"){options_.removeDuplicate=cacheSupported?"until_load":"always"}if(options_.url.lastIndexOf("?")===-1&&options_.queryParams!=null){const queryParams=options_.queryParams;const queryArray=Object.keys(queryParams).map(key=>`${encodeURIComponent(key)}=${encodeURIComponent(String(queryParams[key]))}`);if(queryArray.length>0){options_.url+="?"+queryArray.join("&")}}if(options_.bodyJson!==void 0){options_.body=JSON.stringify(options_.bodyJson);options_.headers["content-type"]=import_http_primer.MimeTypes.JSON}if(options_.bearerToken!==void 0){options_.headers.authorization=`Bearer ${options_.bearerToken}`}else if(options_.alwatrAuth!==void 0){options_.headers.authorization=`Alwatr ${options_.alwatrAuth.userId}:${options_.alwatrAuth.userToken}`}logger_.logProperty?.("fetch.options",options_);return options_}async function handleCacheStrategy_(options){if(options.cacheStrategy==="network_only"){return handleRemoveDuplicate_(options)}logger_.logMethod?.("handleCacheStrategy_");if(!cacheSupported){logger_.incident?.("fetch","fetch_cache_strategy_unsupported",{cacheSupported});options.cacheStrategy="network_only";return handleRemoveDuplicate_(options)}const cacheStorage=await caches.open(options.cacheStorageName);const request=new Request(options.url,options);switch(options.cacheStrategy){case"cache_first":{const cachedResponse=await cacheStorage.match(request);if(cachedResponse!=null){return cachedResponse}const response=await handleRemoveDuplicate_(options);if(response.ok){cacheStorage.put(request,response.clone())}return response}case"cache_only":{const cachedResponse=await cacheStorage.match(request);if(cachedResponse==null){throw new FetchError("cache_not_found","Resource not found in cache")}return cachedResponse}case"network_first":{try{const networkResponse=await handleRemoveDuplicate_(options);if(networkResponse.ok){cacheStorage.put(request,networkResponse.clone())}return networkResponse}catch(err){const cachedResponse=await cacheStorage.match(request);if(cachedResponse!=null){return cachedResponse}throw err}}case"update_cache":{const networkResponse=await handleRemoveDuplicate_(options);if(networkResponse.ok){cacheStorage.put(request,networkResponse.clone())}return networkResponse}case"stale_while_revalidate":{const cachedResponse=await cacheStorage.match(request);const fetchedResponsePromise=handleRemoveDuplicate_(options).then(networkResponse=>{if(networkResponse.ok){cacheStorage.put(request,networkResponse.clone());if(typeof options.revalidateCallback==="function"){setTimeout(options.revalidateCallback,0,networkResponse.clone())}}return networkResponse});return cachedResponse??fetchedResponsePromise}default:{return handleRemoveDuplicate_(options)}}}async function handleRemoveDuplicate_(options){if(options.removeDuplicate==="never"){return handleRetryPattern_(options)}logger_.logMethod?.("handleRemoveDuplicate_");const bodyString=typeof options.body==="string"?options.body:"";const cacheKey=`${options.method} ${options.url} ${bodyString}`;duplicateRequestStorage_[cacheKey]??=handleRetryPattern_(options);try{const response=await duplicateRequestStorage_[cacheKey];if(duplicateRequestStorage_[cacheKey]!=null){if(response.ok!==true||options.removeDuplicate==="until_load"){delete duplicateRequestStorage_[cacheKey]}}return response.clone()}catch(err){delete duplicateRequestStorage_[cacheKey];throw err}}async function handleRetryPattern_(options){if(!(options.retry>1)){return handleTimeout_(options)}logger_.logMethod?.("handleRetryPattern_");options.retry--;const externalAbortSignal=options.signal;try{const response=await handleTimeout_(options);if(!response.ok&&response.status>=import_http_primer.HttpStatusCodes.Error_Server_500_Internal_Server_Error){throw new FetchError("http_error",`HTTP error! status: ${response.status} ${response.statusText}`,response)}return response}catch(err){logger_.accident("fetch","fetch_failed_retry",err);if(globalThis_.navigator?.onLine===false){logger_.accident("handleRetryPattern_","offline","Skip retry because offline");throw err}await import_delay.delay.by(options.retryDelay);options.signal=externalAbortSignal;return handleRetryPattern_(options)}}function handleTimeout_(options){if(options.timeout===0){return globalThis_.fetch(options.url,options)}logger_.logMethod?.("handleTimeout_");return new Promise((resolved,reject)=>{const abortController=typeof AbortController==="function"?new AbortController:null;const externalAbortSignal=options.signal;options.signal=abortController?.signal;if(abortController!==null&&externalAbortSignal!=null){externalAbortSignal.addEventListener("abort",()=>abortController.abort(),{once:true})}const timeoutId=setTimeout(()=>{reject(new FetchError("timeout","fetch_timeout"));abortController?.abort("fetch_timeout")},(0,import_parse_duration.parseDuration)(options.timeout));globalThis_.fetch(options.url,options).then(response=>resolved(response)).catch(reason=>reject(reason)).finally(()=>{clearTimeout(timeoutId)})})}0&&(module.exports={FetchError,cacheSupported,fetch});
|
|
4
4
|
//# sourceMappingURL=main.cjs.map
|
package/dist/main.cjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/main.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * @module @alwatr/fetch\n *\n * An enhanced, lightweight, and dependency-free wrapper for the native `fetch` API.\n * It provides modern features like caching strategies, request retries, timeouts, and\n * duplicate request handling.\n */\n\nimport {delay} from '@alwatr/delay';\nimport {getGlobalThis} from '@alwatr/global-this';\nimport {HttpStatusCodes, MimeTypes} from '@alwatr/http-primer';\nimport {createLogger} from '@alwatr/logger';\nimport {parseDuration} from '@alwatr/parse-duration';\n\nimport type {AlwatrFetchOptions_, FetchOptions} from './type.js';\n\nexport {cacheSupported};\nexport type * from './type.js';\n\nconst logger_ = createLogger('@alwatr/fetch');\nconst globalThis_ = getGlobalThis();\n\n/**\n * A boolean flag indicating whether the browser's Cache API is supported.\n */\nconst cacheSupported = Object.hasOwn(globalThis_, 'caches');\n\n/**\n * A simple in-memory storage for tracking and managing duplicate in-flight requests.\n * The key is a unique identifier for the request (e.g., method + URL + body),\n * and the value is the promise of the ongoing fetch operation.\n */\nconst duplicateRequestStorage_: Record<string, Promise<Response>> = {};\n\n/**\n * Default options for all fetch requests. These can be overridden by passing\n * a custom `options` object to the `fetch` function.\n */\nconst defaultFetchOptions: AlwatrFetchOptions_ = {\n method: 'GET',\n headers: {},\n timeout: 8_000,\n retry: 3,\n retryDelay: 1_000,\n removeDuplicate: 'never',\n cacheStrategy: 'network_only',\n cacheStorageName: 'fetch_cache',\n};\n\n/**\n * Internal-only fetch options type, which includes the URL and ensures all\n * optional properties from AlwatrFetchOptions_ are present.\n */\ntype FetchOptions__ = AlwatrFetchOptions_ & Omit<RequestInit, 'headers'> & {url: string};\n\n/**\n * An enhanced wrapper for the native `fetch` function.\n *\n * This function extends the standard `fetch` with additional features such as:\n * - **Timeout**: Aborts the request if it takes too long.\n * - **Retry Pattern**: Automatically retries the request on failure (e.g., server errors or network issues).\n * - **Duplicate Request Handling**: Prevents sending multiple identical requests in parallel.\n * - **Cache Strategies**: Provides various caching mechanisms using the browser's Cache API.\n * - **Simplified API**: Offers convenient options for adding query parameters, JSON bodies, and auth tokens.\n *\n * @see {@link FetchOptions} for a detailed list of available options.\n *\n * @param {string} url - The URL to fetch.\n * @param {FetchOptions} options - Optional configuration for the fetch request.\n * @returns {Promise<Response>} A promise that resolves to the `Response` object for the request.\n *\n * @example\n * ```typescript\n * async function fetchProducts() {\n * try {\n * const response = await fetch(\"/api/products\", {\n * queryParams: { limit: 10, category: \"electronics\" },\n * timeout: 5_000, // 5 seconds\n * retry: 3,\n * cacheStrategy: \"stale_while_revalidate\",\n * });\n *\n * if (!response.ok) {\n * throw new Error(`HTTP error! status: ${response.status}`);\n * }\n *\n * const data = await response.json();\n * console.log(\"Products:\", data);\n * } catch (error) {\n * console.error(\"Failed to fetch products:\", error);\n * }\n * }\n *\n * fetchProducts();\n * ```\n */\nexport function fetch(url: string, options: FetchOptions): Promise<Response> {\n logger_.logMethodArgs?.('fetch', {url, options});\n\n const options_: FetchOptions__ = {\n ...defaultFetchOptions,\n ...options,\n url,\n };\n\n options_.window ??= null;\n\n if (options_.removeDuplicate === 'auto') {\n options_.removeDuplicate = cacheSupported ? 'until_load' : 'always';\n }\n\n // Append query parameters to the URL if they are provided and the URL doesn't already have them.\n if (options_.url.lastIndexOf('?') === -1 && options_.queryParams != null) {\n const queryParams = options_.queryParams;\n // prettier-ignore\n const queryArray = Object\n .keys(queryParams)\n .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(String(queryParams[key]))}`);\n\n if (queryArray.length > 0) {\n options_.url += '?' + queryArray.join('&');\n }\n }\n\n // If `bodyJson` is provided, stringify it and set the appropriate 'Content-Type' header.\n if (options_.bodyJson !== undefined) {\n options_.body = JSON.stringify(options_.bodyJson);\n options_.headers['content-type'] = MimeTypes.JSON;\n }\n\n // Set the 'Authorization' header for bearer tokens or Alwatr's authentication scheme.\n if (options_.bearerToken !== undefined) {\n options_.headers.authorization = `Bearer ${options_.bearerToken}`;\n }\n else if (options_.alwatrAuth !== undefined) {\n options_.headers.authorization = `Alwatr ${options_.alwatrAuth.userId}:${options_.alwatrAuth.userToken}`;\n }\n\n logger_.logProperty?.('fetch.options', options_);\n\n // Start the fetch lifecycle, beginning with the cache strategy.\n return handleCacheStrategy_(options_);\n}\n\n/**\n * Manages caching strategies for the fetch request.\n * If the strategy is `network_only`, it bypasses caching and proceeds to the next step.\n * Otherwise, it interacts with the browser's Cache API based on the selected strategy.\n *\n * @param {FetchOptions__} options - The fully configured fetch options.\n * @returns {Promise<Response>} A promise resolving to a `Response` object, either from the cache or the network.\n * @private\n */\nasync function handleCacheStrategy_(options: FetchOptions__): Promise<Response> {\n if (options.cacheStrategy === 'network_only') {\n return handleRemoveDuplicate_(options);\n }\n // else\n\n logger_.logMethod?.('handleCacheStrategy_');\n\n if (!cacheSupported) {\n logger_.incident?.('fetch', 'fetch_cache_strategy_unsupported', {\n cacheSupported,\n });\n // Fallback to network_only if Cache API is not available.\n options.cacheStrategy = 'network_only';\n return handleRemoveDuplicate_(options);\n }\n // else\n\n const cacheStorage = await caches.open(options.cacheStorageName);\n\n const request = new Request(options.url, options);\n\n switch (options.cacheStrategy) {\n case 'cache_first': {\n const cachedResponse = await cacheStorage.match(request);\n if (cachedResponse != null) {\n return cachedResponse;\n }\n // else\n\n const response = await handleRemoveDuplicate_(options);\n if (response.ok) {\n cacheStorage.put(request, response.clone());\n }\n return response;\n }\n\n case 'cache_only': {\n const cachedResponse = await cacheStorage.match(request);\n if (cachedResponse == null) {\n logger_.accident('_handleCacheStrategy', 'fetch_cache_not_found', {url: request.url});\n throw new Error('fetch_cache_not_found');\n }\n // else\n\n return cachedResponse;\n }\n\n case 'network_first': {\n try {\n const networkResponse = await handleRemoveDuplicate_(options);\n if (networkResponse.ok) {\n cacheStorage.put(request, networkResponse.clone());\n }\n return networkResponse;\n }\n catch (err) {\n const cachedResponse = await cacheStorage.match(request);\n if (cachedResponse != null) {\n return cachedResponse;\n }\n // else\n\n throw err;\n }\n }\n\n case 'update_cache': {\n const networkResponse = await handleRemoveDuplicate_(options);\n if (networkResponse.ok) {\n cacheStorage.put(request, networkResponse.clone());\n }\n return networkResponse;\n }\n\n case 'stale_while_revalidate': {\n const cachedResponse = await cacheStorage.match(request);\n const fetchedResponsePromise = handleRemoveDuplicate_(options).then((networkResponse) => {\n if (networkResponse.ok) {\n cacheStorage.put(request, networkResponse.clone());\n if (typeof options.revalidateCallback === 'function') {\n setTimeout(options.revalidateCallback, 0, networkResponse.clone());\n }\n }\n return networkResponse;\n });\n\n return cachedResponse ?? fetchedResponsePromise;\n }\n\n default: {\n return handleRemoveDuplicate_(options);\n }\n }\n}\n\n/**\n * Handles duplicate request elimination.\n *\n * It creates a unique key based on the request method, URL, and body. If a request with the\n * same key is already in flight, it returns the promise of the existing request instead of\n * creating a new one. This prevents redundant network calls for identical parallel requests.\n *\n * @param {FetchOptions__} options - The fully configured fetch options.\n * @returns {Promise<Response>} A promise resolving to a cloned `Response` object.\n * @private\n */\nasync function handleRemoveDuplicate_(options: FetchOptions__): Promise<Response> {\n if (options.removeDuplicate === 'never') {\n return handleRetryPattern_(options);\n }\n // else\n\n logger_.logMethod?.('handleRemoveDuplicate_');\n\n // Create a unique key for the request. Including the body is crucial to differentiate\n // between requests to the same URL but with different payloads (e.g., POST requests).\n const bodyString = typeof options.body === 'string' ? options.body : '';\n const cacheKey = `${options.method} ${options.url} ${bodyString}`;\n\n // If a request with the same key doesn't exist, create it and store its promise.\n duplicateRequestStorage_[cacheKey] ??= handleRetryPattern_(options);\n\n try {\n // Await the shared promise to get the response.\n const response = await duplicateRequestStorage_[cacheKey];\n\n // Clean up the stored promise based on the removal strategy.\n if (duplicateRequestStorage_[cacheKey] != null) {\n if (response.ok !== true || options.removeDuplicate === 'until_load') {\n // Remove after completion for 'until_load' or if the request failed.\n delete duplicateRequestStorage_[cacheKey];\n }\n }\n\n // Return a clone of the response, so each caller can consume the body independently.\n return response.clone();\n }\n catch (err) {\n // If the request fails, remove it from storage to allow for retries.\n delete duplicateRequestStorage_[cacheKey];\n throw err;\n }\n}\n\n/**\n * Implements a retry mechanism for the fetch request.\n * If the request fails due to a server error (status >= 500) or a timeout,\n * it will be retried up to the specified number of times.\n *\n * @param {FetchOptions__} options - The fully configured fetch options.\n * @returns {Promise<Response>} A promise that resolves to the final `Response` after all retries.\n * @private\n */\nasync function handleRetryPattern_(options: FetchOptions__): Promise<Response> {\n if (!(options.retry > 1)) {\n return handleTimeout_(options);\n }\n // else\n\n logger_.logMethod?.('handleRetryPattern_');\n options.retry--;\n\n const externalAbortSignal = options.signal;\n\n try {\n const response = await handleTimeout_(options);\n\n // Only retry on server errors (5xx). Client errors (4xx) are not retried.\n if (response.status < HttpStatusCodes.Error_Server_500_Internal_Server_Error) {\n return response;\n }\n // else\n\n throw new Error('fetch_server_error');\n }\n catch (err) {\n logger_.accident('fetch', 'fetch_failed_retry', err);\n\n // Do not retry if the browser is offline.\n if (globalThis_.navigator?.onLine === false) {\n logger_.accident('handleRetryPattern_', 'offline', 'Skip retry because offline');\n throw err;\n }\n\n await delay.by(options.retryDelay);\n\n // Restore the original signal for the next attempt.\n options.signal = externalAbortSignal;\n return handleRetryPattern_(options);\n }\n}\n\n/**\n * Wraps the native fetch call with a timeout mechanism.\n *\n * It uses an `AbortController` to abort the request if it does not complete\n * within the specified `timeout` duration. It also respects external abort signals.\n *\n * @param {FetchOptions__} options - The fully configured fetch options.\n * @returns {Promise<Response>} A promise that resolves with the `Response` or rejects on timeout.\n * @private\n */\nfunction handleTimeout_(options: FetchOptions__): Promise<Response> {\n if (options.timeout === 0) {\n // If timeout is disabled, call fetch directly.\n return globalThis_.fetch(options.url, options);\n }\n\n logger_.logMethod?.('handleTimeout_');\n\n return new Promise((resolved, reject) => {\n const abortController = typeof AbortController === 'function' ? new AbortController() : null;\n const externalAbortSignal = options.signal;\n options.signal = abortController?.signal;\n\n // If an external AbortSignal is provided, listen to it and propagate the abort.\n if (abortController !== null && externalAbortSignal != null) {\n externalAbortSignal.addEventListener('abort', () => abortController.abort(), {once: true});\n }\n\n const timeoutId = setTimeout(() => {\n reject(new Error('fetch_timeout'));\n abortController?.abort('fetch_timeout');\n }, parseDuration(options.timeout!));\n\n globalThis_\n .fetch(options.url, options)\n .then((response) => resolved(response))\n .catch((reason) => reject(reason))\n .finally(() => {\n // Clean up the timeout to prevent it from firing after the request has completed.\n clearTimeout(timeoutId);\n });\n });\n}\n"],
|
|
5
|
-
"mappings": ";;qqBAAA,
|
|
3
|
+
"sources": ["../src/main.ts", "../src/error.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * @module @alwatr/fetch\n *\n * An enhanced, lightweight, and dependency-free wrapper for the native `fetch`\n * API. It provides modern features like caching strategies, request retries,\n * timeouts, and duplicate request handling.\n */\n\nimport {delay} from '@alwatr/delay';\nimport {getGlobalThis} from '@alwatr/global-this';\nimport {hasOwn} from '@alwatr/has-own';\nimport {HttpStatusCodes, MimeTypes} from '@alwatr/http-primer';\nimport {createLogger} from '@alwatr/logger';\nimport {parseDuration} from '@alwatr/parse-duration';\n\nimport {FetchError} from './error.js';\n\nimport type {AlwatrFetchOptions_, FetchOptions, FetchResponse} from './type.js';\n\nexport {cacheSupported};\nexport type * from './type.js';\nexport * from './error.js';\n\nconst logger_ = createLogger('@alwatr/fetch');\nconst globalThis_ = getGlobalThis();\n\n/**\n * A boolean flag indicating whether the browser's Cache API is supported.\n */\nconst cacheSupported = /* #__PURE__ */ hasOwn(globalThis_, 'caches');\n\n/**\n * A simple in-memory storage for tracking and managing duplicate in-flight requests.\n * The key is a unique identifier for the request (e.g., method + URL + body),\n * and the value is the promise of the ongoing fetch operation.\n */\nconst duplicateRequestStorage_: Record<string, Promise<Response>> = {};\n\n/**\n * Default options for all fetch requests. These can be overridden by passing\n * a custom `options` object to the `fetch` function.\n */\nconst defaultFetchOptions: AlwatrFetchOptions_ = {\n method: 'GET',\n headers: {},\n timeout: 8_000,\n retry: 3,\n retryDelay: 1_000,\n removeDuplicate: 'never',\n cacheStrategy: 'network_only',\n cacheStorageName: 'fetch_cache',\n};\n\n/**\n * Internal-only fetch options type, which includes the URL and ensures all\n * optional properties from AlwatrFetchOptions_ are present.\n */\ntype FetchOptions__ = AlwatrFetchOptions_ & Omit<RequestInit, 'headers'> & {url: string};\n\n/**\n * An enhanced wrapper for the native `fetch` function.\n *\n * This function extends the standard `fetch` with additional features such as:\n * - **Timeout**: Aborts the request if it takes too long.\n * - **Retry Pattern**: Automatically retries the request on failure (e.g., server errors or network issues).\n * - **Duplicate Request Handling**: Prevents sending multiple identical requests in parallel.\n * - **Cache Strategies**: Provides various caching mechanisms using the browser's Cache API.\n * - **Simplified API**: Offers convenient options for adding query parameters, JSON bodies, and auth tokens.\n *\n * @see {@link FetchOptions} for a detailed list of available options.\n *\n * @param {string} url - The URL to fetch.\n * @param {FetchOptions} options - Optional configuration for the fetch request.\n * @returns {Promise<FetchResponse>} A promise that resolves to a tuple. On\n * success, it returns `[response, null]`. On failure, it returns `[null,\n * FetchError]`.\n *\n * @example\n * ```typescript\n * import {fetch} from '@alwatr/fetch';\n *\n * async function fetchProducts() {\n * const [response, error] = await fetch('/api/products', {\n * queryParams: { limit: 10 },\n * timeout: 5_000,\n * });\n *\n * if (error) {\n * console.error('Request failed:', error.reason);\n * return;\n * }\n *\n * // At this point, response is guaranteed to be valid and ok.\n * const data = await response.json();\n * console.log('Products:', data);\n * }\n *\n * fetchProducts();\n * ```\n */\nexport async function fetch(url: string, options: FetchOptions = {}): Promise<FetchResponse> {\n logger_.logMethodArgs?.('fetch', {url, options});\n\n const options_ = _processOptions(url, options);\n\n try {\n // Start the fetch lifecycle, beginning with the cache strategy.\n const response = await handleCacheStrategy_(options_);\n\n if (!response.ok) {\n throw new FetchError('http_error', `HTTP error! status: ${response.status} ${response.statusText}`, response);\n }\n\n return [response, null];\n }\n catch (err) {\n let error: FetchError;\n\n if (err instanceof FetchError) {\n error = err;\n\n if (error.response !== undefined && error.data === undefined) {\n const bodyText = await error.response.text().catch(() => '');\n\n if (bodyText.trim().length > 0) {\n try {\n // Try to parse as JSON\n error.data = JSON.parse(bodyText);\n }\n catch {\n error.data = bodyText;\n }\n }\n }\n }\n else if (err instanceof Error) {\n if (err.name === 'AbortError') {\n error = new FetchError('aborted', err.message);\n }\n else {\n error = new FetchError('network_error', err.message);\n }\n }\n else {\n error = new FetchError('unknown_error', String(err ?? 'unknown_error'));\n }\n\n logger_.error('fetch', error.reason, {error});\n return [null, error];\n }\n}\n\n/**\n * Processes and sanitizes the fetch options.\n *\n * @param {string} url - The URL to fetch.\n * @param {FetchOptions} options - The user-provided options.\n * @returns {FetchOptions__} The processed and complete fetch options.\n * @private\n */\nfunction _processOptions(url: string, options: FetchOptions): FetchOptions__ {\n logger_.logMethodArgs?.('_processOptions', {url, options});\n\n const options_: FetchOptions__ = {\n ...defaultFetchOptions,\n ...options,\n url,\n };\n\n options_.window ??= null;\n\n if (options_.removeDuplicate === 'auto') {\n options_.removeDuplicate = cacheSupported ? 'until_load' : 'always';\n }\n\n // Append query parameters to the URL if they are provided and the URL doesn't already have them.\n if (options_.url.lastIndexOf('?') === -1 && options_.queryParams != null) {\n const queryParams = options_.queryParams;\n // prettier-ignore\n const queryArray = Object\n .keys(queryParams)\n .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(String(queryParams[key]))}`);\n\n if (queryArray.length > 0) {\n options_.url += '?' + queryArray.join('&');\n }\n }\n\n // If `bodyJson` is provided, stringify it and set the appropriate 'Content-Type' header.\n if (options_.bodyJson !== undefined) {\n options_.body = JSON.stringify(options_.bodyJson);\n options_.headers['content-type'] = MimeTypes.JSON;\n }\n\n // Set the 'Authorization' header for bearer tokens or Alwatr's authentication scheme.\n if (options_.bearerToken !== undefined) {\n options_.headers.authorization = `Bearer ${options_.bearerToken}`;\n }\n else if (options_.alwatrAuth !== undefined) {\n options_.headers.authorization = `Alwatr ${options_.alwatrAuth.userId}:${options_.alwatrAuth.userToken}`;\n }\n\n logger_.logProperty?.('fetch.options', options_);\n\n return options_;\n}\n\n/**\n * Manages caching strategies for the fetch request.\n * If the strategy is `network_only`, it bypasses caching and proceeds to the next step.\n * Otherwise, it interacts with the browser's Cache API based on the selected strategy.\n *\n * @param {FetchOptions__} options - The fully configured fetch options.\n * @returns {Promise<Response>} A promise resolving to a `Response` object, either from the cache or the network.\n * @private\n */\nasync function handleCacheStrategy_(options: FetchOptions__): Promise<Response> {\n if (options.cacheStrategy === 'network_only') {\n return handleRemoveDuplicate_(options);\n }\n // else\n\n logger_.logMethod?.('handleCacheStrategy_');\n\n if (!cacheSupported) {\n logger_.incident?.('fetch', 'fetch_cache_strategy_unsupported', {\n cacheSupported,\n });\n // Fallback to network_only if Cache API is not available.\n options.cacheStrategy = 'network_only';\n return handleRemoveDuplicate_(options);\n }\n // else\n\n const cacheStorage = await caches.open(options.cacheStorageName);\n\n const request = new Request(options.url, options);\n\n switch (options.cacheStrategy) {\n case 'cache_first': {\n const cachedResponse = await cacheStorage.match(request);\n if (cachedResponse != null) {\n return cachedResponse;\n }\n // else\n\n const response = await handleRemoveDuplicate_(options);\n if (response.ok) {\n cacheStorage.put(request, response.clone());\n }\n return response;\n }\n\n case 'cache_only': {\n const cachedResponse = await cacheStorage.match(request);\n if (cachedResponse == null) {\n throw new FetchError('cache_not_found', 'Resource not found in cache');\n }\n // else\n\n return cachedResponse;\n }\n\n case 'network_first': {\n try {\n const networkResponse = await handleRemoveDuplicate_(options);\n if (networkResponse.ok) {\n cacheStorage.put(request, networkResponse.clone());\n }\n return networkResponse;\n }\n catch (err) {\n const cachedResponse = await cacheStorage.match(request);\n if (cachedResponse != null) {\n return cachedResponse;\n }\n // else\n\n throw err;\n }\n }\n\n case 'update_cache': {\n const networkResponse = await handleRemoveDuplicate_(options);\n if (networkResponse.ok) {\n cacheStorage.put(request, networkResponse.clone());\n }\n return networkResponse;\n }\n\n case 'stale_while_revalidate': {\n const cachedResponse = await cacheStorage.match(request);\n const fetchedResponsePromise = handleRemoveDuplicate_(options).then((networkResponse) => {\n if (networkResponse.ok) {\n cacheStorage.put(request, networkResponse.clone());\n if (typeof options.revalidateCallback === 'function') {\n setTimeout(options.revalidateCallback, 0, networkResponse.clone());\n }\n }\n return networkResponse;\n });\n\n return cachedResponse ?? fetchedResponsePromise;\n }\n\n default: {\n return handleRemoveDuplicate_(options);\n }\n }\n}\n\n/**\n * Handles duplicate request elimination.\n *\n * It creates a unique key based on the request method, URL, and body. If a request with the\n * same key is already in flight, it returns the promise of the existing request instead of\n * creating a new one. This prevents redundant network calls for identical parallel requests.\n *\n * @param {FetchOptions__} options - The fully configured fetch options.\n * @returns {Promise<Response>} A promise resolving to a cloned `Response` object.\n * @private\n */\nasync function handleRemoveDuplicate_(options: FetchOptions__): Promise<Response> {\n if (options.removeDuplicate === 'never') {\n return handleRetryPattern_(options);\n }\n // else\n\n logger_.logMethod?.('handleRemoveDuplicate_');\n\n // Create a unique key for the request. Including the body is crucial to differentiate\n // between requests to the same URL but with different payloads (e.g., POST requests).\n const bodyString = typeof options.body === 'string' ? options.body : '';\n const cacheKey = `${options.method} ${options.url} ${bodyString}`;\n\n // If a request with the same key doesn't exist, create it and store its promise.\n duplicateRequestStorage_[cacheKey] ??= handleRetryPattern_(options);\n\n try {\n // Await the shared promise to get the response.\n const response = await duplicateRequestStorage_[cacheKey];\n\n // Clean up the stored promise based on the removal strategy.\n if (duplicateRequestStorage_[cacheKey] != null) {\n if (response.ok !== true || options.removeDuplicate === 'until_load') {\n // Remove after completion for 'until_load' or if the request failed.\n delete duplicateRequestStorage_[cacheKey];\n }\n }\n\n // Return a clone of the response, so each caller can consume the body independently.\n return response.clone();\n }\n catch (err) {\n // If the request fails, remove it from storage to allow for retries.\n delete duplicateRequestStorage_[cacheKey];\n throw err;\n }\n}\n\n/**\n * Implements a retry mechanism for the fetch request.\n * If the request fails due to a server error (status >= 500) or a timeout,\n * it will be retried up to the specified number of times.\n *\n * @param {FetchOptions__} options - The fully configured fetch options.\n * @returns {Promise<Response>} A promise that resolves to the final `Response` after all retries.\n * @private\n */\nasync function handleRetryPattern_(options: FetchOptions__): Promise<Response> {\n if (!(options.retry > 1)) {\n return handleTimeout_(options);\n }\n // else\n\n logger_.logMethod?.('handleRetryPattern_');\n options.retry--;\n\n const externalAbortSignal = options.signal;\n\n try {\n const response = await handleTimeout_(options);\n\n if (!response.ok && response.status >= HttpStatusCodes.Error_Server_500_Internal_Server_Error) {\n // only retry for server errors (5xx)\n throw new FetchError('http_error', `HTTP error! status: ${response.status} ${response.statusText}`, response);\n }\n\n return response;\n }\n catch (err) {\n logger_.accident('fetch', 'fetch_failed_retry', err);\n\n // Do not retry if the browser is offline.\n if (globalThis_.navigator?.onLine === false) {\n logger_.accident('handleRetryPattern_', 'offline', 'Skip retry because offline');\n throw err;\n }\n\n await delay.by(options.retryDelay);\n\n // Restore the original signal for the next attempt.\n options.signal = externalAbortSignal;\n return handleRetryPattern_(options);\n }\n}\n\n/**\n * Wraps the native fetch call with a timeout mechanism.\n *\n * It uses an `AbortController` to abort the request if it does not complete\n * within the specified `timeout` duration. It also respects external abort signals.\n *\n * @param {FetchOptions__} options - The fully configured fetch options.\n * @returns {Promise<Response>} A promise that resolves with the `Response` or rejects on timeout.\n * @private\n */\nfunction handleTimeout_(options: FetchOptions__): Promise<Response> {\n if (options.timeout === 0) {\n // If timeout is disabled, call fetch directly.\n return globalThis_.fetch(options.url, options);\n }\n\n logger_.logMethod?.('handleTimeout_');\n\n return new Promise((resolved, reject) => {\n const abortController = typeof AbortController === 'function' ? new AbortController() : null;\n const externalAbortSignal = options.signal;\n options.signal = abortController?.signal;\n\n // If an external AbortSignal is provided, listen to it and propagate the abort.\n if (abortController !== null && externalAbortSignal != null) {\n externalAbortSignal.addEventListener('abort', () => abortController.abort(), {once: true});\n }\n\n const timeoutId = setTimeout(() => {\n reject(new FetchError('timeout', 'fetch_timeout'));\n abortController?.abort('fetch_timeout');\n }, parseDuration(options.timeout!));\n\n globalThis_\n .fetch(options.url, options)\n .then((response) => resolved(response))\n .catch((reason) => reject(reason))\n .finally(() => {\n // Clean up the timeout to prevent it from firing after the request has completed.\n clearTimeout(timeoutId);\n });\n });\n}\n", "import type { FetchErrorReason } from \"./type.js\";\n\n/**\n * Custom error class for fetch-related failures.\n *\n * This error is thrown when a fetch request fails, either due to a network issue\n * or an HTTP error status (i.e., `response.ok` is `false`). It enriches the\n * standard `Error` object with the `response` and the parsed `data` from the\n * response body, allowing for more detailed error handling.\n *\n * @example\n * ```typescript\n * const [response, error] = await fetch('/api/endpoint');\n * if (error) {\n * console.error(`Request failed with status ${error.response?.status}`);\n * console.error('Server response:', error.data);\n * }\n * ```\n */\nexport class FetchError extends Error {\n /**\n * The original `Response` object.\n * This is useful for accessing headers and other response metadata.\n * It will be `undefined` for non-HTTP errors like network failures or timeouts.\n */\n public response?: Response;\n\n /**\n * The parsed body of the error response, typically a JSON object.\n * It will be `undefined` for non-HTTP errors.\n */\n public data?: JsonObject | string;\n\n /**\n * The specific reason for the fetch failure.\n */\n public reason: FetchErrorReason;\n\n constructor(reason: FetchErrorReason, message: string, response?: Response, data?: JsonObject | string) {\n super(message);\n this.name = 'FetchError';\n this.reason = reason;\n this.response = response;\n this.data = data;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/main.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module @alwatr/fetch
|
|
3
3
|
*
|
|
4
|
-
* An enhanced, lightweight, and dependency-free wrapper for the native `fetch`
|
|
5
|
-
* It provides modern features like caching strategies, request retries,
|
|
6
|
-
* duplicate request handling.
|
|
4
|
+
* An enhanced, lightweight, and dependency-free wrapper for the native `fetch`
|
|
5
|
+
* API. It provides modern features like caching strategies, request retries,
|
|
6
|
+
* timeouts, and duplicate request handling.
|
|
7
7
|
*/
|
|
8
|
-
import type { FetchOptions } from './type.js';
|
|
8
|
+
import type { FetchOptions, FetchResponse } from './type.js';
|
|
9
9
|
export { cacheSupported };
|
|
10
10
|
export type * from './type.js';
|
|
11
|
+
export * from './error.js';
|
|
11
12
|
/**
|
|
12
13
|
* A boolean flag indicating whether the browser's Cache API is supported.
|
|
13
14
|
*/
|
|
@@ -26,32 +27,32 @@ declare const cacheSupported: boolean;
|
|
|
26
27
|
*
|
|
27
28
|
* @param {string} url - The URL to fetch.
|
|
28
29
|
* @param {FetchOptions} options - Optional configuration for the fetch request.
|
|
29
|
-
* @returns {Promise<
|
|
30
|
+
* @returns {Promise<FetchResponse>} A promise that resolves to a tuple. On
|
|
31
|
+
* success, it returns `[response, null]`. On failure, it returns `[null,
|
|
32
|
+
* FetchError]`.
|
|
30
33
|
*
|
|
31
34
|
* @example
|
|
32
35
|
* ```typescript
|
|
36
|
+
* import {fetch} from '@alwatr/fetch';
|
|
37
|
+
*
|
|
33
38
|
* async function fetchProducts() {
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* if (!response.ok) {
|
|
43
|
-
* throw new Error(`HTTP error! status: ${response.status}`);
|
|
44
|
-
* }
|
|
45
|
-
*
|
|
46
|
-
* const data = await response.json();
|
|
47
|
-
* console.log("Products:", data);
|
|
48
|
-
* } catch (error) {
|
|
49
|
-
* console.error("Failed to fetch products:", error);
|
|
39
|
+
* const [response, error] = await fetch('/api/products', {
|
|
40
|
+
* queryParams: { limit: 10 },
|
|
41
|
+
* timeout: 5_000,
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* if (error) {
|
|
45
|
+
* console.error('Request failed:', error.reason);
|
|
46
|
+
* return;
|
|
50
47
|
* }
|
|
48
|
+
*
|
|
49
|
+
* // At this point, response is guaranteed to be valid and ok.
|
|
50
|
+
* const data = await response.json();
|
|
51
|
+
* console.log('Products:', data);
|
|
51
52
|
* }
|
|
52
53
|
*
|
|
53
54
|
* fetchProducts();
|
|
54
55
|
* ```
|
|
55
56
|
*/
|
|
56
|
-
export declare function fetch(url: string, options
|
|
57
|
+
export declare function fetch(url: string, options?: FetchOptions): Promise<FetchResponse>;
|
|
57
58
|
//# sourceMappingURL=main.d.ts.map
|
package/dist/main.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAWH,OAAO,KAAK,EAAsB,YAAY,EAAE,aAAa,EAAC,MAAM,WAAW,CAAC;AAEhF,OAAO,EAAC,cAAc,EAAC,CAAC;AACxB,mBAAmB,WAAW,CAAC;AAC/B,cAAc,YAAY,CAAC;AAK3B;;GAEG;AACH,QAAA,MAAM,cAAc,SAAgD,CAAC;AA8BrE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,aAAa,CAAC,CAkD3F"}
|
package/dist/main.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** 📦 @alwatr/fetch
|
|
2
|
-
__dev_mode__: console.debug("📦 @alwatr/fetch
|
|
3
|
-
import{delay}from"@alwatr/delay";import{getGlobalThis}from"@alwatr/global-this";import{HttpStatusCodes,MimeTypes}from"@alwatr/http-primer";import{createLogger}from"@alwatr/logger";import{parseDuration}from"@alwatr/parse-duration";var logger_=createLogger("@alwatr/fetch");var globalThis_=getGlobalThis();var cacheSupported=
|
|
1
|
+
/** 📦 @alwatr/fetch v7.0.0 */
|
|
2
|
+
__dev_mode__: console.debug("📦 @alwatr/fetch v7.0.0");
|
|
3
|
+
import{delay}from"@alwatr/delay";import{getGlobalThis}from"@alwatr/global-this";import{hasOwn}from"@alwatr/has-own";import{HttpStatusCodes,MimeTypes}from"@alwatr/http-primer";import{createLogger}from"@alwatr/logger";import{parseDuration}from"@alwatr/parse-duration";var FetchError=class extends Error{constructor(reason,message,response,data){super(message);this.name="FetchError";this.reason=reason;this.response=response;this.data=data}};var logger_=createLogger("@alwatr/fetch");var globalThis_=getGlobalThis();var cacheSupported=hasOwn(globalThis_,"caches");var duplicateRequestStorage_={};var defaultFetchOptions={method:"GET",headers:{},timeout:8e3,retry:3,retryDelay:1e3,removeDuplicate:"never",cacheStrategy:"network_only",cacheStorageName:"fetch_cache"};async function fetch(url,options={}){logger_.logMethodArgs?.("fetch",{url,options});const options_=_processOptions(url,options);try{const response=await handleCacheStrategy_(options_);if(!response.ok){throw new FetchError("http_error",`HTTP error! status: ${response.status} ${response.statusText}`,response)}return[response,null]}catch(err){let error;if(err instanceof FetchError){error=err;if(error.response!==void 0&&error.data===void 0){const bodyText=await error.response.text().catch(()=>"");if(bodyText.trim().length>0){try{error.data=JSON.parse(bodyText)}catch{error.data=bodyText}}}}else if(err instanceof Error){if(err.name==="AbortError"){error=new FetchError("aborted",err.message)}else{error=new FetchError("network_error",err.message)}}else{error=new FetchError("unknown_error",String(err??"unknown_error"))}logger_.error("fetch",error.reason,{error});return[null,error]}}function _processOptions(url,options){logger_.logMethodArgs?.("_processOptions",{url,options});const options_={...defaultFetchOptions,...options,url};options_.window??=null;if(options_.removeDuplicate==="auto"){options_.removeDuplicate=cacheSupported?"until_load":"always"}if(options_.url.lastIndexOf("?")===-1&&options_.queryParams!=null){const queryParams=options_.queryParams;const queryArray=Object.keys(queryParams).map(key=>`${encodeURIComponent(key)}=${encodeURIComponent(String(queryParams[key]))}`);if(queryArray.length>0){options_.url+="?"+queryArray.join("&")}}if(options_.bodyJson!==void 0){options_.body=JSON.stringify(options_.bodyJson);options_.headers["content-type"]=MimeTypes.JSON}if(options_.bearerToken!==void 0){options_.headers.authorization=`Bearer ${options_.bearerToken}`}else if(options_.alwatrAuth!==void 0){options_.headers.authorization=`Alwatr ${options_.alwatrAuth.userId}:${options_.alwatrAuth.userToken}`}logger_.logProperty?.("fetch.options",options_);return options_}async function handleCacheStrategy_(options){if(options.cacheStrategy==="network_only"){return handleRemoveDuplicate_(options)}logger_.logMethod?.("handleCacheStrategy_");if(!cacheSupported){logger_.incident?.("fetch","fetch_cache_strategy_unsupported",{cacheSupported});options.cacheStrategy="network_only";return handleRemoveDuplicate_(options)}const cacheStorage=await caches.open(options.cacheStorageName);const request=new Request(options.url,options);switch(options.cacheStrategy){case"cache_first":{const cachedResponse=await cacheStorage.match(request);if(cachedResponse!=null){return cachedResponse}const response=await handleRemoveDuplicate_(options);if(response.ok){cacheStorage.put(request,response.clone())}return response}case"cache_only":{const cachedResponse=await cacheStorage.match(request);if(cachedResponse==null){throw new FetchError("cache_not_found","Resource not found in cache")}return cachedResponse}case"network_first":{try{const networkResponse=await handleRemoveDuplicate_(options);if(networkResponse.ok){cacheStorage.put(request,networkResponse.clone())}return networkResponse}catch(err){const cachedResponse=await cacheStorage.match(request);if(cachedResponse!=null){return cachedResponse}throw err}}case"update_cache":{const networkResponse=await handleRemoveDuplicate_(options);if(networkResponse.ok){cacheStorage.put(request,networkResponse.clone())}return networkResponse}case"stale_while_revalidate":{const cachedResponse=await cacheStorage.match(request);const fetchedResponsePromise=handleRemoveDuplicate_(options).then(networkResponse=>{if(networkResponse.ok){cacheStorage.put(request,networkResponse.clone());if(typeof options.revalidateCallback==="function"){setTimeout(options.revalidateCallback,0,networkResponse.clone())}}return networkResponse});return cachedResponse??fetchedResponsePromise}default:{return handleRemoveDuplicate_(options)}}}async function handleRemoveDuplicate_(options){if(options.removeDuplicate==="never"){return handleRetryPattern_(options)}logger_.logMethod?.("handleRemoveDuplicate_");const bodyString=typeof options.body==="string"?options.body:"";const cacheKey=`${options.method} ${options.url} ${bodyString}`;duplicateRequestStorage_[cacheKey]??=handleRetryPattern_(options);try{const response=await duplicateRequestStorage_[cacheKey];if(duplicateRequestStorage_[cacheKey]!=null){if(response.ok!==true||options.removeDuplicate==="until_load"){delete duplicateRequestStorage_[cacheKey]}}return response.clone()}catch(err){delete duplicateRequestStorage_[cacheKey];throw err}}async function handleRetryPattern_(options){if(!(options.retry>1)){return handleTimeout_(options)}logger_.logMethod?.("handleRetryPattern_");options.retry--;const externalAbortSignal=options.signal;try{const response=await handleTimeout_(options);if(!response.ok&&response.status>=HttpStatusCodes.Error_Server_500_Internal_Server_Error){throw new FetchError("http_error",`HTTP error! status: ${response.status} ${response.statusText}`,response)}return response}catch(err){logger_.accident("fetch","fetch_failed_retry",err);if(globalThis_.navigator?.onLine===false){logger_.accident("handleRetryPattern_","offline","Skip retry because offline");throw err}await delay.by(options.retryDelay);options.signal=externalAbortSignal;return handleRetryPattern_(options)}}function handleTimeout_(options){if(options.timeout===0){return globalThis_.fetch(options.url,options)}logger_.logMethod?.("handleTimeout_");return new Promise((resolved,reject)=>{const abortController=typeof AbortController==="function"?new AbortController:null;const externalAbortSignal=options.signal;options.signal=abortController?.signal;if(abortController!==null&&externalAbortSignal!=null){externalAbortSignal.addEventListener("abort",()=>abortController.abort(),{once:true})}const timeoutId=setTimeout(()=>{reject(new FetchError("timeout","fetch_timeout"));abortController?.abort("fetch_timeout")},parseDuration(options.timeout));globalThis_.fetch(options.url,options).then(response=>resolved(response)).catch(reason=>reject(reason)).finally(()=>{clearTimeout(timeoutId)})})}export{FetchError,cacheSupported,fetch};
|
|
4
4
|
//# sourceMappingURL=main.mjs.map
|
package/dist/main.mjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/main.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * @module @alwatr/fetch\n *\n * An enhanced, lightweight, and dependency-free wrapper for the native `fetch` API.\n * It provides modern features like caching strategies, request retries, timeouts, and\n * duplicate request handling.\n */\n\nimport {delay} from '@alwatr/delay';\nimport {getGlobalThis} from '@alwatr/global-this';\nimport {HttpStatusCodes, MimeTypes} from '@alwatr/http-primer';\nimport {createLogger} from '@alwatr/logger';\nimport {parseDuration} from '@alwatr/parse-duration';\n\nimport type {AlwatrFetchOptions_, FetchOptions} from './type.js';\n\nexport {cacheSupported};\nexport type * from './type.js';\n\nconst logger_ = createLogger('@alwatr/fetch');\nconst globalThis_ = getGlobalThis();\n\n/**\n * A boolean flag indicating whether the browser's Cache API is supported.\n */\nconst cacheSupported = Object.hasOwn(globalThis_, 'caches');\n\n/**\n * A simple in-memory storage for tracking and managing duplicate in-flight requests.\n * The key is a unique identifier for the request (e.g., method + URL + body),\n * and the value is the promise of the ongoing fetch operation.\n */\nconst duplicateRequestStorage_: Record<string, Promise<Response>> = {};\n\n/**\n * Default options for all fetch requests. These can be overridden by passing\n * a custom `options` object to the `fetch` function.\n */\nconst defaultFetchOptions: AlwatrFetchOptions_ = {\n method: 'GET',\n headers: {},\n timeout: 8_000,\n retry: 3,\n retryDelay: 1_000,\n removeDuplicate: 'never',\n cacheStrategy: 'network_only',\n cacheStorageName: 'fetch_cache',\n};\n\n/**\n * Internal-only fetch options type, which includes the URL and ensures all\n * optional properties from AlwatrFetchOptions_ are present.\n */\ntype FetchOptions__ = AlwatrFetchOptions_ & Omit<RequestInit, 'headers'> & {url: string};\n\n/**\n * An enhanced wrapper for the native `fetch` function.\n *\n * This function extends the standard `fetch` with additional features such as:\n * - **Timeout**: Aborts the request if it takes too long.\n * - **Retry Pattern**: Automatically retries the request on failure (e.g., server errors or network issues).\n * - **Duplicate Request Handling**: Prevents sending multiple identical requests in parallel.\n * - **Cache Strategies**: Provides various caching mechanisms using the browser's Cache API.\n * - **Simplified API**: Offers convenient options for adding query parameters, JSON bodies, and auth tokens.\n *\n * @see {@link FetchOptions} for a detailed list of available options.\n *\n * @param {string} url - The URL to fetch.\n * @param {FetchOptions} options - Optional configuration for the fetch request.\n * @returns {Promise<Response>} A promise that resolves to the `Response` object for the request.\n *\n * @example\n * ```typescript\n * async function fetchProducts() {\n * try {\n * const response = await fetch(\"/api/products\", {\n * queryParams: { limit: 10, category: \"electronics\" },\n * timeout: 5_000, // 5 seconds\n * retry: 3,\n * cacheStrategy: \"stale_while_revalidate\",\n * });\n *\n * if (!response.ok) {\n * throw new Error(`HTTP error! status: ${response.status}`);\n * }\n *\n * const data = await response.json();\n * console.log(\"Products:\", data);\n * } catch (error) {\n * console.error(\"Failed to fetch products:\", error);\n * }\n * }\n *\n * fetchProducts();\n * ```\n */\nexport function fetch(url: string, options: FetchOptions): Promise<Response> {\n logger_.logMethodArgs?.('fetch', {url, options});\n\n const options_: FetchOptions__ = {\n ...defaultFetchOptions,\n ...options,\n url,\n };\n\n options_.window ??= null;\n\n if (options_.removeDuplicate === 'auto') {\n options_.removeDuplicate = cacheSupported ? 'until_load' : 'always';\n }\n\n // Append query parameters to the URL if they are provided and the URL doesn't already have them.\n if (options_.url.lastIndexOf('?') === -1 && options_.queryParams != null) {\n const queryParams = options_.queryParams;\n // prettier-ignore\n const queryArray = Object\n .keys(queryParams)\n .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(String(queryParams[key]))}`);\n\n if (queryArray.length > 0) {\n options_.url += '?' + queryArray.join('&');\n }\n }\n\n // If `bodyJson` is provided, stringify it and set the appropriate 'Content-Type' header.\n if (options_.bodyJson !== undefined) {\n options_.body = JSON.stringify(options_.bodyJson);\n options_.headers['content-type'] = MimeTypes.JSON;\n }\n\n // Set the 'Authorization' header for bearer tokens or Alwatr's authentication scheme.\n if (options_.bearerToken !== undefined) {\n options_.headers.authorization = `Bearer ${options_.bearerToken}`;\n }\n else if (options_.alwatrAuth !== undefined) {\n options_.headers.authorization = `Alwatr ${options_.alwatrAuth.userId}:${options_.alwatrAuth.userToken}`;\n }\n\n logger_.logProperty?.('fetch.options', options_);\n\n // Start the fetch lifecycle, beginning with the cache strategy.\n return handleCacheStrategy_(options_);\n}\n\n/**\n * Manages caching strategies for the fetch request.\n * If the strategy is `network_only`, it bypasses caching and proceeds to the next step.\n * Otherwise, it interacts with the browser's Cache API based on the selected strategy.\n *\n * @param {FetchOptions__} options - The fully configured fetch options.\n * @returns {Promise<Response>} A promise resolving to a `Response` object, either from the cache or the network.\n * @private\n */\nasync function handleCacheStrategy_(options: FetchOptions__): Promise<Response> {\n if (options.cacheStrategy === 'network_only') {\n return handleRemoveDuplicate_(options);\n }\n // else\n\n logger_.logMethod?.('handleCacheStrategy_');\n\n if (!cacheSupported) {\n logger_.incident?.('fetch', 'fetch_cache_strategy_unsupported', {\n cacheSupported,\n });\n // Fallback to network_only if Cache API is not available.\n options.cacheStrategy = 'network_only';\n return handleRemoveDuplicate_(options);\n }\n // else\n\n const cacheStorage = await caches.open(options.cacheStorageName);\n\n const request = new Request(options.url, options);\n\n switch (options.cacheStrategy) {\n case 'cache_first': {\n const cachedResponse = await cacheStorage.match(request);\n if (cachedResponse != null) {\n return cachedResponse;\n }\n // else\n\n const response = await handleRemoveDuplicate_(options);\n if (response.ok) {\n cacheStorage.put(request, response.clone());\n }\n return response;\n }\n\n case 'cache_only': {\n const cachedResponse = await cacheStorage.match(request);\n if (cachedResponse == null) {\n logger_.accident('_handleCacheStrategy', 'fetch_cache_not_found', {url: request.url});\n throw new Error('fetch_cache_not_found');\n }\n // else\n\n return cachedResponse;\n }\n\n case 'network_first': {\n try {\n const networkResponse = await handleRemoveDuplicate_(options);\n if (networkResponse.ok) {\n cacheStorage.put(request, networkResponse.clone());\n }\n return networkResponse;\n }\n catch (err) {\n const cachedResponse = await cacheStorage.match(request);\n if (cachedResponse != null) {\n return cachedResponse;\n }\n // else\n\n throw err;\n }\n }\n\n case 'update_cache': {\n const networkResponse = await handleRemoveDuplicate_(options);\n if (networkResponse.ok) {\n cacheStorage.put(request, networkResponse.clone());\n }\n return networkResponse;\n }\n\n case 'stale_while_revalidate': {\n const cachedResponse = await cacheStorage.match(request);\n const fetchedResponsePromise = handleRemoveDuplicate_(options).then((networkResponse) => {\n if (networkResponse.ok) {\n cacheStorage.put(request, networkResponse.clone());\n if (typeof options.revalidateCallback === 'function') {\n setTimeout(options.revalidateCallback, 0, networkResponse.clone());\n }\n }\n return networkResponse;\n });\n\n return cachedResponse ?? fetchedResponsePromise;\n }\n\n default: {\n return handleRemoveDuplicate_(options);\n }\n }\n}\n\n/**\n * Handles duplicate request elimination.\n *\n * It creates a unique key based on the request method, URL, and body. If a request with the\n * same key is already in flight, it returns the promise of the existing request instead of\n * creating a new one. This prevents redundant network calls for identical parallel requests.\n *\n * @param {FetchOptions__} options - The fully configured fetch options.\n * @returns {Promise<Response>} A promise resolving to a cloned `Response` object.\n * @private\n */\nasync function handleRemoveDuplicate_(options: FetchOptions__): Promise<Response> {\n if (options.removeDuplicate === 'never') {\n return handleRetryPattern_(options);\n }\n // else\n\n logger_.logMethod?.('handleRemoveDuplicate_');\n\n // Create a unique key for the request. Including the body is crucial to differentiate\n // between requests to the same URL but with different payloads (e.g., POST requests).\n const bodyString = typeof options.body === 'string' ? options.body : '';\n const cacheKey = `${options.method} ${options.url} ${bodyString}`;\n\n // If a request with the same key doesn't exist, create it and store its promise.\n duplicateRequestStorage_[cacheKey] ??= handleRetryPattern_(options);\n\n try {\n // Await the shared promise to get the response.\n const response = await duplicateRequestStorage_[cacheKey];\n\n // Clean up the stored promise based on the removal strategy.\n if (duplicateRequestStorage_[cacheKey] != null) {\n if (response.ok !== true || options.removeDuplicate === 'until_load') {\n // Remove after completion for 'until_load' or if the request failed.\n delete duplicateRequestStorage_[cacheKey];\n }\n }\n\n // Return a clone of the response, so each caller can consume the body independently.\n return response.clone();\n }\n catch (err) {\n // If the request fails, remove it from storage to allow for retries.\n delete duplicateRequestStorage_[cacheKey];\n throw err;\n }\n}\n\n/**\n * Implements a retry mechanism for the fetch request.\n * If the request fails due to a server error (status >= 500) or a timeout,\n * it will be retried up to the specified number of times.\n *\n * @param {FetchOptions__} options - The fully configured fetch options.\n * @returns {Promise<Response>} A promise that resolves to the final `Response` after all retries.\n * @private\n */\nasync function handleRetryPattern_(options: FetchOptions__): Promise<Response> {\n if (!(options.retry > 1)) {\n return handleTimeout_(options);\n }\n // else\n\n logger_.logMethod?.('handleRetryPattern_');\n options.retry--;\n\n const externalAbortSignal = options.signal;\n\n try {\n const response = await handleTimeout_(options);\n\n // Only retry on server errors (5xx). Client errors (4xx) are not retried.\n if (response.status < HttpStatusCodes.Error_Server_500_Internal_Server_Error) {\n return response;\n }\n // else\n\n throw new Error('fetch_server_error');\n }\n catch (err) {\n logger_.accident('fetch', 'fetch_failed_retry', err);\n\n // Do not retry if the browser is offline.\n if (globalThis_.navigator?.onLine === false) {\n logger_.accident('handleRetryPattern_', 'offline', 'Skip retry because offline');\n throw err;\n }\n\n await delay.by(options.retryDelay);\n\n // Restore the original signal for the next attempt.\n options.signal = externalAbortSignal;\n return handleRetryPattern_(options);\n }\n}\n\n/**\n * Wraps the native fetch call with a timeout mechanism.\n *\n * It uses an `AbortController` to abort the request if it does not complete\n * within the specified `timeout` duration. It also respects external abort signals.\n *\n * @param {FetchOptions__} options - The fully configured fetch options.\n * @returns {Promise<Response>} A promise that resolves with the `Response` or rejects on timeout.\n * @private\n */\nfunction handleTimeout_(options: FetchOptions__): Promise<Response> {\n if (options.timeout === 0) {\n // If timeout is disabled, call fetch directly.\n return globalThis_.fetch(options.url, options);\n }\n\n logger_.logMethod?.('handleTimeout_');\n\n return new Promise((resolved, reject) => {\n const abortController = typeof AbortController === 'function' ? new AbortController() : null;\n const externalAbortSignal = options.signal;\n options.signal = abortController?.signal;\n\n // If an external AbortSignal is provided, listen to it and propagate the abort.\n if (abortController !== null && externalAbortSignal != null) {\n externalAbortSignal.addEventListener('abort', () => abortController.abort(), {once: true});\n }\n\n const timeoutId = setTimeout(() => {\n reject(new Error('fetch_timeout'));\n abortController?.abort('fetch_timeout');\n }, parseDuration(options.timeout!));\n\n globalThis_\n .fetch(options.url, options)\n .then((response) => resolved(response))\n .catch((reason) => reject(reason))\n .finally(() => {\n // Clean up the timeout to prevent it from firing after the request has completed.\n clearTimeout(timeoutId);\n });\n });\n}\n"],
|
|
5
|
-
"mappings": ";;AAQA,OAAQ,UAAY,gBACpB,OAAQ,kBAAoB,sBAC5B,OAAQ,gBAAiB,cAAgB,sBACzC,OAAQ,iBAAmB,iBAC3B,OAAQ,kBAAoB,
|
|
3
|
+
"sources": ["../src/main.ts", "../src/error.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * @module @alwatr/fetch\n *\n * An enhanced, lightweight, and dependency-free wrapper for the native `fetch`\n * API. It provides modern features like caching strategies, request retries,\n * timeouts, and duplicate request handling.\n */\n\nimport {delay} from '@alwatr/delay';\nimport {getGlobalThis} from '@alwatr/global-this';\nimport {hasOwn} from '@alwatr/has-own';\nimport {HttpStatusCodes, MimeTypes} from '@alwatr/http-primer';\nimport {createLogger} from '@alwatr/logger';\nimport {parseDuration} from '@alwatr/parse-duration';\n\nimport {FetchError} from './error.js';\n\nimport type {AlwatrFetchOptions_, FetchOptions, FetchResponse} from './type.js';\n\nexport {cacheSupported};\nexport type * from './type.js';\nexport * from './error.js';\n\nconst logger_ = createLogger('@alwatr/fetch');\nconst globalThis_ = getGlobalThis();\n\n/**\n * A boolean flag indicating whether the browser's Cache API is supported.\n */\nconst cacheSupported = /* #__PURE__ */ hasOwn(globalThis_, 'caches');\n\n/**\n * A simple in-memory storage for tracking and managing duplicate in-flight requests.\n * The key is a unique identifier for the request (e.g., method + URL + body),\n * and the value is the promise of the ongoing fetch operation.\n */\nconst duplicateRequestStorage_: Record<string, Promise<Response>> = {};\n\n/**\n * Default options for all fetch requests. These can be overridden by passing\n * a custom `options` object to the `fetch` function.\n */\nconst defaultFetchOptions: AlwatrFetchOptions_ = {\n method: 'GET',\n headers: {},\n timeout: 8_000,\n retry: 3,\n retryDelay: 1_000,\n removeDuplicate: 'never',\n cacheStrategy: 'network_only',\n cacheStorageName: 'fetch_cache',\n};\n\n/**\n * Internal-only fetch options type, which includes the URL and ensures all\n * optional properties from AlwatrFetchOptions_ are present.\n */\ntype FetchOptions__ = AlwatrFetchOptions_ & Omit<RequestInit, 'headers'> & {url: string};\n\n/**\n * An enhanced wrapper for the native `fetch` function.\n *\n * This function extends the standard `fetch` with additional features such as:\n * - **Timeout**: Aborts the request if it takes too long.\n * - **Retry Pattern**: Automatically retries the request on failure (e.g., server errors or network issues).\n * - **Duplicate Request Handling**: Prevents sending multiple identical requests in parallel.\n * - **Cache Strategies**: Provides various caching mechanisms using the browser's Cache API.\n * - **Simplified API**: Offers convenient options for adding query parameters, JSON bodies, and auth tokens.\n *\n * @see {@link FetchOptions} for a detailed list of available options.\n *\n * @param {string} url - The URL to fetch.\n * @param {FetchOptions} options - Optional configuration for the fetch request.\n * @returns {Promise<FetchResponse>} A promise that resolves to a tuple. On\n * success, it returns `[response, null]`. On failure, it returns `[null,\n * FetchError]`.\n *\n * @example\n * ```typescript\n * import {fetch} from '@alwatr/fetch';\n *\n * async function fetchProducts() {\n * const [response, error] = await fetch('/api/products', {\n * queryParams: { limit: 10 },\n * timeout: 5_000,\n * });\n *\n * if (error) {\n * console.error('Request failed:', error.reason);\n * return;\n * }\n *\n * // At this point, response is guaranteed to be valid and ok.\n * const data = await response.json();\n * console.log('Products:', data);\n * }\n *\n * fetchProducts();\n * ```\n */\nexport async function fetch(url: string, options: FetchOptions = {}): Promise<FetchResponse> {\n logger_.logMethodArgs?.('fetch', {url, options});\n\n const options_ = _processOptions(url, options);\n\n try {\n // Start the fetch lifecycle, beginning with the cache strategy.\n const response = await handleCacheStrategy_(options_);\n\n if (!response.ok) {\n throw new FetchError('http_error', `HTTP error! status: ${response.status} ${response.statusText}`, response);\n }\n\n return [response, null];\n }\n catch (err) {\n let error: FetchError;\n\n if (err instanceof FetchError) {\n error = err;\n\n if (error.response !== undefined && error.data === undefined) {\n const bodyText = await error.response.text().catch(() => '');\n\n if (bodyText.trim().length > 0) {\n try {\n // Try to parse as JSON\n error.data = JSON.parse(bodyText);\n }\n catch {\n error.data = bodyText;\n }\n }\n }\n }\n else if (err instanceof Error) {\n if (err.name === 'AbortError') {\n error = new FetchError('aborted', err.message);\n }\n else {\n error = new FetchError('network_error', err.message);\n }\n }\n else {\n error = new FetchError('unknown_error', String(err ?? 'unknown_error'));\n }\n\n logger_.error('fetch', error.reason, {error});\n return [null, error];\n }\n}\n\n/**\n * Processes and sanitizes the fetch options.\n *\n * @param {string} url - The URL to fetch.\n * @param {FetchOptions} options - The user-provided options.\n * @returns {FetchOptions__} The processed and complete fetch options.\n * @private\n */\nfunction _processOptions(url: string, options: FetchOptions): FetchOptions__ {\n logger_.logMethodArgs?.('_processOptions', {url, options});\n\n const options_: FetchOptions__ = {\n ...defaultFetchOptions,\n ...options,\n url,\n };\n\n options_.window ??= null;\n\n if (options_.removeDuplicate === 'auto') {\n options_.removeDuplicate = cacheSupported ? 'until_load' : 'always';\n }\n\n // Append query parameters to the URL if they are provided and the URL doesn't already have them.\n if (options_.url.lastIndexOf('?') === -1 && options_.queryParams != null) {\n const queryParams = options_.queryParams;\n // prettier-ignore\n const queryArray = Object\n .keys(queryParams)\n .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(String(queryParams[key]))}`);\n\n if (queryArray.length > 0) {\n options_.url += '?' + queryArray.join('&');\n }\n }\n\n // If `bodyJson` is provided, stringify it and set the appropriate 'Content-Type' header.\n if (options_.bodyJson !== undefined) {\n options_.body = JSON.stringify(options_.bodyJson);\n options_.headers['content-type'] = MimeTypes.JSON;\n }\n\n // Set the 'Authorization' header for bearer tokens or Alwatr's authentication scheme.\n if (options_.bearerToken !== undefined) {\n options_.headers.authorization = `Bearer ${options_.bearerToken}`;\n }\n else if (options_.alwatrAuth !== undefined) {\n options_.headers.authorization = `Alwatr ${options_.alwatrAuth.userId}:${options_.alwatrAuth.userToken}`;\n }\n\n logger_.logProperty?.('fetch.options', options_);\n\n return options_;\n}\n\n/**\n * Manages caching strategies for the fetch request.\n * If the strategy is `network_only`, it bypasses caching and proceeds to the next step.\n * Otherwise, it interacts with the browser's Cache API based on the selected strategy.\n *\n * @param {FetchOptions__} options - The fully configured fetch options.\n * @returns {Promise<Response>} A promise resolving to a `Response` object, either from the cache or the network.\n * @private\n */\nasync function handleCacheStrategy_(options: FetchOptions__): Promise<Response> {\n if (options.cacheStrategy === 'network_only') {\n return handleRemoveDuplicate_(options);\n }\n // else\n\n logger_.logMethod?.('handleCacheStrategy_');\n\n if (!cacheSupported) {\n logger_.incident?.('fetch', 'fetch_cache_strategy_unsupported', {\n cacheSupported,\n });\n // Fallback to network_only if Cache API is not available.\n options.cacheStrategy = 'network_only';\n return handleRemoveDuplicate_(options);\n }\n // else\n\n const cacheStorage = await caches.open(options.cacheStorageName);\n\n const request = new Request(options.url, options);\n\n switch (options.cacheStrategy) {\n case 'cache_first': {\n const cachedResponse = await cacheStorage.match(request);\n if (cachedResponse != null) {\n return cachedResponse;\n }\n // else\n\n const response = await handleRemoveDuplicate_(options);\n if (response.ok) {\n cacheStorage.put(request, response.clone());\n }\n return response;\n }\n\n case 'cache_only': {\n const cachedResponse = await cacheStorage.match(request);\n if (cachedResponse == null) {\n throw new FetchError('cache_not_found', 'Resource not found in cache');\n }\n // else\n\n return cachedResponse;\n }\n\n case 'network_first': {\n try {\n const networkResponse = await handleRemoveDuplicate_(options);\n if (networkResponse.ok) {\n cacheStorage.put(request, networkResponse.clone());\n }\n return networkResponse;\n }\n catch (err) {\n const cachedResponse = await cacheStorage.match(request);\n if (cachedResponse != null) {\n return cachedResponse;\n }\n // else\n\n throw err;\n }\n }\n\n case 'update_cache': {\n const networkResponse = await handleRemoveDuplicate_(options);\n if (networkResponse.ok) {\n cacheStorage.put(request, networkResponse.clone());\n }\n return networkResponse;\n }\n\n case 'stale_while_revalidate': {\n const cachedResponse = await cacheStorage.match(request);\n const fetchedResponsePromise = handleRemoveDuplicate_(options).then((networkResponse) => {\n if (networkResponse.ok) {\n cacheStorage.put(request, networkResponse.clone());\n if (typeof options.revalidateCallback === 'function') {\n setTimeout(options.revalidateCallback, 0, networkResponse.clone());\n }\n }\n return networkResponse;\n });\n\n return cachedResponse ?? fetchedResponsePromise;\n }\n\n default: {\n return handleRemoveDuplicate_(options);\n }\n }\n}\n\n/**\n * Handles duplicate request elimination.\n *\n * It creates a unique key based on the request method, URL, and body. If a request with the\n * same key is already in flight, it returns the promise of the existing request instead of\n * creating a new one. This prevents redundant network calls for identical parallel requests.\n *\n * @param {FetchOptions__} options - The fully configured fetch options.\n * @returns {Promise<Response>} A promise resolving to a cloned `Response` object.\n * @private\n */\nasync function handleRemoveDuplicate_(options: FetchOptions__): Promise<Response> {\n if (options.removeDuplicate === 'never') {\n return handleRetryPattern_(options);\n }\n // else\n\n logger_.logMethod?.('handleRemoveDuplicate_');\n\n // Create a unique key for the request. Including the body is crucial to differentiate\n // between requests to the same URL but with different payloads (e.g., POST requests).\n const bodyString = typeof options.body === 'string' ? options.body : '';\n const cacheKey = `${options.method} ${options.url} ${bodyString}`;\n\n // If a request with the same key doesn't exist, create it and store its promise.\n duplicateRequestStorage_[cacheKey] ??= handleRetryPattern_(options);\n\n try {\n // Await the shared promise to get the response.\n const response = await duplicateRequestStorage_[cacheKey];\n\n // Clean up the stored promise based on the removal strategy.\n if (duplicateRequestStorage_[cacheKey] != null) {\n if (response.ok !== true || options.removeDuplicate === 'until_load') {\n // Remove after completion for 'until_load' or if the request failed.\n delete duplicateRequestStorage_[cacheKey];\n }\n }\n\n // Return a clone of the response, so each caller can consume the body independently.\n return response.clone();\n }\n catch (err) {\n // If the request fails, remove it from storage to allow for retries.\n delete duplicateRequestStorage_[cacheKey];\n throw err;\n }\n}\n\n/**\n * Implements a retry mechanism for the fetch request.\n * If the request fails due to a server error (status >= 500) or a timeout,\n * it will be retried up to the specified number of times.\n *\n * @param {FetchOptions__} options - The fully configured fetch options.\n * @returns {Promise<Response>} A promise that resolves to the final `Response` after all retries.\n * @private\n */\nasync function handleRetryPattern_(options: FetchOptions__): Promise<Response> {\n if (!(options.retry > 1)) {\n return handleTimeout_(options);\n }\n // else\n\n logger_.logMethod?.('handleRetryPattern_');\n options.retry--;\n\n const externalAbortSignal = options.signal;\n\n try {\n const response = await handleTimeout_(options);\n\n if (!response.ok && response.status >= HttpStatusCodes.Error_Server_500_Internal_Server_Error) {\n // only retry for server errors (5xx)\n throw new FetchError('http_error', `HTTP error! status: ${response.status} ${response.statusText}`, response);\n }\n\n return response;\n }\n catch (err) {\n logger_.accident('fetch', 'fetch_failed_retry', err);\n\n // Do not retry if the browser is offline.\n if (globalThis_.navigator?.onLine === false) {\n logger_.accident('handleRetryPattern_', 'offline', 'Skip retry because offline');\n throw err;\n }\n\n await delay.by(options.retryDelay);\n\n // Restore the original signal for the next attempt.\n options.signal = externalAbortSignal;\n return handleRetryPattern_(options);\n }\n}\n\n/**\n * Wraps the native fetch call with a timeout mechanism.\n *\n * It uses an `AbortController` to abort the request if it does not complete\n * within the specified `timeout` duration. It also respects external abort signals.\n *\n * @param {FetchOptions__} options - The fully configured fetch options.\n * @returns {Promise<Response>} A promise that resolves with the `Response` or rejects on timeout.\n * @private\n */\nfunction handleTimeout_(options: FetchOptions__): Promise<Response> {\n if (options.timeout === 0) {\n // If timeout is disabled, call fetch directly.\n return globalThis_.fetch(options.url, options);\n }\n\n logger_.logMethod?.('handleTimeout_');\n\n return new Promise((resolved, reject) => {\n const abortController = typeof AbortController === 'function' ? new AbortController() : null;\n const externalAbortSignal = options.signal;\n options.signal = abortController?.signal;\n\n // If an external AbortSignal is provided, listen to it and propagate the abort.\n if (abortController !== null && externalAbortSignal != null) {\n externalAbortSignal.addEventListener('abort', () => abortController.abort(), {once: true});\n }\n\n const timeoutId = setTimeout(() => {\n reject(new FetchError('timeout', 'fetch_timeout'));\n abortController?.abort('fetch_timeout');\n }, parseDuration(options.timeout!));\n\n globalThis_\n .fetch(options.url, options)\n .then((response) => resolved(response))\n .catch((reason) => reject(reason))\n .finally(() => {\n // Clean up the timeout to prevent it from firing after the request has completed.\n clearTimeout(timeoutId);\n });\n });\n}\n", "import type { FetchErrorReason } from \"./type.js\";\n\n/**\n * Custom error class for fetch-related failures.\n *\n * This error is thrown when a fetch request fails, either due to a network issue\n * or an HTTP error status (i.e., `response.ok` is `false`). It enriches the\n * standard `Error` object with the `response` and the parsed `data` from the\n * response body, allowing for more detailed error handling.\n *\n * @example\n * ```typescript\n * const [response, error] = await fetch('/api/endpoint');\n * if (error) {\n * console.error(`Request failed with status ${error.response?.status}`);\n * console.error('Server response:', error.data);\n * }\n * ```\n */\nexport class FetchError extends Error {\n /**\n * The original `Response` object.\n * This is useful for accessing headers and other response metadata.\n * It will be `undefined` for non-HTTP errors like network failures or timeouts.\n */\n public response?: Response;\n\n /**\n * The parsed body of the error response, typically a JSON object.\n * It will be `undefined` for non-HTTP errors.\n */\n public data?: JsonObject | string;\n\n /**\n * The specific reason for the fetch failure.\n */\n public reason: FetchErrorReason;\n\n constructor(reason: FetchErrorReason, message: string, response?: Response, data?: JsonObject | string) {\n super(message);\n this.name = 'FetchError';\n this.reason = reason;\n this.response = response;\n this.data = data;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/type.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { FetchError } from './error.js';
|
|
1
2
|
import type { HttpMethod, HttpRequestHeaders } from '@alwatr/http-primer';
|
|
2
3
|
import type { Duration } from '@alwatr/parse-duration';
|
|
3
4
|
/**
|
|
@@ -100,4 +101,19 @@ export interface AlwatrFetchOptions_ {
|
|
|
100
101
|
* Combined type for fetch options, including standard RequestInit properties.
|
|
101
102
|
*/
|
|
102
103
|
export type FetchOptions = Partial<AlwatrFetchOptions_> & Omit<RequestInit, 'headers'>;
|
|
104
|
+
/**
|
|
105
|
+
* Represents the tuple returned by the fetch function.
|
|
106
|
+
* On success, it's `[Response, null]`. On failure, it's `[null, FetchError]`.
|
|
107
|
+
*/
|
|
108
|
+
export type FetchResponse = Promise<[Response, null] | [null, FetchError]>;
|
|
109
|
+
/**
|
|
110
|
+
* Defines the specific reason for a fetch failure.
|
|
111
|
+
* - `http_error`: An HTTP error status was received (e.g., 404, 500).
|
|
112
|
+
* - `timeout`: The request was aborted due to a timeout.
|
|
113
|
+
* - `cache_not_found`: The requested resource was not found in the cache_only strategy.
|
|
114
|
+
* - `network_error`: A generic network-level error occurred.
|
|
115
|
+
* - `aborted`: The request was aborted by a user-provided signal.
|
|
116
|
+
* - `unknown_error`: An unspecified error occurred.
|
|
117
|
+
*/
|
|
118
|
+
export type FetchErrorReason = 'http_error' | 'cache_not_found' | 'timeout' | 'network_error' | 'aborted' | 'unknown_error';
|
|
103
119
|
//# sourceMappingURL=type.d.ts.map
|
package/dist/type.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../src/type.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAE,kBAAkB,EAAC,MAAM,qBAAqB,CAAC;AACxE,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../src/type.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAC,UAAU,EAAE,kBAAkB,EAAC,MAAM,qBAAqB,CAAC;AACxE,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,wBAAwB,CAAC;AAGrD;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,aAAa,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;AAEnE;;;;;;;;;GASG;AACH,MAAM,MAAM,aAAa,GAAG,cAAc,GAAG,eAAe,GAAG,YAAY,GAAG,aAAa,GAAG,cAAc,GAAG,wBAAwB,CAAC;AAExI;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,QAAQ,GAAG,YAAY,GAAG,MAAM,CAAC;AAExE;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,MAAM,EAAE,UAAU,CAAC;IAEnB;;OAEG;IACH,OAAO,EAAE,kBAAkB,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IAEpD;;;;OAIG;IACH,OAAO,EAAE,QAAQ,CAAC;IAElB;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,UAAU,EAAE,QAAQ,CAAC;IAErB;;;;OAIG;IACH,eAAe,EAAE,cAAc,CAAC;IAEhC;;;;OAIG;IACH,aAAa,EAAE,aAAa,CAAC;IAE7B;;OAEG;IACH,kBAAkB,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAElE;;;OAGG;IACH,gBAAgB,EAAE,MAAM,CAAC;IAEzB;;;OAGG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IAErB;;OAEG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;IAE1B;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,UAAU,CAAC,EAAE;QACX,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,mBAAmB,CAAC,GAAG,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;AAEvF;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC;AAE3E;;;;;;;;GAQG;AACH,MAAM,MAAM,gBAAgB,GAAG,YAAY,GAAG,iBAAiB,GAAG,SAAS,GAAG,eAAe,GAAG,SAAS,GAAG,eAAe,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alwatr/fetch",
|
|
3
3
|
"description": "`@alwatr/fetch` is an enhanced, lightweight, and dependency-free wrapper for the native `fetch` API. It provides modern features like caching strategies, request retries, timeouts, and intelligent duplicate request handling, all in a compact package.",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "7.0.0",
|
|
5
5
|
"author": "S. Ali Mihandoost <ali.mihandoost@gmail.com>",
|
|
6
6
|
"bugs": "https://github.com/Alwatr/nanolib/issues",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@alwatr/delay": "6.0.
|
|
9
|
-
"@alwatr/global-this": "5.6.
|
|
10
|
-
"@alwatr/
|
|
11
|
-
"@alwatr/
|
|
12
|
-
"@alwatr/
|
|
8
|
+
"@alwatr/delay": "6.0.13",
|
|
9
|
+
"@alwatr/global-this": "5.6.2",
|
|
10
|
+
"@alwatr/has-own": "5.6.0",
|
|
11
|
+
"@alwatr/http-primer": "6.0.13",
|
|
12
|
+
"@alwatr/logger": "6.0.10",
|
|
13
|
+
"@alwatr/parse-duration": "5.5.22"
|
|
13
14
|
},
|
|
14
15
|
"devDependencies": {
|
|
15
|
-
"@alwatr/nano-build": "6.3.
|
|
16
|
+
"@alwatr/nano-build": "6.3.6",
|
|
16
17
|
"@alwatr/prettier-config": "5.0.5",
|
|
17
18
|
"@alwatr/tsconfig-base": "6.0.3",
|
|
18
19
|
"@alwatr/type-helper": "6.1.5",
|
|
20
|
+
"@jest/globals": "^30.2.0",
|
|
19
21
|
"jest": "^30.2.0",
|
|
20
22
|
"typescript": "^5.9.3"
|
|
21
23
|
},
|
|
@@ -86,5 +88,5 @@
|
|
|
86
88
|
"sideEffects": false,
|
|
87
89
|
"type": "module",
|
|
88
90
|
"types": "./dist/main.d.ts",
|
|
89
|
-
"gitHead": "
|
|
91
|
+
"gitHead": "211a46c74acca97bf4b570b20c6dffbdf8bcccfe"
|
|
90
92
|
}
|