swimple 0.11.0 → 0.12.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
@@ -359,6 +359,8 @@ Explicitly invalidate specific cache entries. Can be set multiple times for mult
359
359
 
360
360
  **Important:** When `X-SW-Cache-Invalidate` headers are present, they **take precedence** over automatically inferred invalidation paths. If headers are provided, only the header-specified paths are invalidated - inferred paths are not added. This allows you to have fine-grained control over invalidation even when `inferInvalidation: true`.
361
361
 
362
+ **Query Parameter Handling:** Invalidating a path (e.g., `/api/users`) will invalidate all cache entries with that pathname, regardless of query parameters. For example, invalidating `/api/users` will also invalidate `/api/users?org_id=123`, `/api/users?status=active`, and any other query parameter variants.
363
+
362
364
  **Example:**
363
365
 
364
366
  ```javascript
@@ -531,6 +533,8 @@ When `inferInvalidation: true` (default), the library automatically invalidates
531
533
 
532
534
  The library strips the last path segment to find the collection endpoint. This works for most REST API patterns, but may not handle all edge cases (e.g., nested resources like `/api/users/123/avatar`). For edge cases, you can manually specify invalidation paths using the `X-SW-Cache-Invalidate` header.
533
535
 
536
+ **Query Parameter Handling:** Cache invalidation matches entries by pathname (ignoring query parameters). This means when you invalidate a path, all cache entries with that pathname are invalidated, regardless of their query parameters. For example, invalidating `/api/users` will also invalidate `/api/users?org_id=123`, `/api/users?status=active`, and any other query parameter variants. This ensures that when you update a resource (e.g., `PATCH /api/users/456`), all filtered views of the collection (e.g., `/api/users?org_id=123`) are also invalidated.
537
+
534
538
  **Note:** If you provide `X-SW-Cache-Invalidate` headers, they take precedence over inferred paths. Only the header-specified paths will be invalidated, not the inferred ones.
535
539
 
536
540
  **Example: Handling nested resources**
@@ -550,6 +554,26 @@ fetch("/api/users/123/avatar", {
550
554
  });
551
555
  ```
552
556
 
557
+ **Example: Query parameter invalidation**
558
+
559
+ ```javascript
560
+ // Cache multiple filtered views of the users list
561
+ fetch("/api/users"); // Cached
562
+ fetch("/api/users?org_id=123"); // Cached separately
563
+ fetch("/api/users?org_id=456&status=active"); // Cached separately
564
+
565
+ // PATCH /api/users/789 - automatically invalidates:
566
+ // - /api/users/789 (exact item path)
567
+ // - /api/users (collection, no query params)
568
+ // - /api/users?org_id=123 (collection with query params)
569
+ // - /api/users?org_id=456&status=active (collection with different query params)
570
+ // All cache entries with pathname /api/users are invalidated
571
+ fetch("/api/users/789", {
572
+ method: "PATCH",
573
+ body: JSON.stringify({ name: "Updated User" })
574
+ });
575
+ ```
576
+
553
577
  You can disable automatic invalidation:
554
578
 
