@b9g/cache 0.1.5 → 0.2.0-beta.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
@@ -4,11 +4,11 @@
4
4
 
5
5
  ## Features
6
6
 
7
- - **ServiceWorker Cache API**: Standard `caches` global and Cache interface from ServiceWorker spec
8
- - **Multiple Backends**: Memory, filesystem, Redis, KV store implementations
7
+ - **ServiceWorker Cache API**: Standard `Cache` and `CacheStorage` interfaces from ServiceWorker spec
8
+ - **Multiple Backends**: Memory cache with LRU eviction, PostMessage coordination for workers
9
9
  - **Universal**: Same API works in browsers, Node.js, Bun, and edge platforms
10
10
  - **Request/Response Caching**: Full HTTP semantics with Request/Response objects
11
- - **Registry Pattern**: Named cache management with factory registration
11
+ - **Factory Pattern**: Flexible cache creation with factory functions
12
12
 
13
13
  ## Installation
14
14
 
@@ -18,104 +18,208 @@ npm install @b9g/cache
18
18
 
19
19
  ## Quick Start
20
20
 
21
- ```javascript
22
- import { CacheStorage, MemoryCache } from '@b9g/cache';
21
+ ### Using with Shovel (Recommended)
22
+
23
+ Configure cache providers in via the `shovel` key in package.json or `shovel.json`:
24
+
25
+ ```json
26
+ {
27
+ "caches": {
28
+ "pages": {"provider": "memory"},
29
+ "api": {"provider": "memory", "maxEntries": 5000}
30
+ }
31
+ }
32
+ ```
23
33
 
24
- // Create cache storage
25
- const caches = new CacheStorage();
34
+ Shovel provides `self.caches` as a global following the ServiceWorker CacheStorage API. Access it directly in your handlers and middleware:
26
35
 
27
- // Register cache implementations
28
- caches.register('api', () => new MemoryCache('api'));
29
- caches.register('pages', () => new MemoryCache('pages'));
36
+ ```typescript
37
+ import {Router} from '@b9g/router';
38
+
39
+ const router = new Router();
40
+
41
+ // Cache middleware using generator API
42
+ router.use(async function* (request, _context) {
43
+ if (request.method !== 'GET' || !self.caches) {
44
+ return yield request; // Skip caching
45
+ }
46
+
47
+ // Open cache
48
+ const cache = await self.caches.open('pages-v1');
49
+
50
+ // Check cache
51
+ const cached = await cache.match(request);
52
+ if (cached) {
53
+ return cached; // Cache hit
54
+ }
55
+
56
+ // Cache miss - get response from handler
57
+ const response = yield request;
58
+
59
+ // Store in cache
60
+ if (response.ok) {
61
+ await cache.put(request, response.clone());
62
+ }
63
+
64
+ return response;
65
+ });
66
+
67
+ router.route('/posts/:id')
68
+ .get(async (request, context) => {
69
+ const post = await getPost(context.params.id);
70
+ return Response.json(post, {
71
+ headers: {'Cache-Control': 'max-age=300'},
72
+ });
73
+ });
74
+ ```
75
+
76
+ ### Standalone Usage
77
+
78
+ ```javascript
79
+ import {CustomCacheStorage} from '@b9g/cache';
80
+ import {MemoryCache} from '@b9g/cache/memory';
81
+
82
+ // Create cache storage with factory
83
+ const caches = new CustomCacheStorage((name) => {
84
+ return new MemoryCache(name, {maxEntries: 1000});
85
+ });
30
86
 
31
87
  // Open and use caches
32
88
  const apiCache = await caches.open('api');
33
89
 
34
90
  // Store response
35
91
  const request = new Request('https://api.example.com/posts/1');
36
- const response = new Response(JSON.stringify({ id: 1, title: 'Hello' }));
92
+ const response = new Response(JSON.stringify({id: 1, title: 'Hello'}), {
93
+ headers: {
94
+ 'Content-Type': 'application/json',
95
+ 'Cache-Control': 'max-age=300',
96
+ }
97
+ });
37
98
  await apiCache.put(request, response);
38
99
 
39
100
  // Retrieve response
40
101
  const cached = await apiCache.match(request);
41
- console.log(await cached.json()); // { id: 1, title: 'Hello' }
102
+ console.log(await cached.json()); // {id: 1, title: 'Hello'}
42
103
  ```
43
104
 
44
- ## Cache Implementations
105
+ ## Cache Providers
45
106
 
46
- ### MemoryCache
107
+ Shovel supports multiple cache providers that can be configured in `shovel.json`:
47
108
 
48
- In-memory cache with TTL and size limits:
109
+ ### Built-in Providers
49
110
 
50
- ```javascript
51
- import { MemoryCache } from '@b9g/cache';
111
+ - **`memory`** - In-memory cache with LRU eviction (default)
112
+ - **`redis`** - Redis-backed cache (requires `@b9g/cache-redis`)
113
+ - **`cloudflare`** - Uses Cloudflare Workers native Cache API (only works with the Cloudflare platform)
52
114
 
