swimple 0.1.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).
@@ -224,13 +248,14 @@ Creates a request handler function for your service worker fetch handler.
224
248
  | Option | Type | Required | Default | Description |
225
249
  | ------------------------ | ---------- | -------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
226
250
  | `cacheName` | `string` | Yes | - | Name of the cache, used when calling `Cache.open(cacheName)` internally. Changing this name effectively clears the previous cache entries. |
227
- | `scope` | `string[]` | No | `undefined` | URL prefixes to cache by default (e.g., `['/api/']`). If not set and `defaultTTLSeconds` is set, all same-origin GET requests are cached automatically. If not set and `defaultTTLSeconds` is not set (or 0), no requests are cached by default. Individual requests outside the scope can still enable caching with `X-SW-Cache-TTL-Seconds` header. |
251
+ | `scope` | `string[]` | No | `undefined` | URL prefixes to cache by default (e.g., `['/api/']`). If not set and `defaultTTLSeconds` is set, all same-origin GET requests are cached automatically. If not set and `defaultTTLSeconds` is not set (or 0), no requests are cached by default. Individual requests outside the scope can still enable caching with `X-SW-Cache-TTL-Seconds` header. **Note:** Cross-origin requests are never cached, regardless of scope or TTL headers. |
228
252
  | `defaultStrategy` | `string` | No | `'cache-first'` | Default caching strategy: `'cache-first'`, `'network-first'`, or `'stale-while-revalidate'`. |
229
253
  | `defaultTTLSeconds` | `number` | No | `300` | Maximum age for fresh content. Fresh content will be returned from cache for cache-first and stale-while-revalidate strategies, and also from network-first when offline. Fresh content does not get updated from the network. Since this defaults to `300`, caching is automatic by default for GET requests matching the scope. Set to `0` or `undefined` to disable automatic caching (individual requests can still enable caching with `X-SW-Cache-TTL-Seconds` header). |
230
254
  | `defaultStaleTTLSeconds` | `number` | No | `3600` | Maximum age for stale content. Stale content will be returned from cache for cache-first (when offline), network-first (when offline), and stale-while-revalidate strategies. That means responses past the fresh TTL but within stale TTL can still be returned from cache. Stale content does get updated from the network. |
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
 
