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 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
- await Promise.allSettled(urls.map((url) => cache.delete(url)));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swimple",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "A simple service worker library for request caching",
5
5
  "type": "module",
6
6
  "main": "index.js",
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
  }