53
- const cache = new MemoryCache('my-cache', {
54
- maxEntries: 1000, // Maximum number of entries
55
- ttl: 300, // Time to live in seconds
56
- maxSize: 50 * 1024 * 1024 // Maximum total size in bytes
57
- });
115
+ You can also use custom providers by specifying a module path:
116
+
117
+ ```json
118
+ {
119
+ "caches": {
120
+ "pages": {"provider": "memory"},
121
+ "sessions": {"provider": "redis", "url": "REDIS_URL"},
122
+ "custom": {"provider": "./my-cache-provider.js"}
123
+ }
124
+ }
58
125
  ```
59
126
 
60
- ### FilesystemCache
127
+ Pattern matching is supported for cache names:
128
+
129
+ ```json
130
+ {
131
+ "caches": {
132
+ "api-*": {"provider": "memory", "maxEntries": 5000},
133
+ "page-*": {"provider": "memory", "maxEntries": 100}
134
+ }
135
+ }
136
+ ```
61
137
 
62
- File-based cache for SSG and persistent storage:
138
+ ## Cache Implementations
139
+
140
+ ### MemoryCache
141
+
142
+ In-memory cache with LRU eviction and HTTP Cache-Control header support:
63
143
 
64
144
  ```javascript
65
- import { FilesystemCache } from '@b9g/cache';
145
+ import {MemoryCache} from '@b9g/cache/memory';
66
146
 
67
- const cache = new FilesystemCache('pages', {
68
- directory: './dist/cache',
69
- compression: true,
70
- indexing: true
147
+ const cache = new MemoryCache(name, {
148
+ maxEntries: 1000, // Maximum number of entries (LRU eviction)
71
149
  });
150
+
151
+ // Cache respects Cache-Control headers
152
+ await cache.put(request, new Response(data, {
153
+ headers: {'Cache-Control': 'max-age=300'},
154
+ }));
155
+
156
+ // After 300 seconds, match() returns undefined
72
157
  ```
73
158
 
74
- ## CacheStorage Registry
159
+ ### PostMessageCache
160
+
161
+ Worker-side cache that coordinates with main thread via PostMessage:
75
162
 
76
163
  ```javascript
77
- import { CacheStorage, MemoryCache, FilesystemCache } from '@b9g/cache';
164
+ import {PostMessageCache} from '@b9g/cache/postmessage';
78
165
 
79
- const caches = new CacheStorage();
166
+ // In worker thread - forwards operations to main thread
167
+ const cache = new PostMessageCache({
168
+ name: 'shared',
169
+ timeout: 30000, // Optional, defaults to 30000ms
170
+ });
80
171
 
81
- // Register different implementations
82
- caches.register('api', () =>
83
- new MemoryCache('api', { ttl: 300 })
84
- );
172
+ // Operations are synchronized with main thread's MemoryCache
173
+ await cache.put(request, response);
174
+ ```
85
175
 
86
- caches.register('pages', () =>
87
- new FilesystemCache('pages', { directory: './dist/pages' })
88
- );
176
+ ## CustomCacheStorage
89
177
 
90
- caches.register('assets', () =>
91
- new MemoryCache('assets', { maxEntries: 10000 })
92
- );
178
+ Create cache storage with a factory function:
179
+
180
+ ```javascript
181
+ import {CustomCacheStorage} from '@b9g/cache';
182
+ import {MemoryCache} from '@b9g/cache/memory';
93
183
 
94
- // Use with router
95
- import { Router } from '@b9g/router';
96
- const router = new Router({ caches });
184
+ const caches = new CustomCacheStorage((name) => {
185
+ // Different caches can have different configurations
186
+ if (name === 'api') {
187
+ return new MemoryCache(name, {maxEntries: 5000});
188
+ }
189
+ if (name === 'pages') {
190
+ return new MemoryCache(name, {maxEntries: 100});
191
+ }
192
+ return new MemoryCache();
193
+ });
97
194
  ```
98
195
 
99
196
  ## Exports
100
197
 
101
- ### Classes
198
+ ### Main (`@b9g/cache`)
102
199
 
103
- - `Cache` - Abstract base class for cache implementations
104
- - `CustomCacheStorage` - CacheStorage implementation with factory registration
200
+ - `Cache` - Abstract base class implementing `globalThis.Cache`
201
+ - `CustomCacheStorage` - CacheStorage implementation with factory pattern
202
+ - `generateCacheKey(request, options?)` - Generate cache key from Request
203
+ - `toRequest(request)` - Convert RequestInfo or URL to Request
204
+ - `CacheQueryOptions` - Type for cache query options
105
205
 
106
- ### Functions
206
+ ### Memory (`@b9g/cache/memory`)
107
207
 
108
- - `generateCacheKey(request, options?)` - Generate a cache key from a Request
208
+ - `MemoryCache` - In-memory cache with LRU and Cache-Control support
209
+ - `MemoryCacheOptions` - Configuration type
109
210
 
110
- ### Types
211
+ ### PostMessage (`@b9g/cache/postmessage`)
111
212
 
