swimple 0.1.0 → 0.3.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/README.md +27 -1
- package/helpers.js +53 -4
- package/index.js +75 -7
- package/package.json +1 -1
- package/types.d.ts +3 -1
package/README.md
CHANGED
|
@@ -161,6 +161,30 @@ const handleRequest = createHandleRequest({
|
|
|
161
161
|
|
|
162
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
163
|
|
|
164
|
+
### Example 5: Enable Logging
|
|
165
|
+
|
|
166
|
+
You can enable logging to debug cache behavior. When enabled, the library logs cache hits, misses, header usage, invalidation, and cleanup events to the console.
|
|
167
|
+
|
|
168
|
+
```javascript
|
|
169
|
+
const handleRequest = createHandleRequest({
|
|
170
|
+
cacheName: "api-cache-v1",
|
|
171
|
+
scope: ["/api/"],
|
|
172
|
+
enableLogging: true
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
When logging is enabled, you'll see console output like:
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
[swimple] Cache hit: https://example.com/api/users
|
|
180
|
+
[swimple] Cache miss: https://example.com/api/posts
|
|
181
|
+
[swimple] X-SW-Cache-TTL-Seconds header set: 600 (https://example.com/api/users)
|
|
182
|
+
[swimple] Cache invalidated: https://example.com/api/users/123
|
|
183
|
+
[swimple] Cache entry cleaned up (maxAge): https://example.com/api/old-data
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
This is useful for debugging cache behavior and understanding when requests are served from cache vs. network.
|
|
187
|
+
|
|
164
188
|
## Clearing the cache on logout
|
|
165
189
|
|
|
166
190
|
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).
|
|
@@ -224,13 +248,14 @@ Creates a request handler function for your service worker fetch handler.
|
|
|
224
248
|
| Option | Type | Required | Default | Description |
|
|
225
249
|
| ------------------------ | ---------- | -------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
226
250
|
| `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.
|
|
251
|
+
| `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. **Note:** Cross-origin requests are never cached, regardless of scope or TTL headers. |
|
|
228
252
|
| `defaultStrategy` | `string` | No | `'cache-first'` | Default caching strategy: `'cache-first'`, `'network-first'`, or `'stale-while-revalidate'`. |
|
|
229
253
|
| `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
254
|
| `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
255
|
| `inferInvalidation` | `boolean` | No | `true` | Automatically invalidate cache on POST/PATCH/PUT/DELETE requests. |
|
|
232
256
|
| `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
257
|
| `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). |
|
|
258
|
+
| `enableLogging` | `boolean` | No | `false` | Enable logging for cache hits, misses, header usage, invalidation, and cleanup. When enabled, logs are written to the console with the `[swimple]` prefix. Useful for debugging cache behavior. |
|
|
234
259
|
|
|
235
260
|
#### Returns
|
|
236
261
|
|
|
@@ -647,6 +672,7 @@ self.addEventListener("fetch", (event) => {
|
|
|
647
672
|
|
|
648
673
|
- Only GET requests are cached
|
|
649
674
|
- Only 2xx (OK) GET responses are cached. Non-OK responses (4xx, 5xx, etc.) are not cached
|
|
675
|
+
- **Cross-origin requests are not cached** - Only requests to the same origin as the service worker are cached. Requests to different origins will return `null` and are not processed by the cache handler.
|
|
650
676
|
- 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
677
|
- 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
678
|
- Cache invalidation happens automatically for mutations when `inferInvalidation: true`
|
package/helpers.js
CHANGED
|
@@ -8,6 +8,17 @@
|
|
|
8
8
|
|
|
9
9
|
/** @import { CacheStrategy, HandleRequestConfig } from "./types" */
|
|
10
10
|
|
|
11
|
+
// Module-level logging state
|
|
12
|
+
let loggingEnabled = false;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Set whether logging is enabled
|
|
16
|
+
* @param {boolean} enabled - Whether to enable logging
|
|
17
|
+
*/
|
|
18
|
+
export function setLoggingEnabled(enabled) {
|
|
19
|
+
loggingEnabled = enabled;
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
/**
|
|
12
23
|
* Get header value (case-insensitive)
|
|
13
24
|
* @param {Headers} headers
|
|
@@ -143,9 +154,10 @@ export function getInferredInvalidationPaths(url) {
|
|
|
143
154
|
* Get strategy from request headers or use default
|
|
144
155
|
* @param {Headers} headers
|
|
145
156
|
* @param {CacheStrategy} defaultStrategy
|
|
157
|
+
* @param {string} url - Request URL for logging
|
|
146
158
|
* @returns {CacheStrategy}
|
|
147
159
|
*/
|
|
148
|
-
export function getStrategy(headers, defaultStrategy) {
|
|
160
|
+
export function getStrategy(headers, defaultStrategy, url = "") {
|
|
149
161
|
const strategyHeader = getHeader(headers, "X-SW-Cache-Strategy");
|
|
150
162
|
if (
|
|
151
163
|
strategyHeader &&
|
|
@@ -153,6 +165,7 @@ export function getStrategy(headers, defaultStrategy) {
|
|
|
153
165
|
strategyHeader
|
|
154
166
|
)
|
|
155
167
|
) {
|
|
168
|
+
log(`X-SW-Cache-Strategy header set: ${strategyHeader} (${url})`);
|
|
156
169
|
return /** @type {CacheStrategy} */ (strategyHeader);
|
|
157
170
|
}
|
|
158
171
|
return defaultStrategy;
|
|
@@ -162,13 +175,15 @@ export function getStrategy(headers, defaultStrategy) {
|
|
|
162
175
|
* Get TTL from request headers or use default
|
|
163
176
|
* @param {Headers} headers
|
|
164
177
|
* @param {number} defaultTTL - Default TTL in seconds
|
|
178
|
+
* @param {string} url - Request URL for logging
|
|
165
179
|
* @returns {number | null} TTL in seconds, or null if caching is disabled
|
|
166
180
|
*/
|
|
167
|
-
export function getTTL(headers, defaultTTL) {
|
|
181
|
+
export function getTTL(headers, defaultTTL, url = "") {
|
|
168
182
|
const ttlHeader = getHeader(headers, "X-SW-Cache-TTL-Seconds");
|
|
169
183
|
if (ttlHeader === null) {
|
|
170
184
|
return defaultTTL > 0 ? defaultTTL : null;
|
|
171
185
|
}
|
|
186
|
+
log(`X-SW-Cache-TTL-Seconds header set: ${ttlHeader} (${url})`);
|
|
172
187
|
const ttl = parseInt(ttlHeader, 10);
|
|
173
188
|
if (isNaN(ttl) || ttl <= 0) {
|
|
174
189
|
return null;
|
|
@@ -180,13 +195,15 @@ export function getTTL(headers, defaultTTL) {
|
|
|
180
195
|
* Get stale TTL from request headers or use default
|
|
181
196
|
* @param {Headers} headers
|
|
182
197
|
* @param {number} defaultStaleTTL - Default stale TTL in seconds
|
|
198
|
+
* @param {string} url - Request URL for logging
|
|
183
199
|
* @returns {number | null} Stale TTL in seconds, or null if stale caching is disabled
|
|
184
200
|
*/
|
|
185
|
-
export function getStaleTTL(headers, defaultStaleTTL) {
|
|
201
|
+
export function getStaleTTL(headers, defaultStaleTTL, url = "") {
|
|
186
202
|
const staleTTLHeader = getHeader(headers, "X-SW-Cache-Stale-TTL-Seconds");
|
|
187
203
|
if (staleTTLHeader === null) {
|
|
188
204
|
return defaultStaleTTL > 0 ? defaultStaleTTL : null;
|
|
189
205
|
}
|
|
206
|
+
log(`X-SW-Cache-Stale-TTL-Seconds header set: ${staleTTLHeader} (${url})`);
|
|
190
207
|
const staleTTL = parseInt(staleTTLHeader, 10);
|
|
191
208
|
if (isNaN(staleTTL) || staleTTL <= 0) {
|
|
192
209
|
return null;
|
|
@@ -217,7 +234,12 @@ export function matchesScope(url, scope, defaultTTLSeconds) {
|
|
|
217
234
|
*/
|
|
218
235
|
export async function invalidateCache(cacheName, urls) {
|
|
219
236
|
const cache = await caches.open(cacheName);
|
|
220
|
-
|
|
237
|
+
const deletePromises = urls.map(async (url) => {
|
|
238
|
+
log(`Cache invalidated: ${url}`);
|
|
239
|
+
const deleted = await cache.delete(url);
|
|
240
|
+
return deleted;
|
|
241
|
+
});
|
|
242
|
+
await Promise.allSettled(deletePromises);
|
|
221
243
|
}
|
|
222
244
|
|
|
223
245
|
/**
|
|
@@ -256,15 +278,36 @@ export async function cleanupOldCacheEntries(cacheName, maxAgeSeconds) {
|
|
|
256
278
|
const cache = await caches.open(cacheName);
|
|
257
279
|
const keys = await cache.keys();
|
|
258
280
|
const cleanupPromises = [];
|
|
281
|
+
const cleanedUrls = [];
|
|
259
282
|
|
|
260
283
|
for (const request of keys) {
|
|
261
284
|
const response = await cache.match(request);
|
|
262
285
|
if (response && isOlderThanMaxAge(response, maxAgeSeconds)) {
|
|
286
|
+
const url = request.url || request.toString();
|
|
287
|
+
cleanedUrls.push(url);
|
|
263
288
|
cleanupPromises.push(cache.delete(request));
|
|
264
289
|
}
|
|
265
290
|
}
|
|
266
291
|
|
|
267
292
|
await Promise.allSettled(cleanupPromises);
|
|
293
|
+
if (cleanedUrls.length > 0) {
|
|
294
|
+
cleanedUrls.forEach((url) => {
|
|
295
|
+
log(`Cache entry cleaned up (maxAge): ${url}`);
|
|
296
|
+
});
|
|
297
|
+
log(
|
|
298
|
+
`Cleaned up ${cleanedUrls.length} cache entr${cleanedUrls.length === 1 ? "y" : "ies"} due to maxAge`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Log a message if logging is enabled
|
|
305
|
+
* @param {string} message - Message to log
|
|
306
|
+
*/
|
|
307
|
+
export function log(message) {
|
|
308
|
+
if (loggingEnabled) {
|
|
309
|
+
console.log(`[swimple] ${message}`);
|
|
310
|
+
}
|
|
268
311
|
}
|
|
269
312
|
|
|
270
313
|
/**
|
|
@@ -301,4 +344,10 @@ export function validateConfig(config) {
|
|
|
301
344
|
"config.maxCacheAgeSeconds must be a positive number if provided"
|
|
302
345
|
);
|
|
303
346
|
}
|
|
347
|
+
if (
|
|
348
|
+
cfg.enableLogging !== undefined &&
|
|
349
|
+
typeof cfg.enableLogging !== "boolean"
|
|
350
|
+
) {
|
|
351
|
+
throw new Error("config.enableLogging must be a boolean if provided");
|
|
352
|
+
}
|
|
304
353
|
}
|
package/index.js
CHANGED
|
@@ -23,14 +23,19 @@ import {
|
|
|
23
23
|
clearCache,
|
|
24
24
|
validateConfig,
|
|
25
25
|
isOlderThanMaxAge,
|
|
26
|
-
cleanupOldCacheEntries
|
|
26
|
+
cleanupOldCacheEntries,
|
|
27
|
+
setLoggingEnabled,
|
|
28
|
+
log
|
|
27
29
|
} from "./helpers.js";
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
32
|
* Creates a request handler function for service worker fetch events.
|
|
33
|
+
* The handler implements HTTP caching with configurable strategies (cache-first, network-first, stale-while-revalidate),
|
|
34
|
+
* automatic cache invalidation for mutations, and periodic cache cleanup. Only handles same-origin GET requests
|
|
35
|
+
* that match the configured scope.
|
|
31
36
|
*
|
|
32
|
-
* @param {HandleRequestConfig} config - Configuration options
|
|
33
|
-
* @returns {(event: FetchEvent) => Promise<Response> | null} Request handler function
|
|
37
|
+
* @param {HandleRequestConfig} config - Configuration options including cache name, scope, default strategy, and TTL settings
|
|
38
|
+
* @returns {(event: FetchEvent) => Promise<Response> | null} Request handler function that can be used in service worker fetch event listeners
|
|
34
39
|
*/
|
|
35
40
|
export function createHandleRequest(config) {
|
|
36
41
|
validateConfig(config);
|
|
@@ -46,11 +51,22 @@ export function createHandleRequest(config) {
|
|
|
46
51
|
const maxCacheAgeSeconds = config.maxCacheAgeSeconds ?? 7200;
|
|
47
52
|
const inferInvalidation = config.inferInvalidation ?? true;
|
|
48
53
|
const customFetch = config.customFetch || fetch;
|
|
54
|
+
const enableLogging = config.enableLogging ?? false;
|
|
55
|
+
|
|
56
|
+
// Set module-level logging state
|
|
57
|
+
setLoggingEnabled(enableLogging);
|
|
49
58
|
|
|
50
59
|
// Track fetch counter for periodic cleanup
|
|
51
60
|
let fetchCounter = 0;
|
|
52
61
|
|
|
53
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Service worker fetch event handler that implements HTTP caching strategies.
|
|
64
|
+
* Handles cache invalidation for mutations, implements cache-first/network-first/stale-while-revalidate
|
|
65
|
+
* strategies for GET requests, and performs automatic cache cleanup.
|
|
66
|
+
*
|
|
67
|
+
* @param {FetchEvent} event - The fetch event from the service worker
|
|
68
|
+
* @returns {Promise<Response> | null} The cached or fetched response, or null if request shouldn't be handled
|
|
69
|
+
*/
|
|
54
70
|
return function handleRequest(event) {
|
|
55
71
|
const request = event.request;
|
|
56
72
|
const url = request.url;
|
|
@@ -59,6 +75,7 @@ export function createHandleRequest(config) {
|
|
|
59
75
|
|
|
60
76
|
// Handle cache clearing - header presence (any value) triggers cache clear
|
|
61
77
|
if (getHeader(headers, "X-SW-Cache-Clear") !== null) {
|
|
78
|
+
log(`X-SW-Cache-Clear header set: ${url}`);
|
|
62
79
|
return (async () => {
|
|
63
80
|
await clearCache(cacheName);
|
|
64
81
|
return customFetch(request);
|
|
@@ -70,6 +87,12 @@ export function createHandleRequest(config) {
|
|
|
70
87
|
const invalidateHeaders = getAllHeaders(headers, "X-SW-Cache-Invalidate");
|
|
71
88
|
const isMutation = ["POST", "PATCH", "PUT", "DELETE"].includes(method);
|
|
72
89
|
|
|
90
|
+
if (invalidateHeaders.length > 0) {
|
|
91
|
+
invalidateHeaders.forEach((path) => {
|
|
92
|
+
log(`X-SW-Cache-Invalidate header set: ${path} (${url})`);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
73
96
|
if (invalidateHeaders.length > 0 || (inferInvalidation && isMutation)) {
|
|
74
97
|
return (async () => {
|
|
75
98
|
let pathsToInvalidate = [...invalidateHeaders];
|
|
@@ -93,6 +116,29 @@ export function createHandleRequest(config) {
|
|
|
93
116
|
return null;
|
|
94
117
|
}
|
|
95
118
|
|
|
119
|
+
// Only cache same-origin requests - cross-origin requests are not cached
|
|
120
|
+
// In service worker context, self.location.origin is the service worker's origin
|
|
121
|
+
try {
|
|
122
|
+
const requestUrl = new URL(url);
|
|
123
|
+
// Check if we're in a service worker context and can determine the origin
|
|
124
|
+
if (
|
|
125
|
+
typeof self !== "undefined" &&
|
|
126
|
+
self.location &&
|
|
127
|
+
self.location.origin
|
|
128
|
+
) {
|
|
129
|
+
const serviceWorkerOrigin = self.location.origin;
|
|
130
|
+
// If request origin doesn't match service worker origin, don't cache
|
|
131
|
+
if (requestUrl.origin !== serviceWorkerOrigin) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// In test environments where self.location might not be available,
|
|
136
|
+
// we rely on the test setup to ensure proper origin handling
|
|
137
|
+
} catch (error) {
|
|
138
|
+
// If URL parsing fails, don't cache
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
96
142
|
// Periodic cleanup: run on first fetch and every 100 fetches
|
|
97
143
|
fetchCounter++;
|
|
98
144
|
if (fetchCounter === 1 || fetchCounter % 100 === 0) {
|
|
@@ -108,7 +154,7 @@ export function createHandleRequest(config) {
|
|
|
108
154
|
// Check if request matches scope and should be cached
|
|
109
155
|
const hasExplicitTTLHeader =
|
|
110
156
|
getHeader(headers, "X-SW-Cache-TTL-Seconds") !== null;
|
|
111
|
-
const ttl = getTTL(headers, defaultTTLSeconds);
|
|
157
|
+
const ttl = getTTL(headers, defaultTTLSeconds, url);
|
|
112
158
|
|
|
113
159
|
// If scope doesn't match and there's no explicit TTL header, don't handle the request
|
|
114
160
|
if (!matchesScope(url, scope, defaultTTLSeconds) && !hasExplicitTTLHeader) {
|
|
@@ -120,8 +166,8 @@ export function createHandleRequest(config) {
|
|
|
120
166
|
return null;
|
|
121
167
|
}
|
|
122
168
|
|
|
123
|
-
const staleTTL = getStaleTTL(headers, defaultStaleTTLSeconds);
|
|
124
|
-
const strategy = getStrategy(headers, defaultStrategy);
|
|
169
|
+
const staleTTL = getStaleTTL(headers, defaultStaleTTLSeconds, url);
|
|
170
|
+
const strategy = getStrategy(headers, defaultStrategy, url);
|
|
125
171
|
|
|
126
172
|
// Handle cache-first strategy
|
|
127
173
|
if (strategy === "cache-first") {
|
|
@@ -131,22 +177,30 @@ export function createHandleRequest(config) {
|
|
|
131
177
|
|
|
132
178
|
if (cachedResponse) {
|
|
133
179
|
if (isFresh(cachedResponse, ttl)) {
|
|
180
|
+
log(`Cache hit: ${url}`);
|
|
134
181
|
return cachedResponse;
|
|
135
182
|
}
|
|
136
183
|
|
|
137
184
|
// Reactive cleanup: delete if older than maxCacheAgeSeconds
|
|
138
185
|
if (isOlderThanMaxAge(cachedResponse, maxCacheAgeSeconds)) {
|
|
186
|
+
log(`Cache entry cleaned up (maxAge): ${url}`);
|
|
139
187
|
await cache.delete(request); // Fire-and-forget cleanup
|
|
140
188
|
}
|
|
141
189
|
}
|
|
142
190
|
|
|
143
191
|
// No fresh cache, fetch from network
|
|
192
|
+
if (!cachedResponse) {
|
|
193
|
+
log(`Cache miss: ${url}`);
|
|
194
|
+
} else if (!isStale(cachedResponse, ttl, staleTTL)) {
|
|
195
|
+
log(`Cache miss (stale): ${url}`);
|
|
196
|
+
}
|
|
144
197
|
let networkResponse;
|
|
145
198
|
try {
|
|
146
199
|
networkResponse = await customFetch(request);
|
|
147
200
|
} catch (error) {
|
|
148
201
|
// Network failed, return stale cache if available
|
|
149
202
|
if (cachedResponse && isStale(cachedResponse, ttl, staleTTL)) {
|
|
203
|
+
log(`Cache hit (stale, offline): ${url}`);
|
|
150
204
|
return cachedResponse;
|
|
151
205
|
}
|
|
152
206
|
throw error;
|
|
@@ -175,6 +229,7 @@ export function createHandleRequest(config) {
|
|
|
175
229
|
if (cachedResponse) {
|
|
176
230
|
// Reactive cleanup: delete if older than maxCacheAgeSeconds
|
|
177
231
|
if (isOlderThanMaxAge(cachedResponse, maxCacheAgeSeconds)) {
|
|
232
|
+
log(`Cache entry cleaned up (maxAge): ${url}`);
|
|
178
233
|
cache.delete(request); // Fire-and-forget cleanup
|
|
179
234
|
throw error;
|
|
180
235
|
}
|
|
@@ -182,9 +237,11 @@ export function createHandleRequest(config) {
|
|
|
182
237
|
isFresh(cachedResponse, ttl) ||
|
|
183
238
|
isStale(cachedResponse, ttl, staleTTL)
|
|
184
239
|
) {
|
|
240
|
+
log(`Cache hit (offline): ${url}`);
|
|
185
241
|
return cachedResponse;
|
|
186
242
|
}
|
|
187
243
|
}
|
|
244
|
+
log(`Cache miss (offline): ${url}`);
|
|
188
245
|
throw error;
|
|
189
246
|
}
|
|
190
247
|
|
|
@@ -206,6 +263,7 @@ export function createHandleRequest(config) {
|
|
|
206
263
|
if (cachedResponse) {
|
|
207
264
|
// Reactive cleanup: delete if older than maxCacheAgeSeconds
|
|
208
265
|
if (isOlderThanMaxAge(cachedResponse, maxCacheAgeSeconds)) {
|
|
266
|
+
log(`Cache entry cleaned up (maxAge): ${url}`);
|
|
209
267
|
cache.delete(request); // Fire-and-forget cleanup
|
|
210
268
|
// Continue to fetch from network
|
|
211
269
|
} else {
|
|
@@ -213,6 +271,11 @@ export function createHandleRequest(config) {
|
|
|
213
271
|
const stale = isStale(cachedResponse, ttl, staleTTL);
|
|
214
272
|
|
|
215
273
|
if (fresh || stale) {
|
|
274
|
+
if (fresh) {
|
|
275
|
+
log(`Cache hit: ${url}`);
|
|
276
|
+
} else {
|
|
277
|
+
log(`Cache hit (stale): ${url}`);
|
|
278
|
+
}
|
|
216
279
|
// Return cached response immediately
|
|
217
280
|
// Update cache in background if stale
|
|
218
281
|
if (stale) {
|
|
@@ -237,6 +300,11 @@ export function createHandleRequest(config) {
|
|
|
237
300
|
// No cache or too stale, fetch from network, no need for fallback if offline
|
|
238
301
|
// because we already know if there was a cached response it won't be
|
|
239
302
|
// fresh or stale if we've reached this point
|
|
303
|
+
if (!cachedResponse) {
|
|
304
|
+
log(`Cache miss: ${url}`);
|
|
305
|
+
} else {
|
|
306
|
+
log(`Cache miss (too stale): ${url}`);
|
|
307
|
+
}
|
|
240
308
|
const networkResponse = await customFetch(request);
|
|
241
309
|
|
|
242
310
|
// Cache the response if successful
|
package/package.json
CHANGED
package/types.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type CacheStrategy =
|
|
|
12
12
|
export interface HandleRequestConfig {
|
|
13
13
|
/** Name of the cache, used when calling `Cache.open(cacheName)` internally. Changing this name effectively clears the previous cache entries. */
|
|
14
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. */
|
|
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. Note: Cross-origin requests are never cached, regardless of scope or TTL headers. */
|
|
16
16
|
scope?: string[];
|
|
17
17
|
/** Default caching strategy: `'cache-first'`, `'network-first'`, or `'stale-while-revalidate'`. */
|
|
18
18
|
defaultStrategy?: CacheStrategy;
|
|
@@ -26,4 +26,6 @@ export interface HandleRequestConfig {
|
|
|
26
26
|
customFetch?: typeof fetch;
|
|
27
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
28
|
maxCacheAgeSeconds?: number;
|
|
29
|
+
/** Enable logging for cache hits, misses, header usage, invalidation, and cleanup. Defaults to false. */
|
|
30
|
+
enableLogging?: boolean;
|
|
29
31
|
}
|