555
579
  ```javascript
@@ -744,7 +768,7 @@ self.addEventListener("fetch", (event) => {
744
768
  - Only 2xx (OK) GET responses are cached. Non-OK responses (4xx, 5xx, etc.) are not cached
745
769
  - **Cross-origin requests are not cached** - Only requests to the same origin as the service worker are cached. Requests to different origins will return `null` and are not processed by the cache handler.
746
770
  - Non-GET and non-mutating requests (POST/PATCH/PUT/DELETE) are not processed by the cache handler - it will return null. Practically, this means HEAD requests are not handled by the cache handler.
747
- - Query strings are part of the cache key. Different query strings create different cache entries (e.g., `/api/users?page=1` and `/api/users?page=2` are separate cache entries)
771
+ - Query strings are part of the cache key. Different query strings create different cache entries (e.g., `/api/users?page=1` and `/api/users?page=2` are separate cache entries). However, cache invalidation matches by pathname (ignoring query parameters), so invalidating `/api/users` will invalidate all query variants like `/api/users?page=1`, `/api/users?org_id=123`, etc.
748
772
  - Cache invalidation happens automatically for mutations when `inferInvalidation: true`
749
773
  - All headers are case-insensitive (per HTTP spec)
750
774
  - TTL of `0` completely opts out of caching for a request - the handler returns `null` immediately without checking cache, making network requests, or processing the request.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swimple",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "A simple service worker library for request caching",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/helpers.js CHANGED
@@ -239,20 +239,94 @@ export function matchesScope(url, scope, defaultTTLSeconds) {
239
239
 
240
240
  /**
241
241
  * Invalidate cache entries
242
+ * Matches cache entries by pathname (ignoring query parameters), so invalidating
243
+ * "/api/users" will also invalidate "/api/users?org_id=123" and other query variants.
242
244
  * @param {string} cacheName
243
245
  * @param {string[]} urls
244
246
  * @returns {Promise<void>}
245
247
  */
246
248
  export async function invalidateCache(cacheName, urls) {
247
249
  const cache = await caches.open(cacheName);
248
- const deletePromises = urls.map(async (url) => {
249
- const deleted = await cache.delete(url);
250
- if (deleted) {
251
- logInfo(`Cache invalidated: ${url}`);
250
+ const deletePromises = [];
251
+ const invalidatedUrls = [];
252
+
253
+ // Determine origin for constructing full URLs from relative paths
254
+ // Try self.location.origin first (service worker context)
255
+ // Otherwise, extract from URLs array if any are full URLs
256
+ let cacheOrigin = null;
257
+ if (typeof self !== "undefined" && self.location && self.location.origin) {
258
+ cacheOrigin = self.location.origin;
259
+ } else {
260
+ // Try to extract origin from URLs array
261
+ for (const url of urls) {
262
+ try {
263
+ const urlObj = new URL(url);
264
+ cacheOrigin = urlObj.origin;
265
+ break; // Use first valid origin found
266
+ } catch {
267
+ // Not a full URL, continue
268
+ }
252
269
  }
253
- return deleted;
254
- });
270
+ }
271
+
272
+ if (!cacheOrigin) {
273
+ throw new Error(
274
+ "Cannot determine origin for relative paths. self.location.origin is not available and no full URLs provided."
275
+ );
276
+ }
277
+
278
+ // Iterate over URLs to invalidate (typically much smaller than cache size)
279
+ // Use matchAll with ignoreSearch to avoid loading all cache keys into memory
280
+ for (const url of urls) {
281
+ try {
282
+ // Create a Request object for matching
283
+ // If URL is relative, construct a full URL using the cache origin
284
+ let requestUrl = url;
285
+ try {
286
+ // Try to parse as URL - if it fails, it might be relative
287
+ new URL(url);
288
+ } catch {
289
+ // If relative, construct a full URL
290
+ requestUrl = new URL(url, cacheOrigin).toString();
291
+ }
292
+
293
+ // Create a GET request for matching (cached entries are always GET requests)
294
+ const request = new Request(requestUrl, { method: "GET" });
295
+
296
+ // Use matchAll with ignoreSearch to find all cache entries matching this pathname
297
+ // (ignoring query parameters). This avoids loading all cache keys into memory.
298
+ const matchingResponses = await cache.matchAll(request, {
299
+ ignoreSearch: true
300
+ });
301
+
302
+ // Delete all matching entries
303
+ // matchAll returns Response objects; construct Request objects for delete()
304
+ for (const response of matchingResponses) {
305
+ const responseUrl = response.url;
306
+ const requestToDelete = new Request(responseUrl);
307
+ deletePromises.push(cache.delete(requestToDelete));
308
+ invalidatedUrls.push(responseUrl);
309
+ }
310
+ } catch (error) {
311
+ // If URL parsing or matching fails, try exact match as fallback
312
+ try {
313
+ const request = new Request(url);
314
+ const deleted = await cache.delete(request);
315
+ if (deleted) {
316
+ invalidatedUrls.push(url);
317
+ }
318
+ } catch {
319
+ // Ignore errors for invalid URLs
320
+ }
321
+ }
322
+ }
323
+
255
324
  await Promise.allSettled(deletePromises);
325
+
326
+ // Log each invalidated URL
327
+ invalidatedUrls.forEach((url) => {
328
+ logInfo(`Cache invalidated: ${url}`);
329
+ });
256
330
  }
257
331
 
258
332
  /**
package/src/index.js CHANGED
@@ -108,7 +108,25 @@ export function createHandleRequest(config) {
108
108
  pathsToInvalidate.push(...getInferredInvalidationPaths(url));
109
109
  }
110
110
 
111
+ // Normalize relative paths to full URLs using the mutation request's origin
111
112
  if (pathsToInvalidate.length > 0) {
113
+ try {
114
+ const requestUrlObj = new URL(url);
115
+ const requestOrigin = requestUrlObj.origin;
116
+ pathsToInvalidate = pathsToInvalidate.map((path) => {
117
+ try {
118
+ // Try to parse as URL - if it fails, it's relative
119
+ new URL(path);
120
+ return path; // Already a full URL
121
+ } catch {
122
+ // Relative path - construct full URL using request origin
123
+ return new URL(path, requestOrigin).toString();
124
+ }
125
+ });
126
+ } catch {
127
+ // If we can't parse the request URL, leave paths as-is
128
+ // invalidateCache will handle it
129
+ }
112
130
  await invalidateCache(cacheName, pathsToInvalidate);
113
131
  }
114
132
 
@@ -88,6 +88,8 @@ export function getStaleTTL(headers: Headers, defaultStaleTTL: number, url?: str
88
88
  export function matchesScope(url: string, scope: string[], defaultTTLSeconds: number): boolean;
89
89
  /**
90
90
  * Invalidate cache entries
91
+ * Matches cache entries by pathname (ignoring query parameters), so invalidating
92
+ * "/api/users" will also invalidate "/api/users?org_id=123" and other query variants.
91
93
  * @param {string} cacheName
92
94
  * @param {string[]} urls
93
95
  * @returns {Promise<void>}