swimple 0.3.0 → 0.5.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 +74 -4
- package/headers.js +50 -0
- package/helpers.js +50 -24
- package/index.js +28 -23
- package/package.json +3 -2
- package/types.d.ts +10 -2
package/README.md
CHANGED
|
@@ -163,17 +163,28 @@ The `customFetch` function has the same signature as the native `fetch` function
|
|
|
163
163
|
|
|
164
164
|
### Example 5: Enable Logging
|
|
165
165
|
|
|
166
|
-
You can enable logging to debug cache behavior.
|
|
166
|
+
You can enable logging to debug cache behavior. The library supports three logging levels:
|
|
167
|
+
|
|
168
|
+
- `"none"` (default): No logging
|
|
169
|
+
- `"minimal"`: console.info logs cache hits and cache invalidation only
|
|
170
|
+
- `"verbose"`: console.debug logs all events including cache misses, header usage, and cleanup
|
|
167
171
|
|
|
168
172
|
```javascript
|
|
169
173
|
const handleRequest = createHandleRequest({
|
|
170
174
|
cacheName: "api-cache-v1",
|
|
171
175
|
scope: ["/api/"],
|
|
172
|
-
|
|
176
|
+
loggingLevel: "verbose" // or "minimal" for less verbose output
|
|
173
177
|
});
|
|
174
178
|
```
|
|
175
179
|
|
|
176
|
-
|
|
180
|
+
With `loggingLevel: "minimal"`, you'll see:
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
[swimple] Cache hit: https://example.com/api/users
|
|
184
|
+
[swimple] Cache invalidated: https://example.com/api/users/123
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
With `loggingLevel: "verbose"`, you'll see all events:
|
|
177
188
|
|
|
178
189
|
```
|
|
179
190
|
[swimple] Cache hit: https://example.com/api/users
|
|
@@ -255,7 +266,7 @@ Creates a request handler function for your service worker fetch handler.
|
|
|
255
266
|
| `inferInvalidation` | `boolean` | No | `true` | Automatically invalidate cache on POST/PATCH/PUT/DELETE requests. |
|
|
256
267
|
| `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. |
|
|
257
268
|
| `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
|
-
| `
|
|
269
|
+
| `loggingLevel` | `string` | No | `"none"` | Logging level: `"none"` (no logging), `"minimal"` (cache hits and invalidation only), or `"verbose"` (all logging including misses, header usage, and cleanup). When enabled, logs are written to the console with the `[swimple]` prefix. Useful for debugging cache behavior. |
|
|
259
270
|
|
|
260
271
|
#### Returns
|
|
261
272
|
|
|
@@ -405,6 +416,65 @@ fetch("/api/logout", {
|
|
|
405
416
|
|
|
406
417
|
**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.
|
|
407
418
|
|
|
419
|
+
## Header Constants
|
|
420
|
+
|
|
421
|
+
For convenience, swimple exports header name constants that you can use in your non-service-worker code (e.g., client-side JavaScript) to avoid hard-coding header names. This helps prevent typos and makes refactoring easier.
|
|
422
|
+
|
|
423
|
+
### Importing Header Constants
|
|
424
|
+
|
|
425
|
+
#### Node.js / npm Import
|
|
426
|
+
|
|
427
|
+
```javascript
|
|
428
|
+
import {
|
|
429
|
+
CACHE_STRATEGY_HEADER,
|
|
430
|
+
CACHE_TTL_HEADER,
|
|
431
|
+
CACHE_STALE_TTL_HEADER,
|
|
432
|
+
CACHE_INVALIDATE_HEADER,
|
|
433
|
+
CACHE_CLEAR_HEADER
|
|
434
|
+
} from "swimple/headers";
|
|
435
|
+
|
|
436
|
+
// Use in your fetch calls
|
|
437
|
+
fetch("/api/users", {
|
|
438
|
+
headers: {
|
|
439
|
+
[CACHE_STRATEGY_HEADER]: "network-first",
|
|
440
|
+
[CACHE_TTL_HEADER]: "600"
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
#### CDN Import
|
|
446
|
+
|
|
447
|
+
```javascript
|
|
448
|
+
import {
|
|
449
|
+
CACHE_STRATEGY_HEADER,
|
|
450
|
+
CACHE_TTL_HEADER,
|
|
451
|
+
CACHE_STALE_TTL_HEADER,
|
|
452
|
+
CACHE_INVALIDATE_HEADER,
|
|
453
|
+
CACHE_CLEAR_HEADER
|
|
454
|
+
} from "https://cdn.jsdelivr.net/npm/swimple@1.0.0/headers.js";
|
|
455
|
+
|
|
456
|
+
// Use in your fetch calls
|
|
457
|
+
const headers = new Headers();
|
|
458
|
+
headers.set(CACHE_TTL_HEADER, "300");
|
|
459
|
+
headers.append(CACHE_INVALIDATE_HEADER, "/api/users");
|
|
460
|
+
headers.append(CACHE_INVALIDATE_HEADER, "/api/posts");
|
|
461
|
+
|
|
462
|
+
fetch("/api/users/123", {
|
|
463
|
+
method: "PATCH",
|
|
464
|
+
headers
|
|
465
|
+
});
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### Available Constants
|
|
469
|
+
|
|
470
|
+
- `CACHE_STRATEGY_HEADER` - `"X-SW-Cache-Strategy"`
|
|
471
|
+
- `CACHE_TTL_HEADER` - `"X-SW-Cache-TTL-Seconds"`
|
|
472
|
+
- `CACHE_STALE_TTL_HEADER` - `"X-SW-Cache-Stale-TTL-Seconds"`
|
|
473
|
+
- `CACHE_INVALIDATE_HEADER` - `"X-SW-Cache-Invalidate"`
|
|
474
|
+
- `CACHE_CLEAR_HEADER` - `"X-SW-Cache-Clear"`
|
|
475
|
+
|
|
476
|
+
**Note:** There is also an internal `CACHE_TIMESTAMP_HEADER` constant (`"x-sw-cache-timestamp"`), but this is used internally by the library and should not be set manually.
|
|
477
|
+
|
|
408
478
|
## Caching Behavior
|
|
409
479
|
|
|
410
480
|
### Automatic Caching (Default)
|
package/headers.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Header constants used by swimple for cache control.
|
|
5
|
+
* These constants can be imported in non-service-worker code to set headers on requests.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // Node.js / npm import
|
|
9
|
+
* import { CACHE_STRATEGY_HEADER, CACHE_TTL_HEADER } from "swimple/headers";
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // CDN import
|
|
13
|
+
* import { CACHE_STRATEGY_HEADER, CACHE_TTL_HEADER } from "https://cdn.jsdelivr.net/npm/swimple@1.0.0/headers.js";
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Header name for overriding the caching strategy for a specific request.
|
|
18
|
+
* Values: "cache-first", "network-first", or "stale-while-revalidate"
|
|
19
|
+
*/
|
|
20
|
+
export const CACHE_STRATEGY_HEADER = "X-SW-Cache-Strategy";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Header name for setting the time-to-live (in seconds) for a cached response.
|
|
24
|
+
* Set to "0" to completely opt out of caching for a specific request.
|
|
25
|
+
*/
|
|
26
|
+
export const CACHE_TTL_HEADER = "X-SW-Cache-TTL-Seconds";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Header name for setting the stale time-to-live (in seconds) for a cached response.
|
|
30
|
+
* Used by cache-first (when offline), network-first (when offline), and stale-while-revalidate strategies.
|
|
31
|
+
*/
|
|
32
|
+
export const CACHE_STALE_TTL_HEADER = "X-SW-Cache-Stale-TTL-Seconds";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Header name for explicitly invalidating specific cache entries.
|
|
36
|
+
* Can be set multiple times to invalidate multiple paths.
|
|
37
|
+
*/
|
|
38
|
+
export const CACHE_INVALIDATE_HEADER = "X-SW-Cache-Invalidate";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Header name for clearing the entire cache.
|
|
42
|
+
* Any value works - the header's presence triggers cache clearing.
|
|
43
|
+
*/
|
|
44
|
+
export const CACHE_CLEAR_HEADER = "X-SW-Cache-Clear";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Internal header name used to store the cache timestamp in cached responses.
|
|
48
|
+
* This header is set automatically by the library and should not be set manually.
|
|
49
|
+
*/
|
|
50
|
+
export const CACHE_TIMESTAMP_HEADER = "x-sw-cache-timestamp";
|
package/helpers.js
CHANGED
|
@@ -6,17 +6,25 @@
|
|
|
6
6
|
/// <reference lib="esnext" />
|
|
7
7
|
/// <reference lib="webworker" />
|
|
8
8
|
|
|
9
|
-
/** @import { CacheStrategy, HandleRequestConfig } from "./types" */
|
|
9
|
+
/** @import { CacheStrategy, HandleRequestConfig, LoggingLevel } from "./types" */
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
CACHE_TIMESTAMP_HEADER,
|
|
13
|
+
CACHE_STRATEGY_HEADER,
|
|
14
|
+
CACHE_TTL_HEADER,
|
|
15
|
+
CACHE_STALE_TTL_HEADER
|
|
16
|
+
} from "./headers.js";
|
|
10
17
|
|
|
11
18
|
// Module-level logging state
|
|
12
|
-
|
|
19
|
+
/** @type {LoggingLevel} */
|
|
20
|
+
let loggingLevel = "none";
|
|
13
21
|
|
|
14
22
|
/**
|
|
15
|
-
* Set
|
|
16
|
-
* @param {
|
|
23
|
+
* Set the logging level
|
|
24
|
+
* @param {LoggingLevel} level - Logging level: "none", "minimal", or "verbose"
|
|
17
25
|
*/
|
|
18
|
-
export function
|
|
19
|
-
|
|
26
|
+
export function setLoggingLevel(level) {
|
|
27
|
+
loggingLevel = level;
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
/**
|
|
@@ -70,7 +78,7 @@ export function getAllHeaders(headers, name) {
|
|
|
70
78
|
* @returns {number | null} Timestamp in milliseconds since epoch, or null if not found
|
|
71
79
|
*/
|
|
72
80
|
export function getCacheTimestamp(response) {
|
|
73
|
-
const timestampHeader = getHeader(response.headers,
|
|
81
|
+
const timestampHeader = getHeader(response.headers, CACHE_TIMESTAMP_HEADER);
|
|
74
82
|
if (!timestampHeader) {
|
|
75
83
|
return null;
|
|
76
84
|
}
|
|
@@ -119,7 +127,7 @@ export function isStale(response, ttl, staleTTL) {
|
|
|
119
127
|
*/
|
|
120
128
|
export function addTimestamp(response) {
|
|
121
129
|
const headers = new Headers(response.headers);
|
|
122
|
-
headers.set(
|
|
130
|
+
headers.set(CACHE_TIMESTAMP_HEADER, Date.now().toString());
|
|
123
131
|
return new Response(response.body, {
|
|
124
132
|
status: response.status,
|
|
125
133
|
statusText: response.statusText,
|
|
@@ -158,14 +166,16 @@ export function getInferredInvalidationPaths(url) {
|
|
|
158
166
|
* @returns {CacheStrategy}
|
|
159
167
|
*/
|
|
160
168
|
export function getStrategy(headers, defaultStrategy, url = "") {
|
|
161
|
-
const strategyHeader = getHeader(headers,
|
|
169
|
+
const strategyHeader = getHeader(headers, CACHE_STRATEGY_HEADER);
|
|
162
170
|
if (
|
|
163
171
|
strategyHeader &&
|
|
164
172
|
["cache-first", "network-first", "stale-while-revalidate"].includes(
|
|
165
173
|
strategyHeader
|
|
166
174
|
)
|
|
167
175
|
) {
|
|
168
|
-
|
|
176
|
+
logVerbose(
|
|
177
|
+
`${CACHE_STRATEGY_HEADER} header set: ${strategyHeader} (${url})`
|
|
178
|
+
);
|
|
169
179
|
return /** @type {CacheStrategy} */ (strategyHeader);
|
|
170
180
|
}
|
|
171
181
|
return defaultStrategy;
|
|
@@ -179,11 +189,11 @@ export function getStrategy(headers, defaultStrategy, url = "") {
|
|
|
179
189
|
* @returns {number | null} TTL in seconds, or null if caching is disabled
|
|
180
190
|
*/
|
|
181
191
|
export function getTTL(headers, defaultTTL, url = "") {
|
|
182
|
-
const ttlHeader = getHeader(headers,
|
|
192
|
+
const ttlHeader = getHeader(headers, CACHE_TTL_HEADER);
|
|
183
193
|
if (ttlHeader === null) {
|
|
184
194
|
return defaultTTL > 0 ? defaultTTL : null;
|
|
185
195
|
}
|
|
186
|
-
|
|
196
|
+
logVerbose(`${CACHE_TTL_HEADER} header set: ${ttlHeader} (${url})`);
|
|
187
197
|
const ttl = parseInt(ttlHeader, 10);
|
|
188
198
|
if (isNaN(ttl) || ttl <= 0) {
|
|
189
199
|
return null;
|
|
@@ -199,11 +209,13 @@ export function getTTL(headers, defaultTTL, url = "") {
|
|
|
199
209
|
* @returns {number | null} Stale TTL in seconds, or null if stale caching is disabled
|
|
200
210
|
*/
|
|
201
211
|
export function getStaleTTL(headers, defaultStaleTTL, url = "") {
|
|
202
|
-
const staleTTLHeader = getHeader(headers,
|
|
212
|
+
const staleTTLHeader = getHeader(headers, CACHE_STALE_TTL_HEADER);
|
|
203
213
|
if (staleTTLHeader === null) {
|
|
204
214
|
return defaultStaleTTL > 0 ? defaultStaleTTL : null;
|
|
205
215
|
}
|
|
206
|
-
|
|
216
|
+
logVerbose(
|
|
217
|
+
`${CACHE_STALE_TTL_HEADER} header set: ${staleTTLHeader} (${url})`
|
|
218
|
+
);
|
|
207
219
|
const staleTTL = parseInt(staleTTLHeader, 10);
|
|
208
220
|
if (isNaN(staleTTL) || staleTTL <= 0) {
|
|
209
221
|
return null;
|
|
@@ -235,8 +247,10 @@ export function matchesScope(url, scope, defaultTTLSeconds) {
|
|
|
235
247
|
export async function invalidateCache(cacheName, urls) {
|
|
236
248
|
const cache = await caches.open(cacheName);
|
|
237
249
|
const deletePromises = urls.map(async (url) => {
|
|
238
|
-
log(`Cache invalidated: ${url}`);
|
|
239
250
|
const deleted = await cache.delete(url);
|
|
251
|
+
if (deleted) {
|
|
252
|
+
logInfo(`Cache invalidated: ${url}`);
|
|
253
|
+
}
|
|
240
254
|
return deleted;
|
|
241
255
|
});
|
|
242
256
|
await Promise.allSettled(deletePromises);
|
|
@@ -292,21 +306,31 @@ export async function cleanupOldCacheEntries(cacheName, maxAgeSeconds) {
|
|
|
292
306
|
await Promise.allSettled(cleanupPromises);
|
|
293
307
|
if (cleanedUrls.length > 0) {
|
|
294
308
|
cleanedUrls.forEach((url) => {
|
|
295
|
-
|
|
309
|
+
logVerbose(`Cache entry cleaned up (maxAge): ${url}`);
|
|
296
310
|
});
|
|
297
|
-
|
|
311
|
+
logVerbose(
|
|
298
312
|
`Cleaned up ${cleanedUrls.length} cache entr${cleanedUrls.length === 1 ? "y" : "ies"} due to maxAge`
|
|
299
313
|
);
|
|
300
314
|
}
|
|
301
315
|
}
|
|
302
316
|
|
|
303
317
|
/**
|
|
304
|
-
* Log
|
|
318
|
+
* Log an informational message (minimal and verbose levels)
|
|
305
319
|
* @param {string} message - Message to log
|
|
306
320
|
*/
|
|
307
|
-
export function
|
|
308
|
-
if (
|
|
309
|
-
console.
|
|
321
|
+
export function logInfo(message) {
|
|
322
|
+
if (loggingLevel === "minimal" || loggingLevel === "verbose") {
|
|
323
|
+
console.info(`[swimple] ${message}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Log a verbose message (verbose level only)
|
|
329
|
+
* @param {string} message - Message to log
|
|
330
|
+
*/
|
|
331
|
+
export function logVerbose(message) {
|
|
332
|
+
if (loggingLevel === "verbose") {
|
|
333
|
+
console.debug(`[swimple] ${message}`);
|
|
310
334
|
}
|
|
311
335
|
}
|
|
312
336
|
|
|
@@ -345,9 +369,11 @@ export function validateConfig(config) {
|
|
|
345
369
|
);
|
|
346
370
|
}
|
|
347
371
|
if (
|
|
348
|
-
cfg.
|
|
349
|
-
|
|
372
|
+
cfg.loggingLevel !== undefined &&
|
|
373
|
+
!["none", "minimal", "verbose"].includes(String(cfg.loggingLevel))
|
|
350
374
|
) {
|
|
351
|
-
throw new Error(
|
|
375
|
+
throw new Error(
|
|
376
|
+
'config.loggingLevel must be one of: "none", "minimal", "verbose"'
|
|
377
|
+
);
|
|
352
378
|
}
|
|
353
379
|
}
|
package/index.js
CHANGED
|
@@ -24,9 +24,15 @@ import {
|
|
|
24
24
|
validateConfig,
|
|
25
25
|
isOlderThanMaxAge,
|
|
26
26
|
cleanupOldCacheEntries,
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
setLoggingLevel,
|
|
28
|
+
logInfo,
|
|
29
|
+
logVerbose
|
|
29
30
|
} from "./helpers.js";
|
|
31
|
+
import {
|
|
32
|
+
CACHE_CLEAR_HEADER,
|
|
33
|
+
CACHE_INVALIDATE_HEADER,
|
|
34
|
+
CACHE_TTL_HEADER
|
|
35
|
+
} from "./headers.js";
|
|
30
36
|
|
|
31
37
|
/**
|
|
32
38
|
* Creates a request handler function for service worker fetch events.
|
|
@@ -51,10 +57,10 @@ export function createHandleRequest(config) {
|
|
|
51
57
|
const maxCacheAgeSeconds = config.maxCacheAgeSeconds ?? 7200;
|
|
52
58
|
const inferInvalidation = config.inferInvalidation ?? true;
|
|
53
59
|
const customFetch = config.customFetch || fetch;
|
|
54
|
-
const
|
|
60
|
+
const loggingLevel = config.loggingLevel ?? "none";
|
|
55
61
|
|
|
56
62
|
// Set module-level logging state
|
|
57
|
-
|
|
63
|
+
setLoggingLevel(loggingLevel);
|
|
58
64
|
|
|
59
65
|
// Track fetch counter for periodic cleanup
|
|
60
66
|
let fetchCounter = 0;
|
|
@@ -74,8 +80,8 @@ export function createHandleRequest(config) {
|
|
|
74
80
|
const headers = request.headers;
|
|
75
81
|
|
|
76
82
|
// Handle cache clearing - header presence (any value) triggers cache clear
|
|
77
|
-
if (getHeader(headers,
|
|
78
|
-
|
|
83
|
+
if (getHeader(headers, CACHE_CLEAR_HEADER) !== null) {
|
|
84
|
+
logVerbose(`${CACHE_CLEAR_HEADER} header set: ${url}`);
|
|
79
85
|
return (async () => {
|
|
80
86
|
await clearCache(cacheName);
|
|
81
87
|
return customFetch(request);
|
|
@@ -84,12 +90,12 @@ export function createHandleRequest(config) {
|
|
|
84
90
|
|
|
85
91
|
// Handle invalidation for mutations
|
|
86
92
|
// Check for explicit invalidation headers first (works even if inferInvalidation is false)
|
|
87
|
-
const invalidateHeaders = getAllHeaders(headers,
|
|
93
|
+
const invalidateHeaders = getAllHeaders(headers, CACHE_INVALIDATE_HEADER);
|
|
88
94
|
const isMutation = ["POST", "PATCH", "PUT", "DELETE"].includes(method);
|
|
89
95
|
|
|
90
96
|
if (invalidateHeaders.length > 0) {
|
|
91
97
|
invalidateHeaders.forEach((path) => {
|
|
92
|
-
|
|
98
|
+
logVerbose(`${CACHE_INVALIDATE_HEADER} header set: ${path} (${url})`);
|
|
93
99
|
});
|
|
94
100
|
}
|
|
95
101
|
|
|
@@ -152,8 +158,7 @@ export function createHandleRequest(config) {
|
|
|
152
158
|
}
|
|
153
159
|
|
|
154
160
|
// Check if request matches scope and should be cached
|
|
155
|
-
const hasExplicitTTLHeader =
|
|
156
|
-
getHeader(headers, "X-SW-Cache-TTL-Seconds") !== null;
|
|
161
|
+
const hasExplicitTTLHeader = getHeader(headers, CACHE_TTL_HEADER) !== null;
|
|
157
162
|
const ttl = getTTL(headers, defaultTTLSeconds, url);
|
|
158
163
|
|
|
159
164
|
// If scope doesn't match and there's no explicit TTL header, don't handle the request
|
|
@@ -177,22 +182,22 @@ export function createHandleRequest(config) {
|
|
|
177
182
|
|
|
178
183
|
if (cachedResponse) {
|
|
179
184
|
if (isFresh(cachedResponse, ttl)) {
|
|
180
|
-
|
|
185
|
+
logInfo(`Cache hit: ${url}`);
|
|
181
186
|
return cachedResponse;
|
|
182
187
|
}
|
|
183
188
|
|
|
184
189
|
// Reactive cleanup: delete if older than maxCacheAgeSeconds
|
|
185
190
|
if (isOlderThanMaxAge(cachedResponse, maxCacheAgeSeconds)) {
|
|
186
|
-
|
|
191
|
+
logVerbose(`Cache entry cleaned up (maxAge): ${url}`);
|
|
187
192
|
await cache.delete(request); // Fire-and-forget cleanup
|
|
188
193
|
}
|
|
189
194
|
}
|
|
190
195
|
|
|
191
196
|
// No fresh cache, fetch from network
|
|
192
197
|
if (!cachedResponse) {
|
|
193
|
-
|
|
198
|
+
logVerbose(`Cache miss: ${url}`);
|
|
194
199
|
} else if (!isStale(cachedResponse, ttl, staleTTL)) {
|
|
195
|
-
|
|
200
|
+
logVerbose(`Cache miss (stale): ${url}`);
|
|
196
201
|
}
|
|
197
202
|
let networkResponse;
|
|
198
203
|
try {
|
|
@@ -200,7 +205,7 @@ export function createHandleRequest(config) {
|
|
|
200
205
|
} catch (error) {
|
|
201
206
|
// Network failed, return stale cache if available
|
|
202
207
|
if (cachedResponse && isStale(cachedResponse, ttl, staleTTL)) {
|
|
203
|
-
|
|
208
|
+
logInfo(`Cache hit (stale, offline): ${url}`);
|
|
204
209
|
return cachedResponse;
|
|
205
210
|
}
|
|
206
211
|
throw error;
|
|
@@ -229,7 +234,7 @@ export function createHandleRequest(config) {
|
|
|
229
234
|
if (cachedResponse) {
|
|
230
235
|
// Reactive cleanup: delete if older than maxCacheAgeSeconds
|
|
231
236
|
if (isOlderThanMaxAge(cachedResponse, maxCacheAgeSeconds)) {
|
|
232
|
-
|
|
237
|
+
logVerbose(`Cache entry cleaned up (maxAge): ${url}`);
|
|
233
238
|
cache.delete(request); // Fire-and-forget cleanup
|
|
234
239
|
throw error;
|
|
235
240
|
}
|
|
@@ -237,11 +242,11 @@ export function createHandleRequest(config) {
|
|
|
237
242
|
isFresh(cachedResponse, ttl) ||
|
|
238
243
|
isStale(cachedResponse, ttl, staleTTL)
|
|
239
244
|
) {
|
|
240
|
-
|
|
245
|
+
logInfo(`Cache hit (offline): ${url}`);
|
|
241
246
|
return cachedResponse;
|
|
242
247
|
}
|
|
243
248
|
}
|
|
244
|
-
|
|
249
|
+
logVerbose(`Cache miss (offline): ${url}`);
|
|
245
250
|
throw error;
|
|
246
251
|
}
|
|
247
252
|
|
|
@@ -263,7 +268,7 @@ export function createHandleRequest(config) {
|
|
|
263
268
|
if (cachedResponse) {
|
|
264
269
|
// Reactive cleanup: delete if older than maxCacheAgeSeconds
|
|
265
270
|
if (isOlderThanMaxAge(cachedResponse, maxCacheAgeSeconds)) {
|
|
266
|
-
|
|
271
|
+
logVerbose(`Cache entry cleaned up (maxAge): ${url}`);
|
|
267
272
|
cache.delete(request); // Fire-and-forget cleanup
|
|
268
273
|
// Continue to fetch from network
|
|
269
274
|
} else {
|
|
@@ -272,9 +277,9 @@ export function createHandleRequest(config) {
|
|
|
272
277
|
|
|
273
278
|
if (fresh || stale) {
|
|
274
279
|
if (fresh) {
|
|
275
|
-
|
|
280
|
+
logInfo(`Cache hit: ${url}`);
|
|
276
281
|
} else {
|
|
277
|
-
|
|
282
|
+
logInfo(`Cache hit (stale): ${url}`);
|
|
278
283
|
}
|
|
279
284
|
// Return cached response immediately
|
|
280
285
|
// Update cache in background if stale
|
|
@@ -301,9 +306,9 @@ export function createHandleRequest(config) {
|
|
|
301
306
|
// because we already know if there was a cached response it won't be
|
|
302
307
|
// fresh or stale if we've reached this point
|
|
303
308
|
if (!cachedResponse) {
|
|
304
|
-
|
|
309
|
+
logVerbose(`Cache miss: ${url}`);
|
|
305
310
|
} else {
|
|
306
|
-
|
|
311
|
+
logVerbose(`Cache miss (too stale): ${url}`);
|
|
307
312
|
}
|
|
308
313
|
const networkResponse = await customFetch(request);
|
|
309
314
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "swimple",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A simple service worker library for request caching",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"exports": {
|
|
8
|
-
".": "./index.js"
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./headers": "./headers.js"
|
|
9
10
|
},
|
|
10
11
|
"scripts": {
|
|
11
12
|
"test": "npm run test:unit && npm run test:e2e && npm run test:ui",
|
package/types.d.ts
CHANGED
|
@@ -9,6 +9,14 @@ export type CacheStrategy =
|
|
|
9
9
|
| "network-first"
|
|
10
10
|
| "stale-while-revalidate";
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Logging level for cache operations.
|
|
14
|
+
* - `none`: No logging
|
|
15
|
+
* - `minimal`: Logs cache hits and cache invalidation only
|
|
16
|
+
* - `verbose`: Logs all events including cache misses, cleanup, and header usage
|
|
17
|
+
*/
|
|
18
|
+
export type LoggingLevel = "none" | "minimal" | "verbose";
|
|
19
|
+
|
|
12
20
|
export interface HandleRequestConfig {
|
|
13
21
|
/** Name of the cache, used when calling `Cache.open(cacheName)` internally. Changing this name effectively clears the previous cache entries. */
|
|
14
22
|
cacheName: string;
|
|
@@ -26,6 +34,6 @@ export interface HandleRequestConfig {
|
|
|
26
34
|
customFetch?: typeof fetch;
|
|
27
35
|
/** 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
36
|
maxCacheAgeSeconds?: number;
|
|
29
|
-
/**
|
|
30
|
-
|
|
37
|
+
/** Logging level: "none" (no logging), "minimal" (cache hits and invalidation only), or "verbose" (all logging including misses, cleanup, and headers). Defaults to "none". */
|
|
38
|
+
loggingLevel?: LoggingLevel;
|
|
31
39
|
}
|