@@ -647,6 +672,7 @@ self.addEventListener("fetch", (event) => {
647
672
 
648
673
  - Only GET requests are cached
649
674
  - Only 2xx (OK) GET responses are cached. Non-OK responses (4xx, 5xx, etc.) are not cached
675
+ - **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.
650
676
  - 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.
651
677
  - 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)
652
678
  - Cache invalidation happens automatically for mutations when `inferInvalidation: true`
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,14 +23,19 @@ 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
  /**
30
32
  * Creates a request handler function for service worker fetch events.
33
+ * The handler implements HTTP caching with configurable strategies (cache-first, network-first, stale-while-revalidate),
34
+ * automatic cache invalidation for mutations, and periodic cache cleanup. Only handles same-origin GET requests
35
+ * that match the configured scope.
31
36
  *
32
- * @param {HandleRequestConfig} config - Configuration options
33
- * @returns {(event: FetchEvent) => Promise<Response> | null} Request handler function
37
+ * @param {HandleRequestConfig} config - Configuration options including cache name, scope, default strategy, and TTL settings
38
+ * @returns {(event: FetchEvent) => Promise<Response> | null} Request handler function that can be used in service worker fetch event listeners
34
39
  */
35
40
  export function createHandleRequest(config) {
36
41
  validateConfig(config);
@@ -46,11 +51,22 @@ export function createHandleRequest(config) {
46
51
  const maxCacheAgeSeconds = config.maxCacheAgeSeconds ?? 7200;
47
52
  const inferInvalidation = config.inferInvalidation ?? true;
48
53
  const customFetch = config.customFetch || fetch;
54
+ const enableLogging = config.enableLogging ?? false;
55
+
56
+ // Set module-level logging state
57
+ setLoggingEnabled(enableLogging);
49
58
 
50
59
  // Track fetch counter for periodic cleanup
51
60
  let fetchCounter = 0;
52
61
 
53
- // Main request handler
62
+ /**
63
+ * Service worker fetch event handler that implements HTTP caching strategies.
64
+ * Handles cache invalidation for mutations, implements cache-first/network-first/stale-while-revalidate
65
+ * strategies for GET requests, and performs automatic cache cleanup.
66
+ *
67
+ * @param {FetchEvent} event - The fetch event from the service worker
68
+ * @returns {Promise<Response> | null} The cached or fetched response, or null if request shouldn't be handled
69
+ */
54
70
  return function handleRequest(event) {
55
71
  const request = event.request;
56
72
  const url = request.url;
@@ -59,6 +75,7 @@ export function createHandleRequest(config) {
59
75
 
60
76
  // Handle cache clearing - header presence (any value) triggers cache clear
61
77
  if (getHeader(headers, "X-SW-Cache-Clear") !== null) {
78
+ log(`X-SW-Cache-Clear header set: ${url}`);
62
79
  return (async () => {
63
80
  await clearCache(cacheName);
64
81
  return customFetch(request);
@@ -70,6 +87,12 @@ export function createHandleRequest(config) {
70
87
  const invalidateHeaders = getAllHeaders(headers, "X-SW-Cache-Invalidate");
71
88
  const isMutation = ["POST", "PATCH", "PUT", "DELETE"].includes(method);
72
89
 
90
+ if (invalidateHeaders.length > 0) {
91
+ invalidateHeaders.forEach((path) => {
92
+ log(`X-SW-Cache-Invalidate header set: ${path} (${url})`);
93
+ });
94
+ }
95
+
73
96
  if (invalidateHeaders.length > 0 || (inferInvalidation && isMutation)) {
74
97
  return (async () => {
75
98
  let pathsToInvalidate = [...invalidateHeaders];
@@ -93,6 +116,29 @@ export function createHandleRequest(config) {
93
116
  return null;
94
117
  }
95
118
 
119
+ // Only cache same-origin requests - cross-origin requests are not cached
120
+ // In service worker context, self.location.origin is the service worker's origin
121
+ try {
122
+ const requestUrl = new URL(url);
123
+ // Check if we're in a service worker context and can determine the origin
124
+ if (
125
+ typeof self !== "undefined" &&
126
+ self.location &&
127
+ self.location.origin
128
+ ) {
129
+ const serviceWorkerOrigin = self.location.origin;
130
+ // If request origin doesn't match service worker origin, don't cache
131
+ if (requestUrl.origin !== serviceWorkerOrigin) {
132
+ return null;
133
+ }
134
+ }
135
+ // In test environments where self.location might not be available,
136
+ // we rely on the test setup to ensure proper origin handling
137
+ } catch (error) {
138
+ // If URL parsing fails, don't cache
139
+ return null;
140
+ }
141
+
96
142
  // Periodic cleanup: run on first fetch and every 100 fetches
97
143
  fetchCounter++;
98
144
  if (fetchCounter === 1 || fetchCounter % 100 === 0) {
@@ -108,7 +154,7 @@ export function createHandleRequest(config) {
108
154
  // Check if request matches scope and should be cached
109
155
  const hasExplicitTTLHeader =
110
156
  getHeader(headers, "X-SW-Cache-TTL-Seconds") !== null;
111
- const ttl = getTTL(headers, defaultTTLSeconds);
157
+ const ttl = getTTL(headers, defaultTTLSeconds, url);
112
158
 
113
159
  // If scope doesn't match and there's no explicit TTL header, don't handle the request
114
160
  if (!matchesScope(url, scope, defaultTTLSeconds) && !hasExplicitTTLHeader) {
@@ -120,8 +166,8 @@ export function createHandleRequest(config) {
120
166
  return null;
121
167
  }
122
168
 
123
- const staleTTL = getStaleTTL(headers, defaultStaleTTLSeconds);
124
- const strategy = getStrategy(headers, defaultStrategy);
169
+ const staleTTL = getStaleTTL(headers, defaultStaleTTLSeconds, url);
170
+ const strategy = getStrategy(headers, defaultStrategy, url);
125
171
 
126
172
  // Handle cache-first strategy
127
173
  if (strategy === "cache-first") {
@@ -131,22 +177,30 @@ export function createHandleRequest(config) {
131
177
 
132
178
  if (cachedResponse) {
133
179
  if (isFresh(cachedResponse, ttl)) {
180
+ log(`Cache hit: ${url}`);
134
181
  return cachedResponse;
135
182
  }
136
183
 
137
184
  // Reactive cleanup: delete if older than maxCacheAgeSeconds
138
185
  if (isOlderThanMaxAge(cachedResponse, maxCacheAgeSeconds)) {
186
+ log(`Cache entry cleaned up (maxAge): ${url}`);
139
187
  await cache.delete(request); // Fire-and-forget cleanup
140
188
  }
141
189
  }
142
190
 
143
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
+ }
144
197
  let networkResponse;
145
198
  try {
146
199
  networkResponse = await customFetch(request);
147
200
  } catch (error) {
148
201
  // Network failed, return stale cache if available
149
202
  if (cachedResponse && isStale(cachedResponse, ttl, staleTTL)) {
203
+ log(`Cache hit (stale, offline): ${url}`);
150
204
  return cachedResponse;
151
205
  }
152
206
  throw error;
@@ -175,6 +229,7 @@ export function createHandleRequest(config) {
175
229
  if (cachedResponse) {
176
230
  // Reactive cleanup: delete if older than maxCacheAgeSeconds
177
231
  if (isOlderThanMaxAge(cachedResponse, maxCacheAgeSeconds)) {
232
+ log(`Cache entry cleaned up (maxAge): ${url}`);
178
233
  cache.delete(request); // Fire-and-forget cleanup
179
234
  throw error;
180
235
  }
@@ -182,9 +237,11 @@ export function createHandleRequest(config) {
182
237
  isFresh(cachedResponse, ttl) ||
183
238
  isStale(cachedResponse, ttl, staleTTL)
184
239
  ) {
240
+ log(`Cache hit (offline): ${url}`);
185
241
  return cachedResponse;
186
242
  }
187
243
  }
244
+ log(`Cache miss (offline): ${url}`);
188
245
  throw error;
189
246
  }
190
247
 
@@ -206,6 +263,7 @@ export function createHandleRequest(config) {
206
263
  if (cachedResponse) {
207
264
  // Reactive cleanup: delete if older than maxCacheAgeSeconds
208
265
  if (isOlderThanMaxAge(cachedResponse, maxCacheAgeSeconds)) {
266
+ log(`Cache entry cleaned up (maxAge): ${url}`);
209
267
  cache.delete(request); // Fire-and-forget cleanup
210
268
  // Continue to fetch from network
211
269
  } else {
@@ -213,6 +271,11 @@ export function createHandleRequest(config) {
213
271
  const stale = isStale(cachedResponse, ttl, staleTTL);
214
272
 
215
273
  if (fresh || stale) {
274
+ if (fresh) {
275
+ log(`Cache hit: ${url}`);
276
+ } else {
277
+ log(`Cache hit (stale): ${url}`);
278
+ }
216
279
  // Return cached response immediately
217
280
  // Update cache in background if stale
218
281
  if (stale) {
@@ -237,6 +300,11 @@ export function createHandleRequest(config) {
237
300
  // No cache or too stale, fetch from network, no need for fallback if offline
238
301
  // because we already know if there was a cached response it won't be
239
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
+ }
240
308
  const networkResponse = await customFetch(request);
241
309
 
242
310
  // Cache the response if successful
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swimple",
3
- "version": "0.1.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
@@ -12,7 +12,7 @@ export type CacheStrategy =
12
12
  export interface HandleRequestConfig {
13
13
  /** Name of the cache, used when calling `Cache.open(cacheName)` internally. Changing this name effectively clears the previous cache entries. */
14
14
  cacheName: string;
15
- /** URL prefixes to cache by default (e.g., `['/api/']`). If not set and `defaultTTLSeconds` is set, all same-origin GET requests are cached automatically. If not set and `defaultTTLSeconds` is not set (or 0), no requests are cached by default. Individual requests outside the scope can still enable caching with `X-SW-Cache-TTL-Seconds` header. */
15
+ /** URL prefixes to cache by default (e.g., `['/api/']`). If not set and `defaultTTLSeconds` is set, all same-origin GET requests are cached automatically. If not set and `defaultTTLSeconds` is not set (or 0), no requests are cached by default. Individual requests outside the scope can still enable caching with `X-SW-Cache-TTL-Seconds` header. Note: Cross-origin requests are never cached, regardless of scope or TTL headers. */
16
16
  scope?: string[];
17
17
  /** Default caching strategy: `'cache-first'`, `'network-first'`, or `'stale-while-revalidate'`. */
18
18
  defaultStrategy?: CacheStrategy;
@@ -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
  }