112
- - `CacheQueryOptions` - Options for cache query operations (ignoreSearch, ignoreMethod, ignoreVary)
113
- - `CacheFactory` - Factory function type `(name: string) => Cache | Promise<Cache>`
213
+ - `PostMessageCache` - Worker-side cache with main thread coordination
214
+ - `PostMessageCacheOptions` - Configuration type
215
+ - `handleCacheResponse(message)` - Message handler for worker coordination
114
216
 
115
217
  ## API Reference
116
218
 
117
219
  ### Standard Cache Methods
118
220
 
221
+ All cache implementations provide the standard Cache API:
222
+
119
223
  ```javascript
120
224
  // Check for cached response
121
225
  const response = await cache.match(request, options?);
@@ -140,10 +244,7 @@ const requests = await cache.keys(request?, options?);
140
244
  ### CacheStorage Methods
141
245
 
142
246
  ```javascript
143
- // Register cache factory
144
- caches.register(name, factory);
145
-
146
- // Open named cache
247
+ // Open named cache (creates if doesn't exist)
147
248
  const cache = await caches.open(name);
148
249
 
149
250
  // Check if cache exists
@@ -154,6 +255,12 @@ const deleted = await caches.delete(name);
154
255
 
155
256
  // List cache names
156
257
  const names = await caches.keys();
258
+
259
+ // Match across all caches
260
+ const response = await caches.match(request, options?);
261
+
262
+ // Cleanup (disposes all caches)
263
+ await caches.dispose();
157
264
  ```
158
265
 
159
266
  ## Cache Options
@@ -162,29 +269,52 @@ const names = await caches.keys();
162
269
 
163
270
  ```javascript
164
271
  const response = await cache.match(request, {
165
- ignoreSearch: true, // Ignore query parameters
272
+ ignoreSearch: true, // Ignore query parameters in URL
166
273
  ignoreMethod: false, // Consider HTTP method
167
- ignoreVary: false, // Honor Vary header
168
- cacheName: 'specific' // Target specific cache
274
+ ignoreVary: false, // Honor Vary header (default behavior)
169
275
  });
170
276
  ```
171
277
 
172
- ### Storage Options
278
+ **Vary Header Support:**
279
+
280
+ The cache respects the HTTP `Vary` header by default:
173
281
 
174
282
  ```javascript
175
- // Memory cache options
176
- new MemoryCache('name', {
177
- maxEntries: 1000,
178
- ttl: 300,
179
- maxSize: 50 * 1024 * 1024
180
- });
283
+ // Cache a response that varies on Accept-Encoding
284
+ await cache.put(
285
+ new Request('https://api.example.com/data', {
286
+ headers: {'Accept-Encoding': 'gzip'},
287
+ }),
288
+ new Response(gzippedData, {
289
+ headers: {'Vary': 'Accept-Encoding'},
290
+ })
291
+ );
292
+
293
+ // Same URL with same Accept-Encoding: matches
294
+ await cache.match(new Request('https://api.example.com/data', {
295
+ headers: {'Accept-Encoding': 'gzip'},
296
+ })); // ✓ Returns cached response
297
+
298
+ // Same URL with different Accept-Encoding: no match
299
+ await cache.match(new Request('https://api.example.com/data', {
300
+ headers: {'Accept-Encoding': 'br'},
301
+ })); // ✗ Returns undefined
302
+
303
+ // Use ignoreVary to bypass Vary header checking
304
+ await cache.match(new Request('https://api.example.com/data', {
305
+ headers: {'Accept-Encoding': 'br'},
306
+ }), {ignoreVary: true}); // ✓ Returns cached response
307
+ ```
181
308
 
182
- // Filesystem cache options
183
- new FilesystemCache('name', {
184
- directory: './cache',
185
- compression: true,
186
- indexing: true,
187
- fsync: false
309
+ Special cases:
310
+ - `Vary: *` means the response varies on everything and will never match (unless `ignoreVary: true`)
311
+ - Multiple headers: `Vary: Accept-Encoding, User-Agent` requires all specified headers to match
312
+
313
+ ### MemoryCache Options
314
+
315
+ ```javascript
316
+ new MemoryCache(name, {
317
+ maxEntries: 1000 // Maximum entries (LRU eviction when exceeded)
188
318
  });
189
319
  ```
190
320
 
@@ -193,99 +323,93 @@ new FilesystemCache('name', {
193
323
  ### With Router
194
324
 
195
325
  ```javascript
196
- import { Router } from '@b9g/router';
197
- import { CacheStorage, MemoryCache } from '@b9g/cache';
326
+ import {Router} from '@b9g/router';
327
+ import {CustomCacheStorage} from '@b9g/cache';
328
+ import {MemoryCache} from '@b9g/cache/memory';
198
329
 
199
- const caches = new CacheStorage();
200
- caches.register('api', () => new MemoryCache('api'));
330
+ const caches = new CustomCacheStorage((name) =>
331
+ new MemoryCache(name, {maxEntries: 1000})
332
+ );
201
333
 
202
- const router = new Router({ caches });
334
+ const router = new Router();
203
335
 
204
336
  // Cache-aware middleware
