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 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
- await Promise.allSettled(urls.map((url) => cache.delete(url)));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swimple",
3
- "version": "0.2.0",
3
+ "version": "0.3.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
@@ -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
  }