swimple 0.2.0 → 0.4.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 +36 -0
- package/helpers.js +71 -5
- package/index.js +40 -4
- package/package.json +1 -1
- package/types.d.ts +10 -0
package/README.md
CHANGED
|
@@ -161,6 +161,41 @@ 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. 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
|
|
171
|
+
|
|
172
|
+
```javascript
|
|
173
|
+
const handleRequest = createHandleRequest({
|
|
174
|
+
cacheName: "api-cache-v1",
|
|
175
|
+
scope: ["/api/"],
|
|
176
|
+
loggingLevel: "verbose" // or "minimal" for less verbose output
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
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:
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
[swimple] Cache hit: https://example.com/api/users
|
|
191
|
+
[swimple] Cache miss: https://example.com/api/posts
|
|
192
|
+
[swimple] X-SW-Cache-TTL-Seconds header set: 600 (https://example.com/api/users)
|
|
193
|
+
[swimple] Cache invalidated: https://example.com/api/users/123
|
|
194
|
+
[swimple] Cache entry cleaned up (maxAge): https://example.com/api/old-data
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
This is useful for debugging cache behavior and understanding when requests are served from cache vs. network.
|
|
198
|
+
|
|
164
199
|
## Clearing the cache on logout
|
|
165
200
|
|
|
166
201
|
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 +266,7 @@ Creates a request handler function for your service worker fetch handler.
|
|
|
231
266
|
| `inferInvalidation` | `boolean` | No | `true` | Automatically invalidate cache on POST/PATCH/PUT/DELETE requests. |
|
|
232
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. |
|
|
233
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). |
|
|
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. |
|
|
234
270
|
|
|
235
271
|
#### Returns
|
|
236
272
|
|
package/helpers.js
CHANGED
|
@@ -6,7 +6,19 @@
|
|
|
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
|
+
// Module-level logging state
|
|
12
|
+
/** @type {LoggingLevel} */
|
|
13
|
+
let loggingLevel = "none";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Set the logging level
|
|
17
|
+
* @param {LoggingLevel} level - Logging level: "none", "minimal", or "verbose"
|
|
18
|
+
*/
|
|
19
|
+
export function setLoggingLevel(level) {
|
|
20
|
+
loggingLevel = level;
|
|
21
|
+
}
|
|
10
22
|
|
|
11
23
|
/**
|
|
12
24
|
* Get header value (case-insensitive)
|
|
@@ -143,9 +155,10 @@ export function getInferredInvalidationPaths(url) {
|
|
|
143
155
|
* Get strategy from request headers or use default
|
|
144
156
|
* @param {Headers} headers
|
|
145
157
|
* @param {CacheStrategy} defaultStrategy
|
|
158
|
+
* @param {string} url - Request URL for logging
|
|
146
159
|
* @returns {CacheStrategy}
|
|
147
160
|
*/
|
|
148
|
-
export function getStrategy(headers, defaultStrategy) {
|
|
161
|
+
export function getStrategy(headers, defaultStrategy, url = "") {
|
|
149
162
|
const strategyHeader = getHeader(headers, "X-SW-Cache-Strategy");
|
|
150
163
|
if (
|
|
151
164
|
strategyHeader &&
|
|
@@ -153,6 +166,7 @@ export function getStrategy(headers, defaultStrategy) {
|
|
|
153
166
|
strategyHeader
|
|
154
167
|
)
|
|
155
168
|
) {
|
|
169
|
+
logVerbose(`X-SW-Cache-Strategy header set: ${strategyHeader} (${url})`);
|
|
156
170
|
return /** @type {CacheStrategy} */ (strategyHeader);
|
|
157
171
|
}
|
|
158
172
|
return defaultStrategy;
|
|
@@ -162,13 +176,15 @@ export function getStrategy(headers, defaultStrategy) {
|
|
|
162
176
|
* Get TTL from request headers or use default
|
|
163
177
|
* @param {Headers} headers
|
|
164
178
|
* @param {number} defaultTTL - Default TTL in seconds
|
|
179
|
+
* @param {string} url - Request URL for logging
|
|
165
180
|
* @returns {number | null} TTL in seconds, or null if caching is disabled
|
|
166
181
|
*/
|
|
167
|
-
export function getTTL(headers, defaultTTL) {
|
|
182
|
+
export function getTTL(headers, defaultTTL, url = "") {
|
|
168
183
|
const ttlHeader = getHeader(headers, "X-SW-Cache-TTL-Seconds");
|
|
169
184
|
if (ttlHeader === null) {
|
|
170
185
|
return defaultTTL > 0 ? defaultTTL : null;
|
|
171
186
|
}
|
|
187
|
+
logVerbose(`X-SW-Cache-TTL-Seconds header set: ${ttlHeader} (${url})`);
|
|
172
188
|
const ttl = parseInt(ttlHeader, 10);
|
|
173
189
|
if (isNaN(ttl) || ttl <= 0) {
|
|
174
190
|
return null;
|
|
@@ -180,13 +196,17 @@ export function getTTL(headers, defaultTTL) {
|
|
|
180
196
|
* Get stale TTL from request headers or use default
|
|
181
197
|
* @param {Headers} headers
|
|
182
198
|
* @param {number} defaultStaleTTL - Default stale TTL in seconds
|
|
199
|
+
* @param {string} url - Request URL for logging
|
|
183
200
|
* @returns {number | null} Stale TTL in seconds, or null if stale caching is disabled
|
|
184
201
|
*/
|
|
185
|
-
export function getStaleTTL(headers, defaultStaleTTL) {
|
|
202
|
+
export function getStaleTTL(headers, defaultStaleTTL, url = "") {
|
|
186
203
|
const staleTTLHeader = getHeader(headers, "X-SW-Cache-Stale-TTL-Seconds");
|
|
187
204
|
if (staleTTLHeader === null) {
|
|
188
205
|
return defaultStaleTTL > 0 ? defaultStaleTTL : null;
|
|
189
206
|
}
|
|
207
|
+
logVerbose(
|
|
208
|
+
`X-SW-Cache-Stale-TTL-Seconds header set: ${staleTTLHeader} (${url})`
|
|
209
|
+
);
|
|
190
210
|
const staleTTL = parseInt(staleTTLHeader, 10);
|
|
191
211
|
if (isNaN(staleTTL) || staleTTL <= 0) {
|
|
192
212
|
return null;
|
|
@@ -217,7 +237,14 @@ export function matchesScope(url, scope, defaultTTLSeconds) {
|
|
|
217
237
|
*/
|
|
218
238
|
export async function invalidateCache(cacheName, urls) {
|
|
219
239
|
const cache = await caches.open(cacheName);
|
|
220
|
-
|
|
240
|
+
const deletePromises = urls.map(async (url) => {
|
|
241
|
+
const deleted = await cache.delete(url);
|
|
242
|
+
if (deleted) {
|
|
243
|
+
logInfo(`Cache invalidated: ${url}`);
|
|
244
|
+
}
|
|
245
|
+
return deleted;
|
|
246
|
+
});
|
|
247
|
+
await Promise.allSettled(deletePromises);
|
|
221
248
|
}
|
|
222
249
|
|
|
223
250
|
/**
|
|
@@ -256,15 +283,46 @@ export async function cleanupOldCacheEntries(cacheName, maxAgeSeconds) {
|
|
|
256
283
|
const cache = await caches.open(cacheName);
|
|
257
284
|
const keys = await cache.keys();
|
|
258
285
|
const cleanupPromises = [];
|
|
286
|
+
const cleanedUrls = [];
|
|
259
287
|
|
|
260
288
|
for (const request of keys) {
|
|
261
289
|
const response = await cache.match(request);
|
|
262
290
|
if (response && isOlderThanMaxAge(response, maxAgeSeconds)) {
|
|
291
|
+
const url = request.url || request.toString();
|
|
292
|
+
cleanedUrls.push(url);
|
|
263
293
|
cleanupPromises.push(cache.delete(request));
|
|
264
294
|
}
|
|
265
295
|
}
|
|
266
296
|
|
|
267
297
|
await Promise.allSettled(cleanupPromises);
|
|
298
|
+
if (cleanedUrls.length > 0) {
|
|
299
|
+
cleanedUrls.forEach((url) => {
|
|
300
|
+
logVerbose(`Cache entry cleaned up (maxAge): ${url}`);
|
|
301
|
+
});
|
|
302
|
+
logVerbose(
|
|
303
|
+
`Cleaned up ${cleanedUrls.length} cache entr${cleanedUrls.length === 1 ? "y" : "ies"} due to maxAge`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Log an informational message (minimal and verbose levels)
|
|
310
|
+
* @param {string} message - Message to log
|
|
311
|
+
*/
|
|
312
|
+
export function logInfo(message) {
|
|
313
|
+
if (loggingLevel === "minimal" || loggingLevel === "verbose") {
|
|
314
|
+
console.info(`[swimple] ${message}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Log a verbose message (verbose level only)
|
|
320
|
+
* @param {string} message - Message to log
|
|
321
|
+
*/
|
|
322
|
+
export function logVerbose(message) {
|
|
323
|
+
if (loggingLevel === "verbose") {
|
|
324
|
+
console.debug(`[swimple] ${message}`);
|
|
325
|
+
}
|
|
268
326
|
}
|
|
269
327
|
|
|
270
328
|
/**
|
|
@@ -301,4 +359,12 @@ export function validateConfig(config) {
|
|
|
301
359
|
"config.maxCacheAgeSeconds must be a positive number if provided"
|
|
302
360
|
);
|
|
303
361
|
}
|
|
362
|
+
if (
|
|
363
|
+
cfg.loggingLevel !== undefined &&
|
|
364
|
+
!["none", "minimal", "verbose"].includes(String(cfg.loggingLevel))
|
|
365
|
+
) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
'config.loggingLevel must be one of: "none", "minimal", "verbose"'
|
|
368
|
+
);
|
|
369
|
+
}
|
|
304
370
|
}
|
package/index.js
CHANGED
|
@@ -23,7 +23,10 @@ import {
|
|
|
23
23
|
clearCache,
|
|
24
24
|
validateConfig,
|
|
25
25
|
isOlderThanMaxAge,
|
|
26
|
-
cleanupOldCacheEntries
|
|
26
|
+
cleanupOldCacheEntries,
|
|
27
|
+
setLoggingLevel,
|
|
28
|
+
logInfo,
|
|
29
|
+
logVerbose
|
|
27
30
|
} from "./helpers.js";
|
|
28
31
|
|
|
29
32
|
/**
|
|
@@ -49,6 +52,10 @@ export function createHandleRequest(config) {
|
|
|
49
52
|
const maxCacheAgeSeconds = config.maxCacheAgeSeconds ?? 7200;
|
|
50
53
|
const inferInvalidation = config.inferInvalidation ?? true;
|
|
51
54
|
const customFetch = config.customFetch || fetch;
|
|
55
|
+
const loggingLevel = config.loggingLevel ?? "none";
|
|
56
|
+
|
|
57
|
+
// Set module-level logging state
|
|
58
|
+
setLoggingLevel(loggingLevel);
|
|
52
59
|
|
|
53
60
|
// Track fetch counter for periodic cleanup
|
|
54
61
|
let fetchCounter = 0;
|
|
@@ -69,6 +76,7 @@ export function createHandleRequest(config) {
|
|
|
69
76
|
|
|
70
77
|
// Handle cache clearing - header presence (any value) triggers cache clear
|
|
71
78
|
if (getHeader(headers, "X-SW-Cache-Clear") !== null) {
|
|
79
|
+
logVerbose(`X-SW-Cache-Clear header set: ${url}`);
|
|
72
80
|
return (async () => {
|
|
73
81
|
await clearCache(cacheName);
|
|
74
82
|
return customFetch(request);
|
|
@@ -80,6 +88,12 @@ export function createHandleRequest(config) {
|
|
|
80
88
|
const invalidateHeaders = getAllHeaders(headers, "X-SW-Cache-Invalidate");
|
|
81
89
|
const isMutation = ["POST", "PATCH", "PUT", "DELETE"].includes(method);
|
|
82
90
|
|
|
91
|
+
if (invalidateHeaders.length > 0) {
|
|
92
|
+
invalidateHeaders.forEach((path) => {
|
|
93
|
+
logVerbose(`X-SW-Cache-Invalidate header set: ${path} (${url})`);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
83
97
|
if (invalidateHeaders.length > 0 || (inferInvalidation && isMutation)) {
|
|
84
98
|
return (async () => {
|
|
85
99
|
let pathsToInvalidate = [...invalidateHeaders];
|
|
@@ -141,7 +155,7 @@ export function createHandleRequest(config) {
|
|
|
141
155
|
// Check if request matches scope and should be cached
|
|
142
156
|
const hasExplicitTTLHeader =
|
|
143
157
|
getHeader(headers, "X-SW-Cache-TTL-Seconds") !== null;
|
|
144
|
-
const ttl = getTTL(headers, defaultTTLSeconds);
|
|
158
|
+
const ttl = getTTL(headers, defaultTTLSeconds, url);
|
|
145
159
|
|
|
146
160
|
// If scope doesn't match and there's no explicit TTL header, don't handle the request
|
|
147
161
|
if (!matchesScope(url, scope, defaultTTLSeconds) && !hasExplicitTTLHeader) {
|
|
@@ -153,8 +167,8 @@ export function createHandleRequest(config) {
|
|
|
153
167
|
return null;
|
|
154
168
|
}
|
|
155
169
|
|
|
156
|
-
const staleTTL = getStaleTTL(headers, defaultStaleTTLSeconds);
|
|
157
|
-
const strategy = getStrategy(headers, defaultStrategy);
|
|
170
|
+
const staleTTL = getStaleTTL(headers, defaultStaleTTLSeconds, url);
|
|
171
|
+
const strategy = getStrategy(headers, defaultStrategy, url);
|
|
158
172
|
|
|
159
173
|
// Handle cache-first strategy
|
|
160
174
|
if (strategy === "cache-first") {
|
|
@@ -164,22 +178,30 @@ export function createHandleRequest(config) {
|
|
|
164
178
|
|
|
165
179
|
if (cachedResponse) {
|
|
166
180
|
if (isFresh(cachedResponse, ttl)) {
|
|
181
|
+
logInfo(`Cache hit: ${url}`);
|
|
167
182
|
return cachedResponse;
|
|
168
183
|
}
|
|
169
184
|
|
|
170
185
|
// Reactive cleanup: delete if older than maxCacheAgeSeconds
|
|
171
186
|
if (isOlderThanMaxAge(cachedResponse, maxCacheAgeSeconds)) {
|
|
187
|
+
logVerbose(`Cache entry cleaned up (maxAge): ${url}`);
|
|
172
188
|
await cache.delete(request); // Fire-and-forget cleanup
|
|
173
189
|
}
|
|
174
190
|
}
|
|
175
191
|
|
|
176
192
|
// No fresh cache, fetch from network
|
|
193
|
+
if (!cachedResponse) {
|
|
194
|
+
logVerbose(`Cache miss: ${url}`);
|
|
195
|
+
} else if (!isStale(cachedResponse, ttl, staleTTL)) {
|
|
196
|
+
logVerbose(`Cache miss (stale): ${url}`);
|
|
197
|
+
}
|
|
177
198
|
let networkResponse;
|
|
178
199
|
try {
|
|
179
200
|
networkResponse = await customFetch(request);
|
|
180
201
|
} catch (error) {
|
|
181
202
|
// Network failed, return stale cache if available
|
|
182
203
|
if (cachedResponse && isStale(cachedResponse, ttl, staleTTL)) {
|
|
204
|
+
logInfo(`Cache hit (stale, offline): ${url}`);
|
|
183
205
|
return cachedResponse;
|
|
184
206
|
}
|
|
185
207
|
throw error;
|
|
@@ -208,6 +230,7 @@ export function createHandleRequest(config) {
|
|
|
208
230
|
if (cachedResponse) {
|
|
209
231
|
// Reactive cleanup: delete if older than maxCacheAgeSeconds
|
|
210
232
|
if (isOlderThanMaxAge(cachedResponse, maxCacheAgeSeconds)) {
|
|
233
|
+
logVerbose(`Cache entry cleaned up (maxAge): ${url}`);
|
|
211
234
|
cache.delete(request); // Fire-and-forget cleanup
|
|
212
235
|
throw error;
|
|
213
236
|
}
|
|
@@ -215,9 +238,11 @@ export function createHandleRequest(config) {
|
|
|
215
238
|
isFresh(cachedResponse, ttl) ||
|
|
216
239
|
isStale(cachedResponse, ttl, staleTTL)
|
|
217
240
|
) {
|
|
241
|
+
logInfo(`Cache hit (offline): ${url}`);
|
|
218
242
|
return cachedResponse;
|
|
219
243
|
}
|
|
220
244
|
}
|
|
245
|
+
logVerbose(`Cache miss (offline): ${url}`);
|
|
221
246
|
throw error;
|
|
222
247
|
}
|
|
223
248
|
|
|
@@ -239,6 +264,7 @@ export function createHandleRequest(config) {
|
|
|
239
264
|
if (cachedResponse) {
|
|
240
265
|
// Reactive cleanup: delete if older than maxCacheAgeSeconds
|
|
241
266
|
if (isOlderThanMaxAge(cachedResponse, maxCacheAgeSeconds)) {
|
|
267
|
+
logVerbose(`Cache entry cleaned up (maxAge): ${url}`);
|
|
242
268
|
cache.delete(request); // Fire-and-forget cleanup
|
|
243
269
|
// Continue to fetch from network
|
|
244
270
|
} else {
|
|
@@ -246,6 +272,11 @@ export function createHandleRequest(config) {
|
|
|
246
272
|
const stale = isStale(cachedResponse, ttl, staleTTL);
|
|
247
273
|
|
|
248
274
|
if (fresh || stale) {
|
|
275
|
+
if (fresh) {
|
|
276
|
+
logInfo(`Cache hit: ${url}`);
|
|
277
|
+
} else {
|
|
278
|
+
logInfo(`Cache hit (stale): ${url}`);
|
|
279
|
+
}
|
|
249
280
|
// Return cached response immediately
|
|
250
281
|
// Update cache in background if stale
|
|
251
282
|
if (stale) {
|
|
@@ -270,6 +301,11 @@ export function createHandleRequest(config) {
|
|
|
270
301
|
// No cache or too stale, fetch from network, no need for fallback if offline
|
|
271
302
|
// because we already know if there was a cached response it won't be
|
|
272
303
|
// fresh or stale if we've reached this point
|
|
304
|
+
if (!cachedResponse) {
|
|
305
|
+
logVerbose(`Cache miss: ${url}`);
|
|
306
|
+
} else {
|
|
307
|
+
logVerbose(`Cache miss (too stale): ${url}`);
|
|
308
|
+
}
|
|
273
309
|
const networkResponse = await customFetch(request);
|
|
274
310
|
|
|
275
311
|
// Cache the response if successful
|
package/package.json
CHANGED
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,4 +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;
|
|
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;
|
|
29
39
|
}
|