swimple 0.2.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 +25 -0
- package/helpers.js +53 -4
- package/index.js +39 -4
- package/package.json +1 -1
- package/types.d.ts +2 -0
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).
|
|
@@ -231,6 +255,7 @@ Creates a request handler function for your service worker fetch handler.
|
|
|
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
|
|
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,7 +23,9 @@ 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
|
/**
|
|
@@ -49,6 +51,10 @@ export function createHandleRequest(config) {
|
|
|
49
51
|
const maxCacheAgeSeconds = config.maxCacheAgeSeconds ?? 7200;
|
|
50
52
|
const inferInvalidation = config.inferInvalidation ?? true;
|
|
51
53
|
const customFetch = config.customFetch || fetch;
|
|
54
|
+
const enableLogging = config.enableLogging ?? false;
|
|
55
|
+
|
|
56
|
+
// Set module-level logging state
|
|
57
|
+
setLoggingEnabled(enableLogging);
|
|
52
58
|
|
|
53
59
|
// Track fetch counter for periodic cleanup
|
|
54
60
|
let fetchCounter = 0;
|
|
@@ -69,6 +75,7 @@ export function createHandleRequest(config) {
|
|
|
69
75
|
|
|
70
76
|
// Handle cache clearing - header presence (any value) triggers cache clear
|
|
71
77
|
if (getHeader(headers, "X-SW-Cache-Clear") !== null) {
|
|
78
|
+
log(`X-SW-Cache-Clear header set: ${url}`);
|
|
72
79
|
return (async () => {
|
|
73
80
|
await clearCache(cacheName);
|
|
74
81
|
return customFetch(request);
|
|
@@ -80,6 +87,12 @@ export function createHandleRequest(config) {
|
|
|
80
87
|
const invalidateHeaders = getAllHeaders(headers, "X-SW-Cache-Invalidate");
|
|
81
88
|
const isMutation = ["POST", "PATCH", "PUT", "DELETE"].includes(method);
|
|
82
89
|
|
|
90
|
+
if (invalidateHeaders.length > 0) {
|
|
91
|
+
invalidateHeaders.forEach((path) => {
|
|
92
|
+
log(`X-SW-Cache-Invalidate header set: ${path} (${url})`);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
83
96
|
if (invalidateHeaders.length > 0 || (inferInvalidation && isMutation)) {
|
|
84
97
|
return (async () => {
|
|
85
98
|
let pathsToInvalidate = [...invalidateHeaders];
|
|
@@ -141,7 +154,7 @@ export function createHandleRequest(config) {
|
|
|
141
154
|
// Check if request matches scope and should be cached
|
|
142
155
|
const hasExplicitTTLHeader =
|
|
143
156
|
getHeader(headers, "X-SW-Cache-TTL-Seconds") !== null;
|
|
144
|
-
const ttl = getTTL(headers, defaultTTLSeconds);
|
|
157
|
+
const ttl = getTTL(headers, defaultTTLSeconds, url);
|
|
145
158
|
|
|
146
159
|
// If scope doesn't match and there's no explicit TTL header, don't handle the request
|
|
147
160
|
if (!matchesScope(url, scope, defaultTTLSeconds) && !hasExplicitTTLHeader) {
|
|
@@ -153,8 +166,8 @@ export function createHandleRequest(config) {
|
|
|
153
166
|
return null;
|
|
154
167
|
}
|
|
155
168
|
|
|
156
|
-
const staleTTL = getStaleTTL(headers, defaultStaleTTLSeconds);
|
|
157
|
-
const strategy = getStrategy(headers, defaultStrategy);
|
|
169
|
+
const staleTTL = getStaleTTL(headers, defaultStaleTTLSeconds, url);
|
|
170
|
+
const strategy = getStrategy(headers, defaultStrategy, url);
|
|
158
171
|
|
|
159
172
|
// Handle cache-first strategy
|
|
160
173
|
if (strategy === "cache-first") {
|
|
@@ -164,22 +177,30 @@ export function createHandleRequest(config) {
|
|
|
164
177
|
|
|
165
178
|
if (cachedResponse) {
|
|
166
179
|
if (isFresh(cachedResponse, ttl)) {
|
|
180
|
+
log(`Cache hit: ${url}`);
|
|
167
181
|
return cachedResponse;
|
|
168
182
|
}
|
|
169
183
|
|
|
170
184
|
// Reactive cleanup: delete if older than maxCacheAgeSeconds
|
|
171
185
|
if (isOlderThanMaxAge(cachedResponse, maxCacheAgeSeconds)) {
|
|
186
|
+
log(`Cache entry cleaned up (maxAge): ${url}`);
|
|
172
187
|
await cache.delete(request); // Fire-and-forget cleanup
|
|
173
188
|
}
|
|
174
189
|
}
|
|
175
190
|
|
|
176
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
|
+
}
|
|
177
197
|
let networkResponse;
|
|
178
198
|
try {
|
|
179
199
|
networkResponse = await customFetch(request);
|
|
180
200
|
} catch (error) {
|
|
181
201
|
// Network failed, return stale cache if available
|
|
182
202
|
if (cachedResponse && isStale(cachedResponse, ttl, staleTTL)) {
|
|
203
|
+
log(`Cache hit (stale, offline): ${url}`);
|
|
183
204
|
return cachedResponse;
|
|
184
205
|
}
|
|
185
206
|
throw error;
|
|
@@ -208,6 +229,7 @@ export function createHandleRequest(config) {
|
|
|
208
229
|
if (cachedResponse) {
|
|
209
230
|
// Reactive cleanup: delete if older than maxCacheAgeSeconds
|
|
210
231
|
if (isOlderThanMaxAge(cachedResponse, maxCacheAgeSeconds)) {
|
|
232
|
+
log(`Cache entry cleaned up (maxAge): ${url}`);
|
|
211
233
|
cache.delete(request); // Fire-and-forget cleanup
|
|
212
234
|
throw error;
|
|
213
235
|
}
|
|
@@ -215,9 +237,11 @@ export function createHandleRequest(config) {
|
|
|
215
237
|
isFresh(cachedResponse, ttl) ||
|
|
216
238
|
isStale(cachedResponse, ttl, staleTTL)
|
|
217
239
|
) {
|
|
240
|
+
log(`Cache hit (offline): ${url}`);
|
|
218
241
|
return cachedResponse;
|
|
219
242
|
}
|
|
220
243
|
}
|
|
244
|
+
log(`Cache miss (offline): ${url}`);
|
|
221
245
|
throw error;
|
|
222
246
|
}
|
|
223
247
|
|
|
@@ -239,6 +263,7 @@ export function createHandleRequest(config) {
|
|
|
239
263
|
if (cachedResponse) {
|
|
240
264
|
// Reactive cleanup: delete if older than maxCacheAgeSeconds
|
|
241
265
|
if (isOlderThanMaxAge(cachedResponse, maxCacheAgeSeconds)) {
|
|
266
|
+
log(`Cache entry cleaned up (maxAge): ${url}`);
|
|
242
267
|
cache.delete(request); // Fire-and-forget cleanup
|
|
243
268
|
// Continue to fetch from network
|
|
244
269
|
} else {
|
|
@@ -246,6 +271,11 @@ export function createHandleRequest(config) {
|
|
|
246
271
|
const stale = isStale(cachedResponse, ttl, staleTTL);
|
|
247
272
|
|
|
248
273
|
if (fresh || stale) {
|
|
274
|
+
if (fresh) {
|
|
275
|
+
log(`Cache hit: ${url}`);
|
|
276
|
+
} else {
|
|
277
|
+
log(`Cache hit (stale): ${url}`);
|
|
278
|
+
}
|
|
249
279
|
// Return cached response immediately
|
|
250
280
|
// Update cache in background if stale
|
|
251
281
|
if (stale) {
|
|
@@ -270,6 +300,11 @@ export function createHandleRequest(config) {
|
|
|
270
300
|
// No cache or too stale, fetch from network, no need for fallback if offline
|
|
271
301
|
// because we already know if there was a cached response it won't be
|
|
272
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
|
+
}
|
|
273
308
|
const networkResponse = await customFetch(request);
|
|
274
309
|
|
|
275
310
|
// Cache the response if successful
|
package/package.json
CHANGED
package/types.d.ts
CHANGED
|
@@ -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
|
}
|