205
- router.use(async function* (request, context) {
206
- if (request.method === 'GET' && context.cache) {
207
- const cached = await context.cache.match(request);
208
- if (cached) return cached;
337
+ router.use(async function* (request, _context) {
338
+ if (request.method !== 'GET') {
339
+ return yield request;
209
340
  }
210
-
341
+
342
+ const cache = await caches.open('api');
343
+ const cached = await cache.match(request);
344
+ if (cached) return cached;
345
+
211
346
  const response = yield request;
212
-
213
- if (request.method === 'GET' && context.cache && response.ok) {
214
- await context.cache.put(request, response.clone());
347
+
348
+ if (response.ok) {
349
+ await cache.put(request, response.clone());
215
350
  }
216
-
351
+
217
352
  return response;
218
353
  });
219
354
 
220
- router.route('/api/posts/:id', { cache: { name: 'api' } })
355
+ router.route('/api/posts/:id')
221
356
  .get(postHandler);
222
357
  ```
223
358
 
224
- ### Static Site Generation
359
+ ### Multi-Worker Setup
225
360
 
226
361
  ```javascript
227
- import { FilesystemCache } from '@b9g/cache';
362
+ // Main thread
363
+ import {CustomCacheStorage} from '@b9g/cache';
364
+ import {MemoryCache} from '@b9g/cache/memory';
228
365
 
229
- const cache = new FilesystemCache('pages', {
230
- directory: './dist'
366
+ const caches = new CustomCacheStorage((name) =>
367
+ new MemoryCache()
368
+ );
369
+
370
+ worker.on('message', (message) => {
371
+ if (message.type?.startsWith('cache:')) {
372
+ caches.handleMessage(worker, message);
373
+ }
231
374
  });
232
375
 
233
- // Pre-populate cache at build time
234
- const paths = ['/about', '/contact', '/blog/post-1'];
376
+ // Worker thread
377
+ import {PostMessageCache} from '@b9g/cache/postmessage';
378
+ import {handleCacheResponse} from '@b9g/cache/postmessage';
235
379
 
236
- for (const path of paths) {
237
- const request = new Request(`https://example.com${path}`);
238
- await cache.add(request); // Fetches through your router
239
- }
380
+ const cache = new PostMessageCache('shared');
381
+
382
+ self.addEventListener('message', (event) => {
383
+ if (event.data.type === 'cache:response' || event.data.type === 'cache:error') {
384
+ handleCacheResponse(event.data);
385
+ }
386
+ });
240
387
 
241
- // At runtime, serve from cache
242
- const response = await cache.match(request);
388
+ // Operations coordinate with main thread
389
+ await cache.put(request, response);
243
390
  ```
244
391
 
245
- ### Service Worker
392
+ ### HTTP Caching Semantics
246
393
 
247
394
  ```javascript
248
- // In service worker context
249
- import { CacheStorage } from '@b9g/cache';
250
-
251
- // Use native browser caches when available
252
- const caches = new CacheStorage();
253
-
254
- self.addEventListener('fetch', async event => {
255
- const cache = await caches.open('runtime');
256
-
257
- event.respondWith(
258
- cache.match(event.request).then(response => {
259
- if (response) return response;
260
-
261
- return fetch(event.request).then(response => {
262
- if (response.ok) {
263
- cache.put(event.request, response.clone());
264
- }
265
- return response;
266
- });
267
- })
268
- );
269
- });
270
- ```
271
-
272
- ## Cache Coordination
395
+ import {MemoryCache} from '@b9g/cache/memory';
273
396
 
274
- For multi-worker setups, caches coordinate through PostMessage:
397
+ const cache = new MemoryCache();
275
398
 
276
- ```javascript
277
- // Worker thread cache coordination
278
- const cache = new MemoryCache('shared', {
279
- coordination: {
280
- type: 'postmessage',
281
- channel: 'cache-coordination'
399
+ // Respect Cache-Control headers
400
+ const response = new Response(data, {
401
+ headers: {
402
+ 'Cache-Control': 'max-age=3600', // Cache for 1 hour
403
+ 'Vary': 'Accept-Encoding',
282
404
  }
283
405
  });
284
406
 
285
- // Operations are coordinated across workers
286
- await cache.put(request, response); // Synced to all workers
407
+ await cache.put(request, response);
408
+
409
+ // After 3600 seconds, entry expires automatically
410
+ const cached = await cache.match(request); // undefined after expiry
287
411
  ```
288
412
 
289
413
  ## License
290
414
 
291
- MIT
415
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b9g/cache",
3
- "version": "0.1.5",
3
+ "version": "0.2.0-beta.0",
4
4
  "description": "Universal Cache API for ServiceWorker applications. Provides standard CacheStorage and Cache interfaces across all JavaScript runtimes.",
5
5
  "keywords": [
6
6
  "cache",
@@ -9,14 +9,11 @@
9
9
  "cachestorage",
10
10
  "web-standards",
11
11
  "universal",
12
- "memory",
13
- "filesystem",
14
12
  "shovel"
15
13
  ],
16
14
  "dependencies": {},
17
15
  "devDependencies": {
18
- "@b9g/libuild": "^0.1.18",
19
- "bun-types": "latest"
16
+ "@b9g/libuild": "^0.1.18"
20
17
  },
21
18
  "type": "module",
22
19
  "types": "src/index.d.ts",
package/src/index.d.ts CHANGED
@@ -25,8 +25,11 @@ export declare function toRequest(request: RequestInfo | URL): Request;
25
25
  * Abstract Cache class implementing the Cache API interface
26
26
  * Provides shared implementations for add() and addAll() while requiring
27
27
  * concrete implementations to handle the core storage operations
28
+ *
29
+ * All cache implementations must follow the constructor signature:
30
+ * constructor(name: string, options?: CacheOptions)
28
31
  */
29
- export declare abstract class Cache {
32
+ export declare abstract class Cache implements globalThis.Cache {
30
33
  /**
31
34
  * Returns a Promise that resolves to the response associated with the first matching request
32
35
  */
@@ -64,6 +67,11 @@ export declare abstract class Cache {
64
67
  * Normalizes the request for consistent cache key generation
65
68
  */
66
69
  export declare function generateCacheKey(request: RequestInfo | URL, options?: CacheQueryOptions): string;
70
+ /**
71
+ * Constructor type for Cache implementations
72
+ * All cache classes must accept name as first parameter and optional options as second
73
+ */
74
+ export type CacheConstructor<T extends Cache = Cache, O = any> = new (name: string, options?: O) => T;
67
75
  /**
68
76
  * Factory function for creating Cache instances based on cache name
69
77
  */
@@ -72,7 +80,7 @@ export type CacheFactory = (name: string) => Cache | Promise<Cache>;
72
80
  * CustomCacheStorage implements CacheStorage interface with a configurable factory
73
81
  * The factory function receives the cache name and can return different cache types
74
82
  */
75
- export declare class CustomCacheStorage {
83
+ export declare class CustomCacheStorage implements CacheStorage {
76
84
  #private;
77
85
  constructor(factory: CacheFactory);
78
86
  /**
@@ -96,13 +104,6 @@ export declare class CustomCacheStorage {
96
104
  * Returns a list of all opened cache names
97
105
  */
98
106
  keys(): Promise<string[]>;
99
- /**
100
- * Get statistics about the cache storage
101
- */
102
- getStats(): {
103
- openInstances: number;
104
- cacheNames: string[];
105
- };
106
107
  /**
107
108
  * Dispose of all cache instances
108
109
  * Calls dispose() on each cache if it exists (e.g., RedisCache needs to close connections)
package/src/index.js CHANGED
@@ -110,15 +110,6 @@ var CustomCacheStorage = class {
110
110
  async keys() {
111
111
  return Array.from(this.#instances.keys());
112
112
  }
113
- /**
114
- * Get statistics about the cache storage
115
- */
116
- getStats() {
117
- return {
118
- openInstances: this.#instances.size,
119
- cacheNames: Array.from(this.#instances.keys())
120
- };
121
- }
122
113
  /**
123
114
  * Dispose of all cache instances
124
115
  * Calls dispose() on each cache if it exists (e.g., RedisCache needs to close connections)
@@ -141,16 +132,23 @@ var CustomCacheStorage = class {
141
132
  try {
142
133
  const cache = await this.open(cacheName);
143
134
  let result;
135
+ const transfer = [];
144
136
  switch (type) {
145
137
  case "cache:match": {
146
138
  const req = new Request(message.request.url, message.request);
147
139
  const response = await cache.match(req, message.options);
148
- result = response ? {
149
- status: response.status,
150
- statusText: response.statusText,
151
- headers: Object.fromEntries(response.headers),
152
- body: await response.text()
153
- } : void 0;
140
+ if (response) {
141
+ const body = await response.arrayBuffer();
142
+ transfer.push(body);
143
+ result = {
144
+ status: response.status,
145
+ statusText: response.statusText,
146
+ headers: Object.fromEntries(response.headers),
147
+ body
148
+ };
149
+ } else {
150
+ result = void 0;
151
+ }
154
152
  break;
155
153
  }
156
154
  case "cache:put": {
@@ -180,7 +178,12 @@ var CustomCacheStorage = class {
180
178
  result = true;
181
179
  break;
182
180
  }
183
- worker.postMessage({ type: "cache:response", requestID, result });
181
+ const responseMessage = { type: "cache:response", requestID, result };
182
+ if (transfer.length > 0) {
183
+ worker.postMessage(responseMessage, transfer);
184
+ } else {
185
+ worker.postMessage(responseMessage);
186
+ }
184
187
  } catch (error) {
185
188
  worker.postMessage({
186
189
  type: "cache:error",
package/src/memory.d.ts CHANGED
@@ -9,10 +9,11 @@ export interface MemoryCacheOptions {
9
9
  /**
10
10
  * In-memory cache implementation using Map for storage
11
11
  * Supports LRU eviction and TTL expiration
12
+ * Uses Map's insertion order for LRU tracking
12
13
  */
13
14
  export declare class MemoryCache extends Cache {
14
15
  #private;
15
- constructor(name: string, options?: MemoryCacheOptions);
16
+ constructor(_name: string, options?: MemoryCacheOptions);
16
17
  /**
17
18
  * Find a cached response for the given request
18
19
  */
@@ -33,13 +34,4 @@ export declare class MemoryCache extends Cache {
33
34
  * Clear all entries from the cache
34
35
  */
35
36
  clear(): Promise<void>;
36
- /**
37
- * Get cache statistics
38
- */
39
- getStats(): {
40
- name: string;
41
- size: number;
42
- maxEntries: number;
43
- hitRate: number;
44
- };
45
37
  }
package/src/memory.js CHANGED
@@ -7,33 +7,31 @@ import {
7
7
  } from "./index.js";
8
8
  var MemoryCache = class extends Cache {
9
9
  #storage;
10
- #accessOrder;
11
- #accessCounter;
12
- #name;
13
10
  #options;
14
- constructor(name, options = {}) {
11
+ constructor(_name, options = {}) {
15
12
  super();
16
13
  this.#storage = /* @__PURE__ */ new Map();
17
- this.#accessOrder = /* @__PURE__ */ new Map();
18
- this.#accessCounter = 0;
19
- this.#name = name;
20
14
  this.#options = options;
21
15
  }
22
16
  /**
23
17
  * Find a cached response for the given request
24
18
  */
25
19
  async match(request, options) {
20
+ const req = toRequest(request);
26
21
  if (options?.ignoreSearch) {
27
22
  const filterKey = generateCacheKey(request, options);
28
23
  for (const [key2, entry2] of this.#storage) {
29
24
  if (this.#isExpired(entry2)) {
30
25
  this.#storage.delete(key2);
31
- this.#accessOrder.delete(key2);
32
26
  continue;
33
27
  }
34
28
  const entryKey = generateCacheKey(entry2.request, options);
35
29
  if (entryKey === filterKey) {
36
- this.#accessOrder.set(key2, ++this.#accessCounter);
30
+ if (!options?.ignoreVary && !this.#matchesVary(req, entry2)) {
31
+ continue;
32
+ }
33
+ this.#storage.delete(key2);
34
+ this.#storage.set(key2, entry2);
37
35
  return entry2.response.clone();
38
36
  }
39
37
  }
@@ -46,10 +44,13 @@ var MemoryCache = class extends Cache {
46
44
  }
47
45
  if (this.#isExpired(entry)) {
48
46
  this.#storage.delete(key);
49
- this.#accessOrder.delete(key);
50
47
  return void 0;
51
48
  }
52
- this.#accessOrder.set(key, ++this.#accessCounter);
49
+ if (!options?.ignoreVary && !this.#matchesVary(req, entry)) {
50
+ return void 0;
51
+ }
52
+ this.#storage.delete(key);
53
+ this.#storage.set(key, entry);
53
54
  return entry.response.clone();
54
55
  }
55
56
  /**
@@ -68,8 +69,10 @@ var MemoryCache = class extends Cache {
68
69
  response: clonedResponse,
69
70
  timestamp: Date.now()
70
71
  };
72
+ if (this.#storage.has(key)) {
73
+ this.#storage.delete(key);
74
+ }
71
75
  this.#storage.set(key, entry);
72
- this.#accessOrder.set(key, ++this.#accessCounter);
73
76
  this.#enforceMaxEntries();
74
77
  }
75
78
  /**
@@ -78,23 +81,18 @@ var MemoryCache = class extends Cache {
78
81
  async delete(request, options) {
79
82
  if (options?.ignoreSearch) {
80
83
  const filterKey = generateCacheKey(request, options);
81
- let deleted2 = false;
84
+ let deleted = false;
82
85
  for (const [key2, entry] of this.#storage) {
83
86
  const entryKey = generateCacheKey(entry.request, options);
84
87
  if (entryKey === filterKey) {
85
88
  this.#storage.delete(key2);
86
- this.#accessOrder.delete(key2);
87
- deleted2 = true;
89
+ deleted = true;
88
90
  }
89
91
  }
90
- return deleted2;
92
+ return deleted;
91
93
  }
92
94
  const key = generateCacheKey(request, options);
93
- const deleted = this.#storage.delete(key);
94
- if (deleted) {
95
- this.#accessOrder.delete(key);
96
- }
97
- return deleted;
95
+ return this.#storage.delete(key);
98
96
  }
99
97
  /**
100
98
  * Get all stored requests, optionally filtered by a request pattern
@@ -122,20 +120,6 @@ var MemoryCache = class extends Cache {
122
120
  */
123
121
  async clear() {
124
122
  this.#storage.clear();
125
- this.#accessOrder.clear();
126
- this.#accessCounter = 0;
127
- }
128
- /**
129
- * Get cache statistics
130
- */
131
- getStats() {
132
- return {
133
- name: this.#name,
134
- size: this.#storage.size,
135
- maxEntries: this.#options.maxEntries,
136
- hitRate: 0
137
- // Could be implemented with additional tracking
138
- };
139
123
  }
140
124
  /**
141
125
  * Check if a cache entry has expired based on Cache-Control header
@@ -152,21 +136,42 @@ var MemoryCache = class extends Cache {
152
136
  const maxAge = parseInt(maxAgeMatch[1], 10) * 1e3;
153
137
  return Date.now() - entry.timestamp > maxAge;
154
138
  }
139
+ /**
140
+ * Check if a request matches the Vary header of a cached entry
141
+ * Returns true if the request matches or if there's no Vary header
142
+ */
143
+ #matchesVary(request, entry) {
144
+ const varyHeader = entry.response.headers.get("vary");
145
+ if (!varyHeader) {
146
+ return true;
147
+ }
148
+ if (varyHeader === "*") {
149
+ return false;
150
+ }
151
+ const varyHeaders = varyHeader.split(",").map((h) => h.trim().toLowerCase());
152
+ for (const headerName of varyHeaders) {
153
+ const requestValue = request.headers.get(headerName);
154
+ const cachedValue = entry.request.headers.get(headerName);
155
+ if (requestValue !== cachedValue) {
156
+ return false;
157
+ }
158
+ }
159
+ return true;
160
+ }
155
161
  /**
156
162
  * Enforce maximum entry limits using LRU eviction
163
+ * Removes oldest entries (first in Map iteration order)
157
164
  */
158
165
  #enforceMaxEntries() {
159
166
  if (!this.#options.maxEntries || this.#storage.size <= this.#options.maxEntries) {
160
167
  return;
161
168
  }
162
- const entries = Array.from(this.#accessOrder.entries()).sort(
163
- (a, b) => a[1] - b[1]
164
- );
165
169
  const toRemove = this.#storage.size - this.#options.maxEntries;
166
- for (let i = 0; i < toRemove; i++) {
167
- const [key] = entries[i];
170
+ let removed = 0;
171
+ for (const key of this.#storage.keys()) {
172
+ if (removed >= toRemove) break;
168
173
  this.#storage.delete(key);
169
- this.#accessOrder.delete(key);
174
+ removed++;
170
175
  }
171
176
  }
172
177
  };
@@ -1,4 +1,9 @@
1
1
  import { Cache, type CacheQueryOptions } from "./index.js";
2
+ /**
3
+ * Handle cache response/error messages from main thread.
4
+ * Called by worker.ts when receiving cache:response or cache:error messages.
5
+ */
6
+ export declare function handleCacheResponse(message: any): void;
2
7
  /**
3
8
  * Configuration options for PostMessageCache
4
9
  */
@@ -7,8 +12,8 @@ export interface PostMessageCacheOptions {
7
12
  timeout?: number;
8
13
  }
9
14
  /**
10
- * Worker-side cache that forwards operations to main thread via postMessage
11
- * Only used for MemoryCache in multi-worker environments
15
+ * Worker-side cache that forwards operations to main thread via postMessage.
16
+ * Used for MemoryCache in multi-worker environments so all workers share state.
12
17
  */
13
18
  export declare class PostMessageCache extends Cache {
14
19
  #private;
@@ -1,25 +1,7 @@
1
1
  /// <reference types="./postmessage.d.ts" />
2
2
  // src/postmessage.ts
3
3
  import { Cache } from "./index.js";
4
- function getParentPort() {
5
- return typeof self !== "undefined" ? self : null;
6
- }
7
- var messageHandlerSetup = false;
8
4
  var pendingRequestsRegistry = /* @__PURE__ */ new Map();
9
- function setupMessageHandler() {
10
- if (messageHandlerSetup)
11
- return;
12
- messageHandlerSetup = true;
13
- const parentPort = getParentPort();
14
- if (parentPort && parentPort.addEventListener) {
15
- parentPort.addEventListener("message", (event) => {
16
- const message = event.data;
17
- if (message.type === "cache:response" || message.type === "cache:error") {
18
- handleCacheResponse(message);
19
- }
20
- });
21
- }
22
- }
23
5
  function handleCacheResponse(message) {
24
6
  const pending = pendingRequestsRegistry.get(message.requestID);
25
7
  if (pending) {
@@ -39,22 +21,30 @@ var PostMessageCache = class extends Cache {
39
21
  super();
40
22
  this.#name = name;
41
23
  this.#timeout = options.timeout ?? 3e4;
42
- setupMessageHandler();
43
24
  }
44
- async #sendRequest(type, data) {
45
- const parentPort = getParentPort();
46
- if (!parentPort) {
25
+ async #sendRequest(type, data, transfer) {
26
+ if (typeof self === "undefined") {
47
27
  throw new Error("PostMessageCache can only be used in worker threads");
48
28
  }
29
+ if (globalRequestID >= Number.MAX_SAFE_INTEGER) {
30
+ throw new Error(
31
+ "Congratulations! You've made 9 quadrillion cache requests. Please restart your server and tell us about your workload."
32
+ );
33
+ }
49
34
  const requestID = ++globalRequestID;
50
35
  return new Promise((resolve, reject) => {
51
36
  pendingRequestsRegistry.set(requestID, { resolve, reject });
52
- parentPort.postMessage({
37
+ const message = {
53
38
  type,
54
39
  requestID,
55
40
  cacheName: this.#name,
56
41
  ...data
57
- });
42
+ };
43
+ if (transfer && transfer.length > 0) {
44
+ self.postMessage(message, transfer);
45
+ } else {
46
+ self.postMessage(message);
47
+ }
58
48
  setTimeout(() => {
59
49
  if (pendingRequestsRegistry.has(requestID)) {
60
50
  pendingRequestsRegistry.delete(requestID);
@@ -64,16 +54,26 @@ var PostMessageCache = class extends Cache {
64
54
  });
65
55
  }
66
56
  async match(request, options) {
57
+ let requestBody;
58
+ const transfer = [];
59
+ if (request.method !== "GET" && request.method !== "HEAD") {
60
+ requestBody = await request.arrayBuffer();
61
+ transfer.push(requestBody);
62
+ }
67
63
  const serializedRequest = {
68
64
  url: request.url,
69
65
  method: request.method,
70
66
  headers: Object.fromEntries(request.headers),
71
- body: request.method !== "GET" && request.method !== "HEAD" ? await request.text() : void 0
67
+ body: requestBody
72
68
  };
73
- const response = await this.#sendRequest("cache:match", {
74
- request: serializedRequest,
75
- options
76
- });
69
+ const response = await this.#sendRequest(
70
+ "cache:match",
71
+ {
72
+ request: serializedRequest,
73
+ options
74
+ },
75
+ transfer
76
+ );
77
77
  if (!response) {
78
78
  return void 0;
79
79
  }
@@ -84,49 +84,82 @@ var PostMessageCache = class extends Cache {
84
84
  });
85
85
  }
86
86
  async put(request, response) {
87
+ const transfer = [];
88
+ let requestBody;
89
+ let responseBody;
90
+ if (request.method !== "GET" && request.method !== "HEAD") {
91
+ requestBody = await request.clone().arrayBuffer();
92
+ transfer.push(requestBody);
93
+ }
94
+ responseBody = await response.clone().arrayBuffer();
95
+ transfer.push(responseBody);
87
96
  const serializedRequest = {
88
97
  url: request.url,
89
98
  method: request.method,
90
99
  headers: Object.fromEntries(request.headers),
91
- body: request.method !== "GET" && request.method !== "HEAD" ? await request.clone().text() : void 0
100
+ body: requestBody
92
101
  };
93
102
  const serializedResponse = {
94
103
  status: response.status,
95
104
  statusText: response.statusText,
96
105
  headers: Object.fromEntries(response.headers),
97
- body: await response.clone().text()
106
+ body: responseBody
98
107
  };
99
- await this.#sendRequest("cache:put", {
100
- request: serializedRequest,
101
- response: serializedResponse
102
- });
108
+ await this.#sendRequest(
109
+ "cache:put",
110
+ {
111
+ request: serializedRequest,
112
+ response: serializedResponse
113
+ },
114
+ transfer
115
+ );
103
116
  }
104
117
  async delete(request, options) {
118
+ let requestBody;
119
+ const transfer = [];
120
+ if (request.method !== "GET" && request.method !== "HEAD") {
121
+ requestBody = await request.arrayBuffer();
122
+ transfer.push(requestBody);
123
+ }
105
124
  const serializedRequest = {
106
125
  url: request.url,
107
126
  method: request.method,
108
127
  headers: Object.fromEntries(request.headers),
109
- body: request.method !== "GET" && request.method !== "HEAD" ? await request.text() : void 0
128
+ body: requestBody
110
129
  };
111
- return await this.#sendRequest("cache:delete", {
112
- request: serializedRequest,
113
- options
114
- });
130
+ return await this.#sendRequest(
131
+ "cache:delete",
132
+ {
133
+ request: serializedRequest,
134
+ options
135
+ },
136
+ transfer
137
+ );
115
138
  }
116
139
  async keys(request, options) {
117
140
  let serializedRequest;
141
+ const transfer = [];
118
142
  if (request) {
143
+ let requestBody;
144
+ if (request.method !== "GET" && request.method !== "HEAD") {
145
+ requestBody = await request.arrayBuffer();
146
+ transfer.push(requestBody);
147
+ }
119
148
  serializedRequest = {
120
149
  url: request.url,
121
150
  method: request.method,
122
151
  headers: Object.fromEntries(request.headers),
123
- body: request.method !== "GET" && request.method !== "HEAD" ? await request.text() : void 0
152
+ body: requestBody
124
153
  };
125
154
  }
126
- const keys = await this.#sendRequest("cache:keys", {
127
- request: serializedRequest,
128
- options
129
- });
155
+ const keys = await this.#sendRequest(
156
+ "cache:keys",
157
+ {
158
+ request: serializedRequest,
159
+ options
160
+ },
161
+ transfer
162
+ );
130
163
  return keys.map(
131
164
  (req) => new Request(req.url, {
132
165
  method: req.method,
@@ -140,5 +173,6 @@ var PostMessageCache = class extends Cache {
140
173
  }
141
174
  };
142
175
  export {
143
- PostMessageCache
176
+ PostMessageCache,
177
+ handleCacheResponse
144
178
  };