swimple 0.1.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/LICENSE +21 -0
- package/README.md +714 -0
- package/helpers.js +304 -0
- package/index.js +256 -0
- package/package.json +38 -0
- package/types.d.ts +29 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wes@goulet.dev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
# swimple
|
|
2
|
+
|
|
3
|
+
A simple service worker library for request caching.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ⥠**Low Configuration**: Request caching with automatic invalidation by default - minimal setup required
|
|
8
|
+
- ð **Smart Invalidation**: Automatically invalidate cache on mutations (POST/PATCH/PUT/DELETE)
|
|
9
|
+
- ðïļ **Flexible Strategies**: Support for cache-first, network-first, and stale-while-revalidate
|
|
10
|
+
- ðŠķ **Lightweight**: Single-purpose library with no dependencies
|
|
11
|
+
- ð **Modern**: Designed for ES module service workers (supported by all modern browsers as of January 2026)
|
|
12
|
+
- ð§ **Configurable**: Automatic caching by default, with per-request control
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
### Direct CDN Import (Recommended)
|
|
17
|
+
|
|
18
|
+
```javascript
|
|
19
|
+
// sw.js
|
|
20
|
+
import { createHandleRequest } from "https://cdn.jsdelivr.net/npm/swimple@1.0.0/index.js";
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### NPM Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install swimple
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
// sw.js
|
|
31
|
+
import { createHandleRequest } from "swimple";
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
> NOTE: This documentation focuses on modern browsers with module service workers support. If you aren't using module service workers, you can probably still use this with `importScripts` but I'll leave that to you to figure out.
|
|
37
|
+
|
|
38
|
+
### 1. Register Your Service Worker
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
// in javascript in index.html (or app.js or main.js or whatever frameworks use nowadays)
|
|
42
|
+
try {
|
|
43
|
+
await navigator.serviceWorker.register("/sw.js", { type: "module" });
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error("Error registering module service worker:", error);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2. Set Up Request Handler in Service Worker
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
// sw.js
|
|
53
|
+
import { createHandleRequest } from "https://cdn.jsdelivr.net/npm/swimple@1.0.0/index.js";
|
|
54
|
+
|
|
55
|
+
// create the request handler
|
|
56
|
+
const handleRequest = createHandleRequest({
|
|
57
|
+
cacheName: "api-cache-v1",
|
|
58
|
+
scope: ["/api/"] // this means only GET requests that start with /api/ will be cached
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
self.addEventListener("fetch", (event) => {
|
|
62
|
+
const response = handleRequest(event);
|
|
63
|
+
|
|
64
|
+
if (response) {
|
|
65
|
+
event.respondWith(response);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// fall through to other logic or just let the request go to network by doing nothing
|
|
70
|
+
return;
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
That's it! Your service worker will now cache GET requests that start with `/api/` using the default TTL of 300 seconds. Subsequent requests to the same path will return from the cache if the response is fresh (within the TTL). Any mutations (POST/PATCH/PUT/DELETE) will invalidate the cache. For example a PATCH request to `/api/users/123` will invalidate the cache for `/api/users/123` (ie: the details of that user) and `/api/users` (ie: the list of users that likely includes that user).
|
|
75
|
+
|
|
76
|
+
Any requests outside the scope (ie: not starting with `/api/`) won't be processed by the cache handler (it will return null).
|
|
77
|
+
|
|
78
|
+
## Configuring the request handler
|
|
79
|
+
|
|
80
|
+
If you want to use different defaults for all requests, you can pass in a config object to the `createHandleRequest` function.
|
|
81
|
+
|
|
82
|
+
### Example 1: Network-First Strategy
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
const handleRequest = createHandleRequest({
|
|
86
|
+
cacheName: "api-cache-v1",
|
|
87
|
+
scope: ["/api/"],
|
|
88
|
+
defaultStrategy: "network-first"
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
With network-first, requests always try the network first (even if cached and fresh). If the network fails and you're offline, it will return a cached response (if available and within the fresh or stale TTL). The cache is updated in the background when the network succeeds. This is useful when you want the latest data from the network when online, but still work offline with cached data.
|
|
93
|
+
|
|
94
|
+
### Example 2: Stale-While-Revalidate Strategy
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
const handleRequest = createHandleRequest({
|
|
98
|
+
cacheName: "api-cache-v1",
|
|
99
|
+
scope: ["/api/"],
|
|
100
|
+
defaultStrategy: "stale-while-revalidate"
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
With stale-while-revalidate, requests return from cache immediately if available. If the cache is fresh (within 300 seconds), it's returned without updating. If the cache is stale (past the fresh TTL but within the stale TTL of 3600 seconds), the stale response is returned immediately and the cache is updated in the background. If the cache is too stale (past the stale TTL) or missing, it fetches from network. This provides instant responses while keeping data fresh in the background.
|
|
105
|
+
|
|
106
|
+
### Example 3: Skip Inferring Invalidations
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
const handleRequest = createHandleRequest({
|
|
110
|
+
cacheName: "api-cache-v1",
|
|
111
|
+
scope: ["/api/"],
|
|
112
|
+
inferInvalidation: false
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
With inferInvalidation set to false, the library will not automatically invalidate the cache on mutation requests. You will need to manually invalidate the cache on mutation requests using the `X-SW-Cache-Invalidate` header.
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
fetch("/api/users/123", {
|
|
120
|
+
method: "PATCH",
|
|
121
|
+
headers: {
|
|
122
|
+
"X-SW-Cache-Invalidate": "/api/users/123"
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
If you need to invalidate multiple paths at once, you can set the `X-SW-Cache-Invalidate` header multiple times in a single request.
|
|
128
|
+
|
|
129
|
+
```javascript
|
|
130
|
+
const headers = new Headers();
|
|
131
|
+
headers.append("X-SW-Cache-Invalidate", "/api/users");
|
|
132
|
+
headers.append("X-SW-Cache-Invalidate", "/api/users/123");
|
|
133
|
+
|
|
134
|
+
fetch("/api/users/123", {
|
|
135
|
+
method: "PATCH",
|
|
136
|
+
headers
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Example 4: Custom Fetch Function
|
|
141
|
+
|
|
142
|
+
You can provide a custom fetch function to intercept and modify requests/responses. This is useful for handling authentication errors (e.g., 401/403 responses) or adding custom headers to all requests.
|
|
143
|
+
|
|
144
|
+
```javascript
|
|
145
|
+
const handleRequest = createHandleRequest({
|
|
146
|
+
cacheName: "api-cache-v1",
|
|
147
|
+
scope: ["/api/"],
|
|
148
|
+
customFetch: async (request) => {
|
|
149
|
+
const response = await fetch(request);
|
|
150
|
+
|
|
151
|
+
// Handle authentication errors
|
|
152
|
+
if (response.status === 401 || response.status === 403) {
|
|
153
|
+
// redirect to login page
|
|
154
|
+
// ...
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return response;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The `customFetch` function has the same signature as the native `fetch` function. It will be used for all network requests made by the cache handler.
|
|
163
|
+
|
|
164
|
+
## Clearing the cache on logout
|
|
165
|
+
|
|
166
|
+
It can be useful to clear the cache on logout or other events. You can do this by setting the `X-SW-Cache-Clear` header on a request (any value will work - the header's presence triggers cache clearing).
|
|
167
|
+
|
|
168
|
+
```javascript
|
|
169
|
+
fetch("/api/logout", {
|
|
170
|
+
method: "POST",
|
|
171
|
+
headers: {
|
|
172
|
+
"X-SW-Cache-Clear": "true" // Any value works - header presence triggers cache clear
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
This will clear the entire cache for the cache name specified in the request handler configuration.
|
|
178
|
+
|
|
179
|
+
## Automatic Cache Cleanup
|
|
180
|
+
|
|
181
|
+
The library automatically cleans up cache entries that are older than `maxCacheAgeSeconds` (defaults to 7200 seconds). This prevents unbounded cache growth.
|
|
182
|
+
|
|
183
|
+
Cleanup happens in two ways:
|
|
184
|
+
|
|
185
|
+
1. **Reactive cleanup**: When a cached entry is accessed and found to be older than `maxCacheAgeSeconds`, it's immediately deleted.
|
|
186
|
+
2. **Periodic cleanup**: Every 100 fetches, the library scans the cache and removes all entries older than `maxCacheAgeSeconds`.
|
|
187
|
+
|
|
188
|
+
If the service worker restarts (which can happen at any time), cleanup runs again on the first fetch after restart, ensuring cleanup happens even if the service worker restarts frequently.
|
|
189
|
+
|
|
190
|
+
### Manual Cleanup
|
|
191
|
+
|
|
192
|
+
You can also manually trigger cleanup in your service worker's `activate` handler:
|
|
193
|
+
|
|
194
|
+
```javascript
|
|
195
|
+
// sw.js
|
|
196
|
+
import { cleanupOldCacheEntries } from "https://cdn.jsdelivr.net/npm/swimple@1.0.0/index.js";
|
|
197
|
+
|
|
198
|
+
self.addEventListener("activate", (event) => {
|
|
199
|
+
event.waitUntil(
|
|
200
|
+
cleanupOldCacheEntries("api-cache-v1", 7200) // cacheName, maxAgeSeconds
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Understanding Fresh vs Stale TTL
|
|
206
|
+
|
|
207
|
+
- **Fresh TTL** (`defaultTTLSeconds`): Responses within this time are considered "fresh". For `cache-first` and `stale-while-revalidate` strategies, fresh responses are returned from cache without background network updates.
|
|
208
|
+
|
|
209
|
+
- **Stale TTL** (`defaultStaleTTLSeconds`): Responses past the fresh TTL but within the stale TTL are considered "stale". Stale responses can still be returned from cache:
|
|
210
|
+
- For `cache-first`: Used as a fallback when offline
|
|
211
|
+
- For `network-first`: Used as a fallback when offline
|
|
212
|
+
- For `stale-while-revalidate`: Returned immediately while updating in the background
|
|
213
|
+
|
|
214
|
+
If a response is past the stale TTL (or no stale TTL is set), it's too stale and must be fetched from the network.
|
|
215
|
+
|
|
216
|
+
## API Reference
|
|
217
|
+
|
|
218
|
+
### `createHandleRequest(config)`
|
|
219
|
+
|
|
220
|
+
Creates a request handler function for your service worker fetch handler.
|
|
221
|
+
|
|
222
|
+
#### Configuration Options
|
|
223
|
+
|
|
224
|
+
| Option | Type | Required | Default | Description |
|
|
225
|
+
| ------------------------ | ---------- | -------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
226
|
+
| `cacheName` | `string` | Yes | - | Name of the cache, used when calling `Cache.open(cacheName)` internally. Changing this name effectively clears the previous cache entries. |
|
|
227
|
+
| `scope` | `string[]` | No | `undefined` | URL prefixes to cache by default (e.g., `['/api/']`). If not set and `defaultTTLSeconds` is set, all same-origin GET requests are cached automatically. If not set and `defaultTTLSeconds` is not set (or 0), no requests are cached by default. Individual requests outside the scope can still enable caching with `X-SW-Cache-TTL-Seconds` header. |
|
|
228
|
+
| `defaultStrategy` | `string` | No | `'cache-first'` | Default caching strategy: `'cache-first'`, `'network-first'`, or `'stale-while-revalidate'`. |
|
|
229
|
+
| `defaultTTLSeconds` | `number` | No | `300` | Maximum age for fresh content. Fresh content will be returned from cache for cache-first and stale-while-revalidate strategies, and also from network-first when offline. Fresh content does not get updated from the network. Since this defaults to `300`, caching is automatic by default for GET requests matching the scope. Set to `0` or `undefined` to disable automatic caching (individual requests can still enable caching with `X-SW-Cache-TTL-Seconds` header). |
|
|
230
|
+
| `defaultStaleTTLSeconds` | `number` | No | `3600` | Maximum age for stale content. Stale content will be returned from cache for cache-first (when offline), network-first (when offline), and stale-while-revalidate strategies. That means responses past the fresh TTL but within stale TTL can still be returned from cache. Stale content does get updated from the network. |
|
|
231
|
+
| `inferInvalidation` | `boolean` | No | `true` | Automatically invalidate cache on POST/PATCH/PUT/DELETE requests. |
|
|
232
|
+
| `customFetch` | `function` | No | `fetch` | Custom fetch function to use for network requests. Receives a `Request` object and must return a `Promise<Response>`. Useful for handling authentication errors (401/403) or adding custom headers to all requests. |
|
|
233
|
+
| `maxCacheAgeSeconds` | `number` | No | `7200` | Maximum age (in seconds) before cache entries are automatically cleaned up. Entries older than this age are deleted. Defaults to 7200 seconds (2 hours, which is 2x the default stale TTL). Cache entries are cleaned up reactively (when accessed) and periodically (every 100 fetches). |
|
|
234
|
+
|
|
235
|
+
#### Returns
|
|
236
|
+
|
|
237
|
+
A request handler function `(event: FetchEvent) => Promise<Response> | null` that:
|
|
238
|
+
|
|
239
|
+
- Returns a `Promise<Response>` if the request is handled by the cache handler
|
|
240
|
+
- Returns `null` if the request should fall through to other handlers (e.g., when `X-SW-Cache-TTL-Seconds` is set to `0`, or when the request doesn't match the configured scope and has no TTL header)
|
|
241
|
+
|
|
242
|
+
## HTTP Headers
|
|
243
|
+
|
|
244
|
+
Control caching behavior on a per-request basis using these headers:
|
|
245
|
+
|
|
246
|
+
### `X-SW-Cache-Strategy`
|
|
247
|
+
|
|
248
|
+
Override the default caching strategy for a specific request.
|
|
249
|
+
|
|
250
|
+
**Values:**
|
|
251
|
+
|
|
252
|
+
- `cache-first` - Return from cache if fresh (within TTL), otherwise fetch from network immediately. Stale cache is only used when offline (network request fails). No background updates.
|
|
253
|
+
- `network-first` - Try network first, fall back to stale cache if offline (within stale TTL). Cache updated when network succeeds.
|
|
254
|
+
- `stale-while-revalidate` - Return from cache immediately (fresh = no update, stale = update in background). Fetch from network if too stale or missing.
|
|
255
|
+
|
|
256
|
+
**Example:**
|
|
257
|
+
|
|
258
|
+
```javascript
|
|
259
|
+
fetch("/api/users", {
|
|
260
|
+
headers: {
|
|
261
|
+
"X-SW-Cache-Strategy": "network-first"
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### `X-SW-Cache-TTL-Seconds`
|
|
267
|
+
|
|
268
|
+
Set the time-to-live (in seconds) used to validate a cached response when it's requested. The TTL is not stored with the cached response; only the timestamp of when it was cached is stored. Each request can specify a different TTL to use for validation.
|
|
269
|
+
|
|
270
|
+
**Example:**
|
|
271
|
+
|
|
272
|
+
```javascript
|
|
273
|
+
fetch("/api/users", {
|
|
274
|
+
headers: {
|
|
275
|
+
"X-SW-Cache-TTL-Seconds": "600" // Cache for 10 minutes
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
To completely opt out of caching for a specific request (the handler will return `null` and not process the request at all):
|
|
281
|
+
|
|
282
|
+
```javascript
|
|
283
|
+
fetch("/api/random", {
|
|
284
|
+
headers: {
|
|
285
|
+
"X-SW-Cache-TTL-Seconds": "0" // Handler returns null
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**Important:** Setting TTL to `0` is a complete opt-out. The handler returns `null` immediately without checking cache, making network requests, or processing the request in any way.
|
|
291
|
+
|
|
292
|
+
### `X-SW-Cache-Stale-TTL-Seconds`
|
|
293
|
+
|
|
294
|
+
Maximum age before a cache entry is considered too stale to use. Used by `cache-first` (when offline), `network-first` (when offline) and `stale-while-revalidate` strategies.
|
|
295
|
+
|
|
296
|
+
**How it works:**
|
|
297
|
+
|
|
298
|
+
- **Fresh** (within TTL): Return from cache without background update
|
|
299
|
+
- **Stale** (past TTL but within stale TTL): Return from cache and update in background (or use as offline fallback for cache-first and network-first)
|
|
300
|
+
- **Too stale** (past stale TTL): Must fetch from network, not returned from cache
|
|
301
|
+
|
|
302
|
+
**Example:**
|
|
303
|
+
|
|
304
|
+
```javascript
|
|
305
|
+
fetch("/api/users", {
|
|
306
|
+
headers: {
|
|
307
|
+
"X-SW-Cache-Strategy": "stale-while-revalidate",
|
|
308
|
+
"X-SW-Cache-TTL-Seconds": "300", // Fresh for 5 minutes
|
|
309
|
+
"X-SW-Cache-Stale-TTL-Seconds": "3600" // Usable for 1 hour total
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**Timeline:**
|
|
315
|
+
|
|
316
|
+
- 0-300s: Return from cache (fresh) - no background update
|
|
317
|
+
- 300-3600s: Return from cache (stale) - update in background
|
|
318
|
+
- 3600s+: Cache too stale - fetch from network
|
|
319
|
+
|
|
320
|
+
### `X-SW-Cache-Invalidate`
|
|
321
|
+
|
|
322
|
+
Explicitly invalidate specific cache entries. Can be set multiple times for multiple paths.
|
|
323
|
+
|
|
324
|
+
**Important:** When `X-SW-Cache-Invalidate` headers are present, they **take precedence** over automatically inferred invalidation paths. If headers are provided, only the header-specified paths are invalidated - inferred paths are not added. This allows you to have fine-grained control over invalidation even when `inferInvalidation: true`.
|
|
325
|
+
|
|
326
|
+
**Example:**
|
|
327
|
+
|
|
328
|
+
```javascript
|
|
329
|
+
const headers = new Headers();
|
|
330
|
+
headers.append("X-SW-Cache-Invalidate", "/api/users");
|
|
331
|
+
headers.append("X-SW-Cache-Invalidate", "/api/users/123");
|
|
332
|
+
headers.append("X-SW-Cache-Invalidate", "/api/teams");
|
|
333
|
+
|
|
334
|
+
fetch("/api/users/123", {
|
|
335
|
+
method: "PATCH",
|
|
336
|
+
headers,
|
|
337
|
+
body: JSON.stringify(userData)
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
**Example: Headers override inferred paths**
|
|
342
|
+
|
|
343
|
+
```javascript
|
|
344
|
+
// PATCH /api/users/123 with inferInvalidation: true
|
|
345
|
+
// Without header, would invalidate: /api/users/123 AND /api/users
|
|
346
|
+
// With header, only invalidates: /api/users (header takes precedence)
|
|
347
|
+
|
|
348
|
+
fetch("/api/users/123", {
|
|
349
|
+
method: "PATCH",
|
|
350
|
+
headers: {
|
|
351
|
+
"X-SW-Cache-Invalidate": "/api/users" // Only this path is invalidated
|
|
352
|
+
},
|
|
353
|
+
body: JSON.stringify(userData)
|
|
354
|
+
});
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### `X-SW-Cache-Clear`
|
|
358
|
+
|
|
359
|
+
Clear the entire cache. Typically used on logout to remove all user-specific cached data.
|
|
360
|
+
|
|
361
|
+
**Important:** If this header is present (regardless of its value), the cache will be cleared.
|
|
362
|
+
|
|
363
|
+
**Important behavior:**
|
|
364
|
+
|
|
365
|
+
- When this header is present, the handler **always goes to network** - it does not check cache or return cached values
|
|
366
|
+
- Stale responses are **never used**, even if offline
|
|
367
|
+
- If the network request fails (e.g., offline), the error **bubbles up** to the caller
|
|
368
|
+
- The cache is **always cleared** before making the network request, regardless of whether the network request succeeds or fails
|
|
369
|
+
|
|
370
|
+
**Example:**
|
|
371
|
+
|
|
372
|
+
```javascript
|
|
373
|
+
fetch("/api/logout", {
|
|
374
|
+
method: "POST",
|
|
375
|
+
headers: {
|
|
376
|
+
"X-SW-Cache-Clear": "true" // Any value works - header presence triggers cache clear
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**Note:** While this header is typically used on mutation requests (POST/PATCH/PUT/DELETE), if used on a GET request, it will still clear the cache and attempt to fetch from network. If the network fails, the error will propagate to the caller.
|
|
382
|
+
|
|
383
|
+
## Caching Behavior
|
|
384
|
+
|
|
385
|
+
### Automatic Caching (Default)
|
|
386
|
+
|
|
387
|
+
Since `defaultTTLSeconds` defaults to `300`, caching is automatic by default. All GET requests matching the configured scope are cached automatically.
|
|
388
|
+
|
|
389
|
+
```javascript
|
|
390
|
+
const handleRequest = createHandleRequest({
|
|
391
|
+
cacheName: "api-cache-v1",
|
|
392
|
+
scope: ["/api/"]
|
|
393
|
+
// defaultTTLSeconds defaults to 300, so all /api/* GETs cached automatically
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Automatically cached with default TTL of 300 seconds
|
|
397
|
+
fetch("/api/users");
|
|
398
|
+
|
|
399
|
+
// Completely opt out of caching for specific requests (handler returns null)
|
|
400
|
+
fetch("/api/random", {
|
|
401
|
+
headers: {
|
|
402
|
+
"X-SW-Cache-TTL-Seconds": "0"
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Disabling Automatic Caching
|
|
408
|
+
|
|
409
|
+
To disable automatic caching, set `defaultTTLSeconds` to `0` or `undefined`. Individual requests can still enable caching with the `X-SW-Cache-TTL-Seconds` header.
|
|
410
|
+
|
|
411
|
+
```javascript
|
|
412
|
+
const handleRequest = createHandleRequest({
|
|
413
|
+
cacheName: "api-cache-v1",
|
|
414
|
+
scope: ["/api/"],
|
|
415
|
+
defaultTTLSeconds: 0 // Disable automatic caching
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Must explicitly enable caching per request
|
|
419
|
+
fetch("/api/users", {
|
|
420
|
+
headers: {
|
|
421
|
+
"X-SW-Cache-TTL-Seconds": "300"
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
## Automatic Cache Invalidation
|
|
427
|
+
|
|
428
|
+
When `inferInvalidation: true` (default), the library automatically invalidates relevant cache entries on mutation requests:
|
|
429
|
+
|
|
430
|
+
| Request | Invalidates |
|
|
431
|
+
| ----------------------- | ----------------------------------------- |
|
|
432
|
+
| `POST /api/users` | `GET /api/users` |
|
|
433
|
+
| `PATCH /api/users/123` | `GET /api/users/123` AND `GET /api/users` |
|
|
434
|
+
| `PUT /api/users/123` | `GET /api/users/123` AND `GET /api/users` |
|
|
435
|
+
| `DELETE /api/users/123` | `GET /api/users/123` AND `GET /api/users` |
|
|
436
|
+
|
|
437
|
+
The library strips the last path segment to find the collection endpoint. This works for most REST API patterns, but may not handle all edge cases (e.g., nested resources like `/api/users/123/avatar`). For edge cases, you can manually specify invalidation paths using the `X-SW-Cache-Invalidate` header.
|
|
438
|
+
|
|
439
|
+
**Note:** If you provide `X-SW-Cache-Invalidate` headers, they take precedence over inferred paths. Only the header-specified paths will be invalidated, not the inferred ones.
|
|
440
|
+
|
|
441
|
+
**Example: Handling nested resources**
|
|
442
|
+
|
|
443
|
+
```javascript
|
|
444
|
+
// DELETE /api/users/123/avatar - inferred invalidation would only handle
|
|
445
|
+
// /api/users/123/avatar and /api/users/123, but we also want to invalidate
|
|
446
|
+
// /api/users since the user list might show avatar thumbnails
|
|
447
|
+
const headers = new Headers();
|
|
448
|
+
headers.append("X-SW-Cache-Invalidate", "/api/users/123/avatar");
|
|
449
|
+
headers.append("X-SW-Cache-Invalidate", "/api/users/123");
|
|
450
|
+
headers.append("X-SW-Cache-Invalidate", "/api/users");
|
|
451
|
+
|
|
452
|
+
fetch("/api/users/123/avatar", {
|
|
453
|
+
method: "DELETE",
|
|
454
|
+
headers
|
|
455
|
+
});
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
You can disable automatic invalidation:
|
|
459
|
+
|
|
460
|
+
```javascript
|
|
461
|
+
const handleRequest = createHandleRequest({
|
|
462
|
+
cacheName: "api-cache-v1",
|
|
463
|
+
scope: ["/api/"],
|
|
464
|
+
inferInvalidation: false // Disable automatic invalidation
|
|
465
|
+
});
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## More Usage Examples
|
|
469
|
+
|
|
470
|
+
### Example 1: Basic API Caching
|
|
471
|
+
|
|
472
|
+
```javascript
|
|
473
|
+
// sw.js
|
|
474
|
+
import { createHandleRequest } from "swimple";
|
|
475
|
+
|
|
476
|
+
const handleRequest = createHandleRequest({
|
|
477
|
+
cacheName: "api-cache-v1",
|
|
478
|
+
scope: ["/api/"]
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
self.addEventListener("fetch", (event) => {
|
|
482
|
+
const response = handleRequest(event);
|
|
483
|
+
if (response) {
|
|
484
|
+
event.respondWith(response);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// Client code
|
|
489
|
+
// Cache user list for 10 minutes
|
|
490
|
+
fetch("/api/users", {
|
|
491
|
+
headers: {
|
|
492
|
+
"X-SW-Cache-TTL-Seconds": "600"
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Update user - automatically invalidates /api/users and /api/users/123
|
|
497
|
+
fetch("/api/users/123", {
|
|
498
|
+
method: "PATCH",
|
|
499
|
+
body: JSON.stringify({ name: "Jane Doe" })
|
|
500
|
+
});
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Example 2: Automatic Caching with Custom TTL
|
|
504
|
+
|
|
505
|
+
```javascript
|
|
506
|
+
// sw.js
|
|
507
|
+
const handleRequest = createHandleRequest({
|
|
508
|
+
cacheName: "api-cache-v1",
|
|
509
|
+
scope: ["/api/"],
|
|
510
|
+
defaultStrategy: "cache-first",
|
|
511
|
+
defaultTTLSeconds: 600 // Cache all API calls for 10 minutes
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
self.addEventListener("fetch", (event) => {
|
|
515
|
+
const response = handleRequest(event);
|
|
516
|
+
if (response) {
|
|
517
|
+
event.respondWith(response);
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// Client code
|
|
522
|
+
// Automatically cached
|
|
523
|
+
fetch("/api/users");
|
|
524
|
+
|
|
525
|
+
// Completely opt out of caching for specific request (handler returns null)
|
|
526
|
+
fetch("/api/live-data", {
|
|
527
|
+
headers: {
|
|
528
|
+
"X-SW-Cache-TTL-Seconds": "0"
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### Example 3: Stale-While-Revalidate
|
|
534
|
+
|
|
535
|
+
```javascript
|
|
536
|
+
// sw.js
|
|
537
|
+
const handleRequest = createHandleRequest({
|
|
538
|
+
cacheName: "api-cache-v1",
|
|
539
|
+
scope: ["/api/"],
|
|
540
|
+
defaultStrategy: "stale-while-revalidate",
|
|
541
|
+
defaultTTLSeconds: 300, // Fresh for 5 minutes - return from cache without background update
|
|
542
|
+
defaultStaleTTLSeconds: 3600 // Stale for up to 1 hour - return from cache and update in background
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
self.addEventListener("fetch", (event) => {
|
|
546
|
+
const response = handleRequest(event);
|
|
547
|
+
if (response) {
|
|
548
|
+
event.respondWith(response);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// Client code
|
|
553
|
+
// Returns cached data immediately (fresh = no update, stale = update in background)
|
|
554
|
+
fetch("/api/users");
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### Example 4: Multiple Scopes
|
|
558
|
+
|
|
559
|
+
```javascript
|
|
560
|
+
// sw.js
|
|
561
|
+
const handleRequest = createHandleRequest({
|
|
562
|
+
cacheName: "api-cache-v1",
|
|
563
|
+
scope: ["/api/", "/graphql/", "/data/"],
|
|
564
|
+
defaultTTLSeconds: 300
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
self.addEventListener("fetch", (event) => {
|
|
568
|
+
const response = handleRequest(event);
|
|
569
|
+
if (response) {
|
|
570
|
+
event.respondWith(response);
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### Example 5: Logout with Cache Clear
|
|
576
|
+
|
|
577
|
+
```javascript
|
|
578
|
+
// Client code
|
|
579
|
+
async function logout() {
|
|
580
|
+
await fetch("/api/logout", {
|
|
581
|
+
method: "POST",
|
|
582
|
+
headers: {
|
|
583
|
+
"X-SW-Cache-Clear": "true" // Clear all cached user data
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
window.location.href = "/login";
|
|
588
|
+
}
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### Example 6: Custom Request Handler Chain
|
|
592
|
+
|
|
593
|
+
Your service worker might have other "handlers" or "middlewares" that need to be called before the cache handler.
|
|
594
|
+
|
|
595
|
+
```javascript
|
|
596
|
+
// sw.js
|
|
597
|
+
import { createHandleRequest } from "swimple";
|
|
598
|
+
|
|
599
|
+
const handleRequest = createHandleRequest({
|
|
600
|
+
cacheName: "api-cache-v1",
|
|
601
|
+
scope: ["/api/"],
|
|
602
|
+
defaultTTLSeconds: 300
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// Custom handler for special handling
|
|
606
|
+
const customHandler = (event) => {
|
|
607
|
+
const url = new URL(event.request.url);
|
|
608
|
+
|
|
609
|
+
// Special handling for auth endpoints
|
|
610
|
+
if (url.pathname.startsWith("/api/auth")) {
|
|
611
|
+
return fetch(event.request);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return null; // Fall through
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
self.addEventListener("fetch", (event) => {
|
|
618
|
+
// Try custom handler first
|
|
619
|
+
let response = customHandler(event);
|
|
620
|
+
if (response) {
|
|
621
|
+
event.respondWith(response);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Then try cache handler
|
|
626
|
+
response = handleRequest(event);
|
|
627
|
+
if (response) {
|
|
628
|
+
event.respondWith(response);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Default fetch
|
|
633
|
+
event.respondWith(fetch(event.request));
|
|
634
|
+
});
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
## How It Works
|
|
638
|
+
|
|
639
|
+
1. **GET Requests**: The request handler checks if a GET request matches the configured scope and caching criteria
|
|
640
|
+
2. **Cache Lookup**: For eligible requests, it checks the Cache API for a valid cached response
|
|
641
|
+
3. **Strategy Execution**: Based on the strategy (cache-first, network-first, stale-while-revalidate), it either returns cached data or fetches from network
|
|
642
|
+
4. **TTL Management**: Cached responses store only the timestamp of when they were cached. When a request is made, the TTL from the request (or default) is used to calculate if the cached response is fresh or stale by comparing the current time with the cached timestamp. Responses within the TTL are "fresh" (returned without background updates). Responses past the TTL but within the stale TTL are "stale" (returned with background updates or used as offline fallback)
|
|
643
|
+
5. **Mutation Handling**: POST/PATCH/PUT/DELETE requests trigger cache invalidation based on inferred or explicit paths
|
|
644
|
+
6. **Cache Clearing**: Requests with `X-SW-Cache-Clear` header wipe the entire cache
|
|
645
|
+
|
|
646
|
+
## Important Notes
|
|
647
|
+
|
|
648
|
+
- Only GET requests are cached
|
|
649
|
+
- Only 2xx (OK) GET responses are cached. Non-OK responses (4xx, 5xx, etc.) are not cached
|
|
650
|
+
- Non-GET and non-mutating requests (POST/PATCH/PUT/DELETE) are not processed by the cache handler - it will return null. Practically, this means HEAD requests are not handled by the cache handler.
|
|
651
|
+
- Query strings are part of the cache key. Different query strings create different cache entries (e.g., `/api/users?page=1` and `/api/users?page=2` are separate cache entries)
|
|
652
|
+
- Cache invalidation happens automatically for mutations when `inferInvalidation: true`
|
|
653
|
+
- All headers are case-insensitive (per HTTP spec)
|
|
654
|
+
- TTL of `0` completely opts out of caching for a request - the handler returns `null` immediately without checking cache, making network requests, or processing the request.
|
|
655
|
+
- Cache entries store only the timestamp of when they were cached. The TTL is not stored; it's provided by each request (or set with a default via `createHandleRequest` config) and the freshness or staleness is calculated at request time. This means one request could use a longer TTL than another request and therefore allow a later expiration time.
|
|
656
|
+
|
|
657
|
+
## Error Handling
|
|
658
|
+
|
|
659
|
+
The library follows a straightforward error handling approach:
|
|
660
|
+
|
|
661
|
+
### Configuration Validation Errors
|
|
662
|
+
|
|
663
|
+
Invalid configuration values passed to `createHandleRequest` will throw errors immediately. This helps catch configuration mistakes early.
|
|
664
|
+
|
|
665
|
+
```javascript
|
|
666
|
+
// This will throw an error
|
|
667
|
+
const handleRequest = createHandleRequest({
|
|
668
|
+
cacheName: "api-cache-v1",
|
|
669
|
+
defaultStrategy: "invalid-strategy" // Error: invalid strategy
|
|
670
|
+
});
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### Exceptional Errors
|
|
674
|
+
|
|
675
|
+
The library does not catch or swallow exceptional errors. If an internal operation like `cache.delete()` throws an exception (which is truly exceptional since browsers don't throw in normal cases), that error will bubble up to your code.
|
|
676
|
+
|
|
677
|
+
This means you can wrap your `handleRequest` calls in try/catch if you want to handle errors:
|
|
678
|
+
|
|
679
|
+
```javascript
|
|
680
|
+
self.addEventListener("fetch", (event) => {
|
|
681
|
+
try {
|
|
682
|
+
const response = handleRequest(event);
|
|
683
|
+
if (response) {
|
|
684
|
+
event.respondWith(response);
|
|
685
|
+
}
|
|
686
|
+
} catch (error) {
|
|
687
|
+
// Handle exceptional errors
|
|
688
|
+
console.error("Cache handler error:", error);
|
|
689
|
+
// Fall back to network
|
|
690
|
+
event.respondWith(fetch(event.request));
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
If you don't wrap `handleRequest` in try/catch, any exceptional errors will propagate normally, which may cause the service worker fetch handler to fail. Whether you need error handling depends on your application's requirements.
|
|
696
|
+
|
|
697
|
+
## License
|
|
698
|
+
|
|
699
|
+
MIT
|
|
700
|
+
|
|
701
|
+
## Contributing
|
|
702
|
+
|
|
703
|
+
Contributions are welcome! Please open an issue or PR on GitHub.
|
|
704
|
+
|
|
705
|
+
## Links
|
|
706
|
+
|
|
707
|
+
- [GitHub Repository](#)
|
|
708
|
+
- [NPM Package](#)
|
|
709
|
+
- [Documentation](#)
|
|
710
|
+
- [Report Issues](#)
|
|
711
|
+
|
|
712
|
+
## TODO
|
|
713
|
+
|
|
714
|
+
- [ ] make a landing page (at https://swimple.goulet.dev) with example usage and a link to the documentation
|
package/helpers.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
// This library is meant to be called from service workers, so we include
|
|
4
|
+
// webworker types and exclude DOM types (which don't exist in service workers)
|
|
5
|
+
/// <reference no-default-lib="true"/>
|
|
6
|
+
/// <reference lib="esnext" />
|
|
7
|
+
/// <reference lib="webworker" />
|
|
8
|
+
|
|
9
|
+
/** @import { CacheStrategy, HandleRequestConfig } from "./types" */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get header value (case-insensitive)
|
|
13
|
+
* @param {Headers} headers
|
|
14
|
+
* @param {string} name
|
|
15
|
+
* @returns {string | null}
|
|
16
|
+
*/
|
|
17
|
+
export function getHeader(headers, name) {
|
|
18
|
+
const lowerName = name.toLowerCase();
|
|
19
|
+
// Headers is iterable in browsers, iterate through entries
|
|
20
|
+
const entries = /** @type {unknown} */ (headers);
|
|
21
|
+
const iterable = /** @type {Iterable<[string, string]>} */ (entries);
|
|
22
|
+
for (const [key, value] of iterable) {
|
|
23
|
+
if (key.toLowerCase() === lowerName) {
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get all header values for a given header name (case-insensitive)
|
|
32
|
+
* Useful when a header has been set multiple times (e.g., multiple X-SW-Cache-Invalidate headers)
|
|
33
|
+
* When headers are set multiple times with append(), they are concatenated with ", " (comma+space)
|
|
34
|
+
* This function splits them back into individual values
|
|
35
|
+
* @param {Headers} headers
|
|
36
|
+
* @param {string} name - Header name to look up
|
|
37
|
+
* @returns {string[]} Array of all header values for the given name
|
|
38
|
+
*/
|
|
39
|
+
export function getAllHeaders(headers, name) {
|
|
40
|
+
const lowerName = name.toLowerCase();
|
|
41
|
+
const values = [];
|
|
42
|
+
// Headers is iterable in browsers, iterate through entries
|
|
43
|
+
const entries = /** @type {unknown} */ (headers);
|
|
44
|
+
const iterable = /** @type {Iterable<[string, string]>} */ (entries);
|
|
45
|
+
for (const [key, value] of iterable) {
|
|
46
|
+
if (key.toLowerCase() === lowerName) {
|
|
47
|
+
// Split comma+space separated values (HTTP spec: multiple header values are comma-separated)
|
|
48
|
+
// Split on ", " (comma+space) to handle concatenated values from multiple append() calls
|
|
49
|
+
const splitValues = value.split(", ");
|
|
50
|
+
values.push(...splitValues);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return values;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get cache timestamp from response
|
|
58
|
+
* @param {Response} response
|
|
59
|
+
* @returns {number | null} Timestamp in milliseconds since epoch, or null if not found
|
|
60
|
+
*/
|
|
61
|
+
export function getCacheTimestamp(response) {
|
|
62
|
+
const timestampHeader = getHeader(response.headers, "x-sw-cache-timestamp");
|
|
63
|
+
if (!timestampHeader) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const timestamp = parseInt(timestampHeader, 10);
|
|
67
|
+
return isNaN(timestamp) ? null : timestamp;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if response is fresh
|
|
72
|
+
* @param {Response} response
|
|
73
|
+
* @param {number} ttl - Time-to-live in seconds
|
|
74
|
+
* @returns {boolean}
|
|
75
|
+
*/
|
|
76
|
+
export function isFresh(response, ttl) {
|
|
77
|
+
const timestamp = getCacheTimestamp(response);
|
|
78
|
+
if (timestamp === null) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const age = Date.now() - timestamp;
|
|
82
|
+
return age < ttl * 1000;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if response is stale (but usable)
|
|
87
|
+
* @param {Response} response
|
|
88
|
+
* @param {number} ttl - Time-to-live in seconds
|
|
89
|
+
* @param {number | null} staleTTL - Stale time-to-live in seconds
|
|
90
|
+
* @returns {boolean}
|
|
91
|
+
*/
|
|
92
|
+
export function isStale(response, ttl, staleTTL) {
|
|
93
|
+
if (staleTTL === null) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const timestamp = getCacheTimestamp(response);
|
|
97
|
+
if (timestamp === null) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const age = Date.now() - timestamp;
|
|
101
|
+
return age >= ttl * 1000 && age < staleTTL * 1000;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Add timestamp to response
|
|
106
|
+
* @param {Response} response
|
|
107
|
+
* @returns {Response}
|
|
108
|
+
*/
|
|
109
|
+
export function addTimestamp(response) {
|
|
110
|
+
const headers = new Headers(response.headers);
|
|
111
|
+
headers.set("x-sw-cache-timestamp", Date.now().toString());
|
|
112
|
+
return new Response(response.body, {
|
|
113
|
+
status: response.status,
|
|
114
|
+
statusText: response.statusText,
|
|
115
|
+
headers: headers
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get inferred invalidation paths
|
|
121
|
+
* @param {string} url
|
|
122
|
+
* @returns {string[]}
|
|
123
|
+
*/
|
|
124
|
+
export function getInferredInvalidationPaths(url) {
|
|
125
|
+
const urlObj = new URL(url);
|
|
126
|
+
const path = urlObj.pathname;
|
|
127
|
+
const paths = [url]; // Exact path
|
|
128
|
+
|
|
129
|
+
// Strip last path segment to get parent collection
|
|
130
|
+
const lastSlash = path.lastIndexOf("/");
|
|
131
|
+
if (lastSlash > 0) {
|
|
132
|
+
const parentPath = path.substring(0, lastSlash);
|
|
133
|
+
if (parentPath) {
|
|
134
|
+
const parentUrl = new URL(parentPath, urlObj.origin);
|
|
135
|
+
paths.push(parentUrl.toString());
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return paths;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get strategy from request headers or use default
|
|
144
|
+
* @param {Headers} headers
|
|
145
|
+
* @param {CacheStrategy} defaultStrategy
|
|
146
|
+
* @returns {CacheStrategy}
|
|
147
|
+
*/
|
|
148
|
+
export function getStrategy(headers, defaultStrategy) {
|
|
149
|
+
const strategyHeader = getHeader(headers, "X-SW-Cache-Strategy");
|
|
150
|
+
if (
|
|
151
|
+
strategyHeader &&
|
|
152
|
+
["cache-first", "network-first", "stale-while-revalidate"].includes(
|
|
153
|
+
strategyHeader
|
|
154
|
+
)
|
|
155
|
+
) {
|
|
156
|
+
return /** @type {CacheStrategy} */ (strategyHeader);
|
|
157
|
+
}
|
|
158
|
+
return defaultStrategy;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get TTL from request headers or use default
|
|
163
|
+
* @param {Headers} headers
|
|
164
|
+
* @param {number} defaultTTL - Default TTL in seconds
|
|
165
|
+
* @returns {number | null} TTL in seconds, or null if caching is disabled
|
|
166
|
+
*/
|
|
167
|
+
export function getTTL(headers, defaultTTL) {
|
|
168
|
+
const ttlHeader = getHeader(headers, "X-SW-Cache-TTL-Seconds");
|
|
169
|
+
if (ttlHeader === null) {
|
|
170
|
+
return defaultTTL > 0 ? defaultTTL : null;
|
|
171
|
+
}
|
|
172
|
+
const ttl = parseInt(ttlHeader, 10);
|
|
173
|
+
if (isNaN(ttl) || ttl <= 0) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return ttl;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get stale TTL from request headers or use default
|
|
181
|
+
* @param {Headers} headers
|
|
182
|
+
* @param {number} defaultStaleTTL - Default stale TTL in seconds
|
|
183
|
+
* @returns {number | null} Stale TTL in seconds, or null if stale caching is disabled
|
|
184
|
+
*/
|
|
185
|
+
export function getStaleTTL(headers, defaultStaleTTL) {
|
|
186
|
+
const staleTTLHeader = getHeader(headers, "X-SW-Cache-Stale-TTL-Seconds");
|
|
187
|
+
if (staleTTLHeader === null) {
|
|
188
|
+
return defaultStaleTTL > 0 ? defaultStaleTTL : null;
|
|
189
|
+
}
|
|
190
|
+
const staleTTL = parseInt(staleTTLHeader, 10);
|
|
191
|
+
if (isNaN(staleTTL) || staleTTL <= 0) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
return staleTTL;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Check if URL matches scope. Returns true if scope array is empty or if the URL pathname starts with any of the scope prefixes.
|
|
199
|
+
* @param {string} url
|
|
200
|
+
* @param {string[]} scope
|
|
201
|
+
* @param {number} defaultTTLSeconds
|
|
202
|
+
* @returns {boolean}
|
|
203
|
+
*/
|
|
204
|
+
export function matchesScope(url, scope, defaultTTLSeconds) {
|
|
205
|
+
if (scope.length === 0) {
|
|
206
|
+
return defaultTTLSeconds > 0;
|
|
207
|
+
}
|
|
208
|
+
const urlObj = new URL(url);
|
|
209
|
+
return scope.some((prefix) => urlObj.pathname.startsWith(prefix));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Invalidate cache entries
|
|
214
|
+
* @param {string} cacheName
|
|
215
|
+
* @param {string[]} urls
|
|
216
|
+
* @returns {Promise<void>}
|
|
217
|
+
*/
|
|
218
|
+
export async function invalidateCache(cacheName, urls) {
|
|
219
|
+
const cache = await caches.open(cacheName);
|
|
220
|
+
await Promise.allSettled(urls.map((url) => cache.delete(url)));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Clear entire cache
|
|
225
|
+
* @param {string} cacheName
|
|
226
|
+
* @returns {Promise<void>}
|
|
227
|
+
*/
|
|
228
|
+
export async function clearCache(cacheName) {
|
|
229
|
+
const cache = await caches.open(cacheName);
|
|
230
|
+
const keys = await cache.keys();
|
|
231
|
+
await Promise.allSettled(keys.map((request) => cache.delete(request)));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Check if a cached response is older than the maximum age
|
|
236
|
+
* @param {Response} response
|
|
237
|
+
* @param {number} maxAgeSeconds - Maximum age in seconds
|
|
238
|
+
* @returns {boolean}
|
|
239
|
+
*/
|
|
240
|
+
export function isOlderThanMaxAge(response, maxAgeSeconds) {
|
|
241
|
+
const timestamp = getCacheTimestamp(response);
|
|
242
|
+
if (timestamp === null) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
const age = Date.now() - timestamp;
|
|
246
|
+
return age >= maxAgeSeconds * 1000;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Clean up cache entries older than maxAgeSeconds
|
|
251
|
+
* @param {string} cacheName
|
|
252
|
+
* @param {number} maxAgeSeconds - Maximum age in seconds
|
|
253
|
+
* @returns {Promise<void>}
|
|
254
|
+
*/
|
|
255
|
+
export async function cleanupOldCacheEntries(cacheName, maxAgeSeconds) {
|
|
256
|
+
const cache = await caches.open(cacheName);
|
|
257
|
+
const keys = await cache.keys();
|
|
258
|
+
const cleanupPromises = [];
|
|
259
|
+
|
|
260
|
+
for (const request of keys) {
|
|
261
|
+
const response = await cache.match(request);
|
|
262
|
+
if (response && isOlderThanMaxAge(response, maxAgeSeconds)) {
|
|
263
|
+
cleanupPromises.push(cache.delete(request));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
await Promise.allSettled(cleanupPromises);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Validate configuration object
|
|
272
|
+
* @param {HandleRequestConfig} config - Configuration object to validate
|
|
273
|
+
* @throws {Error} If config is invalid
|
|
274
|
+
*/
|
|
275
|
+
export function validateConfig(config) {
|
|
276
|
+
if (!config || typeof config !== "object") {
|
|
277
|
+
throw new Error("config is required and must be an object");
|
|
278
|
+
}
|
|
279
|
+
const cfg = config;
|
|
280
|
+
if (!cfg.cacheName || typeof cfg.cacheName !== "string") {
|
|
281
|
+
throw new Error("config.cacheName is required and must be a string");
|
|
282
|
+
}
|
|
283
|
+
if (
|
|
284
|
+
cfg.defaultStrategy &&
|
|
285
|
+
!["cache-first", "network-first", "stale-while-revalidate"].includes(
|
|
286
|
+
String(cfg.defaultStrategy)
|
|
287
|
+
)
|
|
288
|
+
) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
"config.defaultStrategy must be one of: 'cache-first', 'network-first', 'stale-while-revalidate'"
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
if (cfg.customFetch !== undefined && typeof cfg.customFetch !== "function") {
|
|
294
|
+
throw new Error("config.customFetch must be a function");
|
|
295
|
+
}
|
|
296
|
+
if (
|
|
297
|
+
cfg.maxCacheAgeSeconds !== undefined &&
|
|
298
|
+
(typeof cfg.maxCacheAgeSeconds !== "number" || cfg.maxCacheAgeSeconds <= 0)
|
|
299
|
+
) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
"config.maxCacheAgeSeconds must be a positive number if provided"
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
// This library is meant to be called from service workers, so we include
|
|
4
|
+
// webworker types and exclude DOM types (which don't exist in service workers)
|
|
5
|
+
/// <reference no-default-lib="true"/>
|
|
6
|
+
/// <reference lib="esnext" />
|
|
7
|
+
/// <reference lib="webworker" />
|
|
8
|
+
|
|
9
|
+
/** @import { HandleRequestConfig, CacheStrategy } from "./types" */
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
getHeader,
|
|
13
|
+
getAllHeaders,
|
|
14
|
+
isFresh,
|
|
15
|
+
isStale,
|
|
16
|
+
addTimestamp,
|
|
17
|
+
getInferredInvalidationPaths,
|
|
18
|
+
getStrategy,
|
|
19
|
+
getTTL,
|
|
20
|
+
getStaleTTL,
|
|
21
|
+
matchesScope,
|
|
22
|
+
invalidateCache,
|
|
23
|
+
clearCache,
|
|
24
|
+
validateConfig,
|
|
25
|
+
isOlderThanMaxAge,
|
|
26
|
+
cleanupOldCacheEntries
|
|
27
|
+
} from "./helpers.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Creates a request handler function for service worker fetch events.
|
|
31
|
+
*
|
|
32
|
+
* @param {HandleRequestConfig} config - Configuration options
|
|
33
|
+
* @returns {(event: FetchEvent) => Promise<Response> | null} Request handler function
|
|
34
|
+
*/
|
|
35
|
+
export function createHandleRequest(config) {
|
|
36
|
+
validateConfig(config);
|
|
37
|
+
|
|
38
|
+
// Set defaults
|
|
39
|
+
const cacheName = config.cacheName;
|
|
40
|
+
const scope = config.scope || [];
|
|
41
|
+
const defaultStrategy = /** @type {CacheStrategy} */ (
|
|
42
|
+
config.defaultStrategy || "cache-first"
|
|
43
|
+
);
|
|
44
|
+
const defaultTTLSeconds = config.defaultTTLSeconds ?? 300;
|
|
45
|
+
const defaultStaleTTLSeconds = config.defaultStaleTTLSeconds ?? 3600;
|
|
46
|
+
const maxCacheAgeSeconds = config.maxCacheAgeSeconds ?? 7200;
|
|
47
|
+
const inferInvalidation = config.inferInvalidation ?? true;
|
|
48
|
+
const customFetch = config.customFetch || fetch;
|
|
49
|
+
|
|
50
|
+
// Track fetch counter for periodic cleanup
|
|
51
|
+
let fetchCounter = 0;
|
|
52
|
+
|
|
53
|
+
// Main request handler
|
|
54
|
+
return function handleRequest(event) {
|
|
55
|
+
const request = event.request;
|
|
56
|
+
const url = request.url;
|
|
57
|
+
const method = request.method;
|
|
58
|
+
const headers = request.headers;
|
|
59
|
+
|
|
60
|
+
// Handle cache clearing - header presence (any value) triggers cache clear
|
|
61
|
+
if (getHeader(headers, "X-SW-Cache-Clear") !== null) {
|
|
62
|
+
return (async () => {
|
|
63
|
+
await clearCache(cacheName);
|
|
64
|
+
return customFetch(request);
|
|
65
|
+
})();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Handle invalidation for mutations
|
|
69
|
+
// Check for explicit invalidation headers first (works even if inferInvalidation is false)
|
|
70
|
+
const invalidateHeaders = getAllHeaders(headers, "X-SW-Cache-Invalidate");
|
|
71
|
+
const isMutation = ["POST", "PATCH", "PUT", "DELETE"].includes(method);
|
|
72
|
+
|
|
73
|
+
if (invalidateHeaders.length > 0 || (inferInvalidation && isMutation)) {
|
|
74
|
+
return (async () => {
|
|
75
|
+
let pathsToInvalidate = [...invalidateHeaders];
|
|
76
|
+
|
|
77
|
+
// Only add inferred paths if no explicit headers were provided
|
|
78
|
+
// Headers take precedence over inferred paths
|
|
79
|
+
if (pathsToInvalidate.length === 0 && inferInvalidation && isMutation) {
|
|
80
|
+
pathsToInvalidate.push(...getInferredInvalidationPaths(url));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (pathsToInvalidate.length > 0) {
|
|
84
|
+
await invalidateCache(cacheName, pathsToInvalidate);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return customFetch(request);
|
|
88
|
+
})();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Only handle GET requests
|
|
92
|
+
if (method !== "GET") {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Periodic cleanup: run on first fetch and every 100 fetches
|
|
97
|
+
fetchCounter++;
|
|
98
|
+
if (fetchCounter === 1 || fetchCounter % 100 === 0) {
|
|
99
|
+
// Run cleanup asynchronously, don't block the fetch
|
|
100
|
+
cleanupOldCacheEntries(cacheName, maxCacheAgeSeconds).catch(() => {
|
|
101
|
+
// Ignore cleanup errors
|
|
102
|
+
});
|
|
103
|
+
if (fetchCounter % 100 === 0) {
|
|
104
|
+
fetchCounter = 1; // Reset to 1 after cleanup, not 0
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check if request matches scope and should be cached
|
|
109
|
+
const hasExplicitTTLHeader =
|
|
110
|
+
getHeader(headers, "X-SW-Cache-TTL-Seconds") !== null;
|
|
111
|
+
const ttl = getTTL(headers, defaultTTLSeconds);
|
|
112
|
+
|
|
113
|
+
// If scope doesn't match and there's no explicit TTL header, don't handle the request
|
|
114
|
+
if (!matchesScope(url, scope, defaultTTLSeconds) && !hasExplicitTTLHeader) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// If TTL is 0 or null, don't cache
|
|
119
|
+
if (ttl === null || ttl === 0) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const staleTTL = getStaleTTL(headers, defaultStaleTTLSeconds);
|
|
124
|
+
const strategy = getStrategy(headers, defaultStrategy);
|
|
125
|
+
|
|
126
|
+
// Handle cache-first strategy
|
|
127
|
+
if (strategy === "cache-first") {
|
|
128
|
+
return (async () => {
|
|
129
|
+
const cache = await caches.open(cacheName);
|
|
130
|
+
const cachedResponse = await cache.match(request);
|
|
131
|
+
|
|
132
|
+
if (cachedResponse) {
|
|
133
|
+
if (isFresh(cachedResponse, ttl)) {
|
|
134
|
+
return cachedResponse;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Reactive cleanup: delete if older than maxCacheAgeSeconds
|
|
138
|
+
if (isOlderThanMaxAge(cachedResponse, maxCacheAgeSeconds)) {
|
|
139
|
+
await cache.delete(request); // Fire-and-forget cleanup
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// No fresh cache, fetch from network
|
|
144
|
+
let networkResponse;
|
|
145
|
+
try {
|
|
146
|
+
networkResponse = await customFetch(request);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
// Network failed, return stale cache if available
|
|
149
|
+
if (cachedResponse && isStale(cachedResponse, ttl, staleTTL)) {
|
|
150
|
+
return cachedResponse;
|
|
151
|
+
}
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Cache the response if successful
|
|
156
|
+
if (networkResponse.ok) {
|
|
157
|
+
const responseToCache = addTimestamp(networkResponse.clone());
|
|
158
|
+
await cache.put(request, responseToCache);
|
|
159
|
+
}
|
|
160
|
+
return networkResponse;
|
|
161
|
+
})();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Handle network-first strategy
|
|
165
|
+
if (strategy === "network-first") {
|
|
166
|
+
return (async () => {
|
|
167
|
+
const cache = await caches.open(cacheName);
|
|
168
|
+
|
|
169
|
+
let networkResponse;
|
|
170
|
+
try {
|
|
171
|
+
networkResponse = await customFetch(request);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
// Network failed, try cache
|
|
174
|
+
const cachedResponse = await cache.match(request);
|
|
175
|
+
if (cachedResponse) {
|
|
176
|
+
// Reactive cleanup: delete if older than maxCacheAgeSeconds
|
|
177
|
+
if (isOlderThanMaxAge(cachedResponse, maxCacheAgeSeconds)) {
|
|
178
|
+
cache.delete(request); // Fire-and-forget cleanup
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
if (
|
|
182
|
+
isFresh(cachedResponse, ttl) ||
|
|
183
|
+
isStale(cachedResponse, ttl, staleTTL)
|
|
184
|
+
) {
|
|
185
|
+
return cachedResponse;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Cache the response if successful
|
|
192
|
+
if (networkResponse.ok) {
|
|
193
|
+
const responseToCache = addTimestamp(networkResponse.clone());
|
|
194
|
+
await cache.put(request, responseToCache);
|
|
195
|
+
}
|
|
196
|
+
return networkResponse;
|
|
197
|
+
})();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Handle stale-while-revalidate strategy
|
|
201
|
+
if (strategy === "stale-while-revalidate") {
|
|
202
|
+
return (async () => {
|
|
203
|
+
const cache = await caches.open(cacheName);
|
|
204
|
+
const cachedResponse = await cache.match(request);
|
|
205
|
+
|
|
206
|
+
if (cachedResponse) {
|
|
207
|
+
// Reactive cleanup: delete if older than maxCacheAgeSeconds
|
|
208
|
+
if (isOlderThanMaxAge(cachedResponse, maxCacheAgeSeconds)) {
|
|
209
|
+
cache.delete(request); // Fire-and-forget cleanup
|
|
210
|
+
// Continue to fetch from network
|
|
211
|
+
} else {
|
|
212
|
+
const fresh = isFresh(cachedResponse, ttl);
|
|
213
|
+
const stale = isStale(cachedResponse, ttl, staleTTL);
|
|
214
|
+
|
|
215
|
+
if (fresh || stale) {
|
|
216
|
+
// Return cached response immediately
|
|
217
|
+
// Update cache in background if stale
|
|
218
|
+
if (stale) {
|
|
219
|
+
customFetch(request)
|
|
220
|
+
.then((networkResponse) => {
|
|
221
|
+
if (networkResponse.ok) {
|
|
222
|
+
const responseToCache = addTimestamp(
|
|
223
|
+
networkResponse.clone()
|
|
224
|
+
);
|
|
225
|
+
cache.put(request, responseToCache);
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
.catch(() => {
|
|
229
|
+
// Ignore background update errors
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return cachedResponse;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// No cache or too stale, fetch from network, no need for fallback if offline
|
|
238
|
+
// because we already know if there was a cached response it won't be
|
|
239
|
+
// fresh or stale if we've reached this point
|
|
240
|
+
const networkResponse = await customFetch(request);
|
|
241
|
+
|
|
242
|
+
// Cache the response if successful
|
|
243
|
+
if (networkResponse.ok) {
|
|
244
|
+
const responseToCache = addTimestamp(networkResponse.clone());
|
|
245
|
+
await cache.put(request, responseToCache);
|
|
246
|
+
}
|
|
247
|
+
return networkResponse;
|
|
248
|
+
})();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return null;
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Export cleanup function for manual use
|
|
256
|
+
export { cleanupOldCacheEntries } from "./helpers.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "swimple",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A simple service worker library for request caching",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "npm run test:unit && npm run test:e2e && npm run test:ui",
|
|
12
|
+
"test:unit": "node --test tests/**/*.unit.test.js",
|
|
13
|
+
"test:e2e": "node --test tests/**/*.e2e.test.js",
|
|
14
|
+
"test:ui": "playwright test",
|
|
15
|
+
"format:check": "prettier --check .",
|
|
16
|
+
"format:fix": "prettier --write ."
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"service-worker",
|
|
20
|
+
"cache",
|
|
21
|
+
"caching",
|
|
22
|
+
"sw",
|
|
23
|
+
"offline",
|
|
24
|
+
"pwa"
|
|
25
|
+
],
|
|
26
|
+
"author": "wes@goulet.dev",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@playwright/test": "^1.57.0",
|
|
30
|
+
"prettier": "^3.7.4"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=24"
|
|
34
|
+
},
|
|
35
|
+
"volta": {
|
|
36
|
+
"node": "24.12.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caching strategy for handling requests.
|
|
3
|
+
* - `cache-first`: Return from cache if fresh (within TTL), otherwise fetch from network immediately. Stale cache is only used when offline (network request fails). No background updates.
|
|
4
|
+
* - `network-first`: Try network first, fall back to stale cache if offline (within stale TTL). Cache updated when network succeeds.
|
|
5
|
+
* - `stale-while-revalidate`: Return from cache immediately (fresh = no update, stale = update in background). Fetch from network if too stale or missing.
|
|
6
|
+
*/
|
|
7
|
+
export type CacheStrategy =
|
|
8
|
+
| "cache-first"
|
|
9
|
+
| "network-first"
|
|
10
|
+
| "stale-while-revalidate";
|
|
11
|
+
|
|
12
|
+
export interface HandleRequestConfig {
|
|
13
|
+
/** Name of the cache, used when calling `Cache.open(cacheName)` internally. Changing this name effectively clears the previous cache entries. */
|
|
14
|
+
cacheName: string;
|
|
15
|
+
/** URL prefixes to cache by default (e.g., `['/api/']`). If not set and `defaultTTLSeconds` is set, all same-origin GET requests are cached automatically. If not set and `defaultTTLSeconds` is not set (or 0), no requests are cached by default. Individual requests outside the scope can still enable caching with `X-SW-Cache-TTL-Seconds` header. */
|
|
16
|
+
scope?: string[];
|
|
17
|
+
/** Default caching strategy: `'cache-first'`, `'network-first'`, or `'stale-while-revalidate'`. */
|
|
18
|
+
defaultStrategy?: CacheStrategy;
|
|
19
|
+
/** Maximum age for fresh content. Fresh content will be returned from cache for cache-first and stale-while-revalidate strategies, and also from network-first when offline. Fresh content does not get updated from the network. Since this defaults to `300`, caching is automatic by default for GET requests matching the scope. Set to `0` or `undefined` to disable automatic caching (individual requests can still enable caching with `X-SW-Cache-TTL-Seconds` header). */
|
|
20
|
+
defaultTTLSeconds?: number;
|
|
21
|
+
/** Maximum age for stale content. Stale content will be returned from cache for cache-first (when offline), network-first (when offline), and stale-while-revalidate strategies. That means responses past the fresh TTL but within stale TTL can still be returned from cache. Stale content does get updated from the network. */
|
|
22
|
+
defaultStaleTTLSeconds?: number;
|
|
23
|
+
/** Automatically invalidate cache on POST/PATCH/PUT/DELETE requests. */
|
|
24
|
+
inferInvalidation?: boolean;
|
|
25
|
+
/** Custom fetch function to use for network requests. Receives a `Request` object and must return a `Promise<Response>`. Useful for handling authentication errors (401/403) or adding custom headers to all requests. */
|
|
26
|
+
customFetch?: typeof fetch;
|
|
27
|
+
/** Maximum age (in seconds) before cache entries are automatically cleaned up. Entries older than this age are deleted. Defaults to 7200 seconds (2 hours, which is 2x the default stale TTL). Cache entries are cleaned up reactively (when accessed) and periodically (every 100 fetches). */
|
|
28
|
+
maxCacheAgeSeconds?: number;
|
|
29
|
+
}
|