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 +25 -1
- package/package.json +1 -1
- package/src/helpers.js +80 -6
- package/src/index.js +18 -0
- package/types/helpers.d.ts +2 -0
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
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 =
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
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
|
|
package/types/helpers.d.ts
CHANGED
|
@@ -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>}
|