@b9g/cache 0.1.3 → 0.1.5

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
@@ -1,13 +1,13 @@
1
1
  # @b9g/cache
2
2
 
3
- Universal Cache API implementation providing CacheStorage and Cache interfaces across all JavaScript runtimes.
3
+ **Universal Cache API for ServiceWorker applications. Provides standard CacheStorage and Cache interfaces across all JavaScript runtimes.**
4
4
 
5
5
  ## Features
6
6
 
7
- - **Standard APIs**: Implements the Cache and CacheStorage APIs from service workers
8
- - **Multiple Backends**: Memory, filesystem, Redis, KV store implementations
7
+ - **ServiceWorker Cache API**: Standard `caches` global and Cache interface from ServiceWorker spec
8
+ - **Multiple Backends**: Memory, filesystem, Redis, KV store implementations
9
9
  - **Universal**: Same API works in browsers, Node.js, Bun, and edge platforms
10
- - **Request/Response**: HTTP-semantic caching with full Request/Response support
10
+ - **Request/Response Caching**: Full HTTP semantics with Request/Response objects
11
11
  - **Registry Pattern**: Named cache management with factory registration
12
12
 
13
13
  ## Installation
@@ -96,7 +96,23 @@ import { Router } from '@b9g/router';
96
96
  const router = new Router({ caches });
97
97
  ```
98
98
 
99
- ## Cache API Reference
99
+ ## Exports
100
+
101
+ ### Classes
102
+
103
+ - `Cache` - Abstract base class for cache implementations
104
+ - `CustomCacheStorage` - CacheStorage implementation with factory registration
105
+
106
+ ### Functions
107
+
108
+ - `generateCacheKey(request, options?)` - Generate a cache key from a Request
109
+
110
+ ### Types
111
+
112
+ - `CacheQueryOptions` - Options for cache query operations (ignoreSearch, ignoreMethod, ignoreVary)
113
+ - `CacheFactory` - Factory function type `(name: string) => Cache | Promise<Cache>`
114
+
115
+ ## API Reference
100
116
 
101
117
  ### Standard Cache Methods
102
118
 
@@ -186,13 +202,13 @@ caches.register('api', () => new MemoryCache('api'));
186
202
  const router = new Router({ caches });
187
203
 
188
204
  // Cache-aware middleware
189
- router.use(async (request, context, next) => {
205
+ router.use(async function* (request, context) {
190
206
  if (request.method === 'GET' && context.cache) {
191
207
  const cached = await context.cache.match(request);
192
208
  if (cached) return cached;
193
209
  }
194
210
 
195
- const response = await next();
211
+ const response = yield request;
196
212
 
197
213
  if (request.method === 'GET' && context.cache && response.ok) {
198
214
  await context.cache.put(request, response.clone());
package/package.json CHANGED
@@ -1,16 +1,21 @@
1
1
  {
2
2
  "name": "@b9g/cache",
3
- "version": "0.1.3",
4
- "description": "Universal Cache API implementation",
3
+ "version": "0.1.5",
4
+ "description": "Universal Cache API for ServiceWorker applications. Provides standard CacheStorage and Cache interfaces across all JavaScript runtimes.",
5
5
  "keywords": [
6
6
  "cache",
7
7
  "storage",
8
+ "serviceworker",
9
+ "cachestorage",
8
10
  "web-standards",
9
- "universal"
11
+ "universal",
12
+ "memory",
13
+ "filesystem",
14
+ "shovel"
10
15
  ],
11
16
  "dependencies": {},
12
17
  "devDependencies": {
13
- "@b9g/libuild": "^0.1.11",
18
+ "@b9g/libuild": "^0.1.18",
14
19
  "bun-types": "latest"
15
20
  },
16
21
  "type": "module",
@@ -21,10 +26,6 @@
21
26
  "types": "./src/index.d.ts",
22
27
  "import": "./src/index.js"
23
28
  },
24
- "./cache": {
25
- "types": "./src/cache.d.ts",
26
- "import": "./src/cache.js"
27
- },
28
29
  "./memory": {
29
30
  "types": "./src/memory.d.ts",
30
31
  "import": "./src/memory.js"
@@ -34,18 +35,6 @@
34
35
  "import": "./src/postmessage.js"
35
36
  },
36
37
  "./package.json": "./package.json",
37
- "./cache.js": {
38
- "types": "./src/cache.d.ts",
39
- "import": "./src/cache.js"
40
- },
41
- "./cache-storage": {
42
- "types": "./src/cache-storage.d.ts",
43
- "import": "./src/cache-storage.js"
44
- },
45
- "./cache-storage.js": {
46
- "types": "./src/cache-storage.d.ts",
47
- "import": "./src/cache-storage.js"
48
- },
49
38
  "./index": {
50
39
  "types": "./src/index.d.ts",
51
40
  "import": "./src/index.js"
package/src/index.d.ts CHANGED
@@ -3,15 +3,113 @@
3
3
  *
4
4
  * Provides HTTP-aware caching with PostMessage coordination for worker environments
5
5
  */
6
- export { Cache, type CacheQueryOptions, generateCacheKey, cloneResponse } from "./cache.js";
7
- export { CustomCacheStorage, type CacheFactory } from "./cache-storage.js";
8
- export { MemoryCache, MemoryCacheManager, type MemoryCacheOptions } from "./memory.js";
9
- export { PostMessageCache, type PostMessageCacheOptions } from "./postmessage.js";
10
- import { MemoryCache, type MemoryCacheOptions } from "./memory.js";
11
6
  /**
12
- * Platform adapter factory function
13
- * Creates a MemoryCache instance with the given configuration
7
+ * Cache query options for matching requests
8
+ * Based on the Cache API specification
14
9
  */
15
- export declare function createCache(config?: MemoryCacheOptions & {
16
- name?: string;
17
- }): MemoryCache;
10
+ export interface CacheQueryOptions {
11
+ /** Ignore the search portion of the request URL */
12
+ ignoreSearch?: boolean;
13
+ /** Ignore the request method */
14
+ ignoreMethod?: boolean;
15
+ /** Ignore the Vary header */
16
+ ignoreVary?: boolean;
17
+ /** Custom cache name for scoped operations */
18
+ cacheName?: string;
19
+ }
20
+ /**
21
+ * Convert RequestInfo or URL to Request
22
+ */
23
+ export declare function toRequest(request: RequestInfo | URL): Request;
24
+ /**
25
+ * Abstract Cache class implementing the Cache API interface
26
+ * Provides shared implementations for add() and addAll() while requiring
27
+ * concrete implementations to handle the core storage operations
28
+ */
29
+ export declare abstract class Cache {
30
+ /**
31
+ * Returns a Promise that resolves to the response associated with the first matching request
32
+ */
33
+ abstract match(request: RequestInfo | URL, options?: CacheQueryOptions): Promise<Response | undefined>;
34
+ /**
35
+ * Puts a request/response pair into the cache
36
+ */
37
+ abstract put(request: RequestInfo | URL, response: Response): Promise<void>;
38
+ /**
39
+ * Finds the cache entry whose key is the request, and if found, deletes it and returns true
40
+ */
41
+ abstract delete(request: RequestInfo | URL, options?: CacheQueryOptions): Promise<boolean>;
42
+ /**
43
+ * Returns a Promise that resolves to an array of cache keys (Request objects)
44
+ */
45
+ abstract keys(request?: RequestInfo | URL, options?: CacheQueryOptions): Promise<readonly Request[]>;
46
+ /**
47
+ * Takes a URL, retrieves it and adds the resulting response object to the cache
48
+ * Shared implementation using fetch() and put()
49
+ */
50
+ add(request: Request): Promise<void>;
51
+ /**
52
+ * Takes an array of URLs, retrieves them, and adds the resulting response objects to the cache
53
+ * Shared implementation using add()
54
+ */
55
+ addAll(requests: Request[]): Promise<void>;
56
+ /**
57
+ * Returns a Promise that resolves to an array of all matching responses
58
+ * Default implementation using keys() and match() - can be overridden for efficiency
59
+ */
60
+ matchAll(request?: Request, options?: CacheQueryOptions): Promise<readonly Response[]>;
61
+ }
62
+ /**
63
+ * Generate a cache key from a Request, string URL, or URL object
64
+ * Normalizes the request for consistent cache key generation
65
+ */
66
+ export declare function generateCacheKey(request: RequestInfo | URL, options?: CacheQueryOptions): string;
67
+ /**
68
+ * Factory function for creating Cache instances based on cache name
69
+ */
70
+ export type CacheFactory = (name: string) => Cache | Promise<Cache>;
71
+ /**
72
+ * CustomCacheStorage implements CacheStorage interface with a configurable factory
73
+ * The factory function receives the cache name and can return different cache types
74
+ */
75
+ export declare class CustomCacheStorage {
76
+ #private;
77
+ constructor(factory: CacheFactory);
78
+ /**
79
+ * Matches a request across all caches
80
+ */
81
+ match(request: Request, options?: CacheQueryOptions): Promise<Response | undefined>;
82
+ /**
83
+ * Opens a cache with the given name
84
+ * Returns existing instance if already opened, otherwise creates a new one
85
+ */
86
+ open(name: string): Promise<Cache>;
87
+ /**
88
+ * Returns true if a cache with the given name exists (has been opened)
89
+ */
90
+ has(name: string): Promise<boolean>;
91
+ /**
92
+ * Deletes a cache with the given name
93
+ */
94
+ delete(name: string): Promise<boolean>;
95
+ /**
96
+ * Returns a list of all opened cache names
97
+ */
98
+ keys(): Promise<string[]>;
99
+ /**
100
+ * Get statistics about the cache storage
101
+ */
102
+ getStats(): {
103
+ openInstances: number;
104
+ cacheNames: string[];
105
+ };
106
+ /**
107
+ * Dispose of all cache instances
108
+ * Calls dispose() on each cache if it exists (e.g., RedisCache needs to close connections)
109
+ */
110
+ dispose(): Promise<void>;
111
+ /**
112
+ * Handle cache messages from worker threads (PostMessageCache coordination)
113
+ */
114
+ handleMessage(worker: any, message: any): Promise<void>;
115
+ }
package/src/index.js CHANGED
@@ -1,21 +1,198 @@
1
1
  /// <reference types="./index.d.ts" />
2
2
  // src/index.ts
3
- import { Cache, generateCacheKey, cloneResponse } from "./cache.js";
4
- import { CustomCacheStorage } from "./cache-storage.js";
5
- import { MemoryCache, MemoryCacheManager } from "./memory.js";
6
- import { PostMessageCache } from "./postmessage.js";
7
- import { MemoryCache as MemoryCache2 } from "./memory.js";
8
- function createCache(config = {}) {
9
- const name = config.name || "default";
10
- return new MemoryCache2(name, config);
3
+ function toRequest(request) {
4
+ if (typeof request === "string") {
5
+ return new Request(request);
6
+ }
7
+ if (request instanceof URL) {
8
+ return new Request(request.href);
9
+ }
10
+ return request;
11
11
  }
12
+ var Cache = class {
13
+ /**
14
+ * Takes a URL, retrieves it and adds the resulting response object to the cache
15
+ * Shared implementation using fetch() and put()
16
+ */
17
+ async add(request) {
18
+ const response = await fetch(request);
19
+ if (!response.ok) {
20
+ throw new TypeError(
21
+ `Failed to fetch ${request.url}: ${response.status} ${response.statusText}`
22
+ );
23
+ }
24
+ await this.put(request, response);
25
+ }
26
+ /**
27
+ * Takes an array of URLs, retrieves them, and adds the resulting response objects to the cache
28
+ * Shared implementation using add()
29
+ */
30
+ async addAll(requests) {
31
+ await Promise.all(requests.map((request) => this.add(request)));
32
+ }
33
+ /**
34
+ * Returns a Promise that resolves to an array of all matching responses
35
+ * Default implementation using keys() and match() - can be overridden for efficiency
36
+ */
37
+ async matchAll(request, options) {
38
+ const keys = await this.keys(request, options);
39
+ const responses = [];
40
+ for (const key of keys) {
41
+ const response = await this.match(key, options);
42
+ if (response) {
43
+ responses.push(response);
44
+ }
45
+ }
46
+ return responses;
47
+ }
48
+ };
49
+ function generateCacheKey(request, options) {
50
+ const req = toRequest(request);
51
+ const url = new URL(req.url);
52
+ if (options?.ignoreSearch) {
53
+ url.search = "";
54
+ }
55
+ const method = options?.ignoreMethod ? "GET" : req.method;
56
+ return `${method}:${url.href}`;
57
+ }
58
+ var CustomCacheStorage = class {
59
+ #instances;
60
+ #factory;
61
+ constructor(factory) {
62
+ this.#instances = /* @__PURE__ */ new Map();
63
+ this.#factory = factory;
64
+ }
65
+ /**
66
+ * Matches a request across all caches
67
+ */
68
+ async match(request, options) {
69
+ for (const cache of this.#instances.values()) {
70
+ const response = await cache.match(request, options);
71
+ if (response) {
72
+ return response;
73
+ }
74
+ }
75
+ return void 0;
76
+ }
77
+ /**
78
+ * Opens a cache with the given name
79
+ * Returns existing instance if already opened, otherwise creates a new one
80
+ */
81
+ async open(name) {
82
+ const existingInstance = this.#instances.get(name);
83
+ if (existingInstance) {
84
+ return existingInstance;
85
+ }
86
+ const cache = await this.#factory(name);
87
+ this.#instances.set(name, cache);
88
+ return cache;
89
+ }
90
+ /**
91
+ * Returns true if a cache with the given name exists (has been opened)
92
+ */
93
+ async has(name) {
94
+ return this.#instances.has(name);
95
+ }
96
+ /**
97
+ * Deletes a cache with the given name
98
+ */
99
+ async delete(name) {
100
+ const instance = this.#instances.get(name);
101
+ if (instance) {
102
+ this.#instances.delete(name);
103
+ return true;
104
+ }
105
+ return false;
106
+ }
107
+ /**
108
+ * Returns a list of all opened cache names
109
+ */
110
+ async keys() {
111
+ return Array.from(this.#instances.keys());
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
+ /**
123
+ * Dispose of all cache instances
124
+ * Calls dispose() on each cache if it exists (e.g., RedisCache needs to close connections)
125
+ */
126
+ async dispose() {
127
+ const disposePromises = [];
128
+ for (const cache of this.#instances.values()) {
129
+ if (typeof cache.dispose === "function") {
130
+ disposePromises.push(cache.dispose());
131
+ }
132
+ }
133
+ await Promise.allSettled(disposePromises);
134
+ this.#instances.clear();
135
+ }
136
+ /**
137
+ * Handle cache messages from worker threads (PostMessageCache coordination)
138
+ */
139
+ async handleMessage(worker, message) {
140
+ const { type, requestID, cacheName } = message;
141
+ try {
142
+ const cache = await this.open(cacheName);
143
+ let result;
144
+ switch (type) {
145
+ case "cache:match": {
146
+ const req = new Request(message.request.url, message.request);
147
+ 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;
154
+ break;
155
+ }
156
+ case "cache:put": {
157
+ const req = new Request(message.request.url, message.request);
158
+ const res = new Response(message.response.body, message.response);
159
+ await cache.put(req, res);
160
+ result = true;
161
+ break;
162
+ }
163
+ case "cache:delete": {
164
+ const req = new Request(message.request.url, message.request);
165
+ result = await cache.delete(req, message.options);
166
+ break;
167
+ }
168
+ case "cache:keys": {
169
+ const req = message.request ? new Request(message.request.url, message.request) : void 0;
170
+ const keys = await cache.keys(req, message.options);
171
+ result = keys.map((r) => ({
172
+ url: r.url,
173
+ method: r.method,
174
+ headers: Object.fromEntries(r.headers.entries())
175
+ }));
176
+ break;
177
+ }
178
+ case "cache:clear":
179
+ await cache.clear?.();
180
+ result = true;
181
+ break;
182
+ }
183
+ worker.postMessage({ type: "cache:response", requestID, result });
184
+ } catch (error) {
185
+ worker.postMessage({
186
+ type: "cache:error",
187
+ requestID,
188
+ error: error.message
189
+ });
190
+ }
191
+ }
192
+ };
12
193
  export {
13
194
  Cache,
14
195
  CustomCacheStorage,
15
- MemoryCache,
16
- MemoryCacheManager,
17
- PostMessageCache,
18
- cloneResponse,
19
- createCache,
20
- generateCacheKey
196
+ generateCacheKey,
197
+ toRequest
21
198
  };
package/src/memory.d.ts CHANGED
@@ -1,40 +1,34 @@
1
- import { Cache, type CacheQueryOptions } from "./cache.js";
1
+ import { Cache, type CacheQueryOptions } from "./index.js";
2
2
  /**
3
3
  * Configuration options for MemoryCache
4
4
  */
5
5
  export interface MemoryCacheOptions {
6
6
  /** Maximum number of entries to store */
7
7
  maxEntries?: number;
8
- /** Maximum age of entries in milliseconds */
9
- maxAge?: number;
10
8
  }
11
9
  /**
12
10
  * In-memory cache implementation using Map for storage
13
11
  * Supports LRU eviction and TTL expiration
14
12
  */
15
13
  export declare class MemoryCache extends Cache {
16
- private name;
17
- private options;
18
- private storage;
19
- private accessOrder;
20
- private accessCounter;
14
+ #private;
21
15
  constructor(name: string, options?: MemoryCacheOptions);
22
16
  /**
23
17
  * Find a cached response for the given request
24
18
  */
25
- match(request: Request, options?: CacheQueryOptions): Promise<Response | undefined>;
19
+ match(request: RequestInfo | URL, options?: CacheQueryOptions): Promise<Response | undefined>;
26
20
  /**
27
21
  * Store a request/response pair in the cache
28
22
  */
29
- put(request: Request, response: Response): Promise<void>;
23
+ put(request: RequestInfo | URL, response: Response): Promise<void>;
30
24
  /**
31
25
  * Delete matching entries from the cache
32
26
  */
33
- delete(request: Request, options?: CacheQueryOptions): Promise<boolean>;
27
+ delete(request: RequestInfo | URL, options?: CacheQueryOptions): Promise<boolean>;
34
28
  /**
35
29
  * Get all stored requests, optionally filtered by a request pattern
36
30
  */
37
- keys(request?: Request, options?: CacheQueryOptions): Promise<Request[]>;
31
+ keys(request?: RequestInfo | URL, options?: CacheQueryOptions): Promise<readonly Request[]>;
38
32
  /**
39
33
  * Clear all entries from the cache
40
34
  */
@@ -46,77 +40,6 @@ export declare class MemoryCache extends Cache {
46
40
  name: string;
47
41
  size: number;
48
42
  maxEntries: number;
49
- maxAge: number;
50
43
  hitRate: number;
51
44
  };
52
- /**
53
- * Dispose of the cache and clean up resources
54
- */
55
- dispose(): Promise<void>;
56
- /**
57
- * Check if a cache entry has expired
58
- */
59
- private isExpired;
60
- /**
61
- * Check if a response should be cached
62
- */
63
- private isCacheable;
64
- /**
65
- * Enforce maximum entry limits using LRU eviction
66
- */
67
- private enforceMaxEntries;
68
- }
69
- /**
70
- * Memory Cache Manager for Main Thread
71
- *
72
- * Coordinates MemoryCache operations across Worker threads by managing
73
- * shared MemoryCache instances and handling postMessage requests.
74
- *
75
- * Only MemoryCache needs coordination since it stores data in process memory.
76
- * Other cache types can be used directly by workers without coordination.
77
- */
78
- interface WorkerLike {
79
- postMessage(value: any): void;
80
- on(event: string, listener: (data: any) => void): void;
81
- }
82
- interface CacheMessage {
83
- type: string;
84
- requestId: string;
85
- cacheName: string;
86
- request?: SerializedRequest;
87
- response?: SerializedResponse;
88
- options?: any;
89
- }
90
- interface SerializedRequest {
91
- url: string;
92
- method: string;
93
- headers: Record<string, string>;
94
- body?: string;
95
- }
96
- interface SerializedResponse {
97
- status: number;
98
- statusText: string;
99
- headers: Record<string, string>;
100
- body: string;
101
- }
102
- export declare class MemoryCacheManager {
103
- private memoryCaches;
104
- /**
105
- * Handle memory cache-related message from a Worker
106
- */
107
- handleMessage(worker: WorkerLike, message: CacheMessage): Promise<void>;
108
- /**
109
- * Get or create a MemoryCache instance
110
- */
111
- private getMemoryCache;
112
- private handleMatch;
113
- private handlePut;
114
- private handleDelete;
115
- private handleKeys;
116
- private handleClear;
117
- /**
118
- * Dispose of all memory caches
119
- */
120
- dispose(): Promise<void>;
121
45
  }
122
- export {};
package/src/memory.js CHANGED
@@ -3,62 +3,96 @@
3
3
  import {
4
4
  Cache,
5
5
  generateCacheKey,
6
- cloneResponse
7
- } from "./cache.js";
6
+ toRequest
7
+ } from "./index.js";
8
8
  var MemoryCache = class extends Cache {
9
+ #storage;
10
+ #accessOrder;
11
+ #accessCounter;
12
+ #name;
13
+ #options;
9
14
  constructor(name, options = {}) {
10
15
  super();
11
- this.name = name;
12
- this.options = options;
16
+ this.#storage = /* @__PURE__ */ new Map();
17
+ this.#accessOrder = /* @__PURE__ */ new Map();
18
+ this.#accessCounter = 0;
19
+ this.#name = name;
20
+ this.#options = options;
13
21
  }
14
- storage = /* @__PURE__ */ new Map();
15
- accessOrder = /* @__PURE__ */ new Map();
16
- // For LRU tracking
17
- accessCounter = 0;
18
22
  /**
19
23
  * Find a cached response for the given request
20
24
  */
21
25
  async match(request, options) {
26
+ if (options?.ignoreSearch) {
27
+ const filterKey = generateCacheKey(request, options);
28
+ for (const [key2, entry2] of this.#storage) {
29
+ if (this.#isExpired(entry2)) {
30
+ this.#storage.delete(key2);
31
+ this.#accessOrder.delete(key2);
32
+ continue;
33
+ }
34
+ const entryKey = generateCacheKey(entry2.request, options);
35
+ if (entryKey === filterKey) {
36
+ this.#accessOrder.set(key2, ++this.#accessCounter);
37
+ return entry2.response.clone();
38
+ }
39
+ }
40
+ return void 0;
41
+ }
22
42
  const key = generateCacheKey(request, options);
23
- const entry = this.storage.get(key);
43
+ const entry = this.#storage.get(key);
24
44
  if (!entry) {
25
45
  return void 0;
26
46
  }
27
- if (this.isExpired(entry)) {
28
- this.storage.delete(key);
29
- this.accessOrder.delete(key);
47
+ if (this.#isExpired(entry)) {
48
+ this.#storage.delete(key);
49
+ this.#accessOrder.delete(key);
30
50
  return void 0;
31
51
  }
32
- this.accessOrder.set(key, ++this.accessCounter);
33
- return cloneResponse(entry.response);
52
+ this.#accessOrder.set(key, ++this.#accessCounter);
53
+ return entry.response.clone();
34
54
  }
35
55
  /**
36
56
  * Store a request/response pair in the cache
37
57
  */
38
58
  async put(request, response) {
39
- const key = generateCacheKey(request);
40
- if (!this.isCacheable(response)) {
41
- return;
59
+ const req = toRequest(request);
60
+ const key = generateCacheKey(req);
61
+ if (response.bodyUsed) {
62
+ throw new TypeError("Response body has already been used");
42
63
  }
43
- const clonedRequest = request.clone();
44
- const clonedResponse = await cloneResponse(response);
64
+ const clonedRequest = req.clone();
65
+ const clonedResponse = response.clone();
45
66
  const entry = {
46
67
  request: clonedRequest,
47
68
  response: clonedResponse,
48
69
  timestamp: Date.now()
49
70
  };
50
- this.storage.set(key, entry);
51
- this.accessOrder.set(key, ++this.accessCounter);
52
- this.enforceMaxEntries();
71
+ this.#storage.set(key, entry);
72
+ this.#accessOrder.set(key, ++this.#accessCounter);
73
+ this.#enforceMaxEntries();
53
74
  }
54
75
  /**
55
76
  * Delete matching entries from the cache
56
77
  */
57
78
  async delete(request, options) {
79
+ if (options?.ignoreSearch) {
80
+ const filterKey = generateCacheKey(request, options);
81
+ let deleted2 = false;
82
+ for (const [key2, entry] of this.#storage) {
83
+ const entryKey = generateCacheKey(entry.request, options);
84
+ if (entryKey === filterKey) {
85
+ this.#storage.delete(key2);
86
+ this.#accessOrder.delete(key2);
87
+ deleted2 = true;
88
+ }
89
+ }
90
+ return deleted2;
91
+ }
58
92
  const key = generateCacheKey(request, options);
59
- const deleted = this.storage.delete(key);
93
+ const deleted = this.#storage.delete(key);
60
94
  if (deleted) {
61
- this.accessOrder.delete(key);
95
+ this.#accessOrder.delete(key);
62
96
  }
63
97
  return deleted;
64
98
  }
@@ -67,8 +101,8 @@ var MemoryCache = class extends Cache {
67
101
  */
68
102
  async keys(request, options) {
69
103
  const keys = [];
70
- for (const [_, entry] of this.storage) {
71
- if (this.isExpired(entry)) {
104
+ for (const [_, entry] of this.#storage) {
105
+ if (this.#isExpired(entry)) {
72
106
  continue;
73
107
  }
74
108
  if (!request) {
@@ -87,208 +121,55 @@ var MemoryCache = class extends Cache {
87
121
  * Clear all entries from the cache
88
122
  */
89
123
  async clear() {
90
- this.storage.clear();
91
- this.accessOrder.clear();
92
- this.accessCounter = 0;
124
+ this.#storage.clear();
125
+ this.#accessOrder.clear();
126
+ this.#accessCounter = 0;
93
127
  }
94
128
  /**
95
129
  * Get cache statistics
96
130
  */
97
131
  getStats() {
98
132
  return {
99
- name: this.name,
100
- size: this.storage.size,
101
- maxEntries: this.options.maxEntries,
102
- maxAge: this.options.maxAge,
133
+ name: this.#name,
134
+ size: this.#storage.size,
135
+ maxEntries: this.#options.maxEntries,
103
136
  hitRate: 0
104
137
  // Could be implemented with additional tracking
105
138
  };
106
139
  }
107
140
  /**
108
- * Dispose of the cache and clean up resources
109
- */
110
- async dispose() {
111
- await this.clear();
112
- }
113
- /**
114
- * Check if a cache entry has expired
141
+ * Check if a cache entry has expired based on Cache-Control header
115
142
  */
116
- isExpired(entry) {
117
- if (!this.options.maxAge) {
143
+ #isExpired(entry) {
144
+ const cacheControl = entry.response.headers.get("cache-control");
145
+ if (!cacheControl) {
118
146
  return false;
119
147
  }
120
- return Date.now() - entry.timestamp > this.options.maxAge;
121
- }
122
- /**
123
- * Check if a response should be cached
124
- */
125
- isCacheable(response) {
126
- if (!response.ok) {
148
+ const maxAgeMatch = cacheControl.match(/max-age=(\d+)/);
149
+ if (!maxAgeMatch) {
127
150
  return false;
128
151
  }
129
- const cacheControl = response.headers.get("cache-control");
130
- if (cacheControl) {
131
- if (cacheControl.includes("no-cache") || cacheControl.includes("no-store")) {
132
- return false;
133
- }
134
- }
135
- return true;
152
+ const maxAge = parseInt(maxAgeMatch[1], 10) * 1e3;
153
+ return Date.now() - entry.timestamp > maxAge;
136
154
  }
137
155
  /**
138
156
  * Enforce maximum entry limits using LRU eviction
139
157
  */
140
- enforceMaxEntries() {
141
- if (!this.options.maxEntries || this.storage.size <= this.options.maxEntries) {
158
+ #enforceMaxEntries() {
159
+ if (!this.#options.maxEntries || this.#storage.size <= this.#options.maxEntries) {
142
160
  return;
143
161
  }
144
- const entries = Array.from(this.accessOrder.entries()).sort((a, b) => a[1] - b[1]);
145
- const toRemove = this.storage.size - this.options.maxEntries;
162
+ const entries = Array.from(this.#accessOrder.entries()).sort(
163
+ (a, b) => a[1] - b[1]
164
+ );
165
+ const toRemove = this.#storage.size - this.#options.maxEntries;
146
166
  for (let i = 0; i < toRemove; i++) {
147
167
  const [key] = entries[i];
148
- this.storage.delete(key);
149
- this.accessOrder.delete(key);
168
+ this.#storage.delete(key);
169
+ this.#accessOrder.delete(key);
150
170
  }
151
171
  }
152
172
  };
153
- var MemoryCacheManager = class {
154
- memoryCaches = /* @__PURE__ */ new Map();
155
- /**
156
- * Handle memory cache-related message from a Worker
157
- */
158
- async handleMessage(worker, message) {
159
- const { type, requestId } = message;
160
- try {
161
- let result;
162
- switch (type) {
163
- case "cache:match":
164
- result = await this.handleMatch(message);
165
- break;
166
- case "cache:put":
167
- result = await this.handlePut(message);
168
- break;
169
- case "cache:delete":
170
- result = await this.handleDelete(message);
171
- break;
172
- case "cache:keys":
173
- result = await this.handleKeys(message);
174
- break;
175
- case "cache:clear":
176
- result = await this.handleClear(message);
177
- break;
178
- default:
179
- throw new Error(`Unknown cache operation: ${type}`);
180
- }
181
- worker.postMessage({
182
- type: "cache:response",
183
- requestId,
184
- result
185
- });
186
- } catch (error) {
187
- worker.postMessage({
188
- type: "cache:error",
189
- requestId,
190
- error: error.message
191
- });
192
- }
193
- }
194
- /**
195
- * Get or create a MemoryCache instance
196
- */
197
- getMemoryCache(name, options) {
198
- if (!this.memoryCaches.has(name)) {
199
- this.memoryCaches.set(name, new MemoryCache(name, options));
200
- }
201
- return this.memoryCaches.get(name);
202
- }
203
- async handleMatch(message) {
204
- const { cacheName, request, options } = message;
205
- if (!request)
206
- throw new Error("Request is required for match operation");
207
- const cache = this.getMemoryCache(cacheName);
208
- const req = new Request(request.url, {
209
- method: request.method,
210
- headers: request.headers,
211
- body: request.body
212
- });
213
- const response = await cache.match(req, options);
214
- if (!response) {
215
- return void 0;
216
- }
217
- return {
218
- status: response.status,
219
- statusText: response.statusText,
220
- headers: Object.fromEntries(response.headers),
221
- body: await response.text()
222
- };
223
- }
224
- async handlePut(message) {
225
- const { cacheName, request, response } = message;
226
- if (!request || !response)
227
- throw new Error("Request and response are required for put operation");
228
- const cache = this.getMemoryCache(cacheName);
229
- const req = new Request(request.url, {
230
- method: request.method,
231
- headers: request.headers,
232
- body: request.body
233
- });
234
- const res = new Response(response.body, {
235
- status: response.status,
236
- statusText: response.statusText,
237
- headers: response.headers
238
- });
239
- await cache.put(req, res);
240
- return true;
241
- }
242
- async handleDelete(message) {
243
- const { cacheName, request, options } = message;
244
- if (!request)
245
- throw new Error("Request is required for delete operation");
246
- const cache = this.getMemoryCache(cacheName);
247
- const req = new Request(request.url, {
248
- method: request.method,
249
- headers: request.headers,
250
- body: request.body
251
- });
252
- return await cache.delete(req, options);
253
- }
254
- async handleKeys(message) {
255
- const { cacheName, request, options } = message;
256
- const cache = this.getMemoryCache(cacheName);
257
- let req;
258
- if (request) {
259
- req = new Request(request.url, {
260
- method: request.method,
261
- headers: request.headers,
262
- body: request.body
263
- });
264
- }
265
- const keys = await cache.keys(req, options);
266
- return keys.map((r) => ({
267
- url: r.url,
268
- method: r.method,
269
- headers: Object.fromEntries(r.headers),
270
- body: void 0
271
- // Keys typically don't need body
272
- }));
273
- }
274
- async handleClear(message) {
275
- const { cacheName } = message;
276
- const cache = this.getMemoryCache(cacheName);
277
- await cache.clear();
278
- return true;
279
- }
280
- /**
281
- * Dispose of all memory caches
282
- */
283
- async dispose() {
284
- const disposePromises = Array.from(this.memoryCaches.values()).map(
285
- (cache) => cache.dispose()
286
- );
287
- await Promise.all(disposePromises);
288
- this.memoryCaches.clear();
289
- }
290
- };
291
173
  export {
292
- MemoryCache,
293
- MemoryCacheManager
174
+ MemoryCache
294
175
  };
@@ -1,29 +1,21 @@
1
- import { Cache, type CacheQueryOptions } from "./cache.js";
1
+ import { Cache, type CacheQueryOptions } from "./index.js";
2
2
  /**
3
3
  * Configuration options for PostMessageCache
4
4
  */
5
5
  export interface PostMessageCacheOptions {
6
- /** Maximum number of entries to store */
7
- maxEntries?: number;
8
- /** Maximum age of entries in milliseconds */
9
- maxAge?: number;
6
+ /** Timeout for cache operations in milliseconds (default: 30000) */
7
+ timeout?: number;
10
8
  }
11
9
  /**
12
10
  * Worker-side cache that forwards operations to main thread via postMessage
13
11
  * Only used for MemoryCache in multi-worker environments
14
12
  */
15
13
  export declare class PostMessageCache extends Cache {
16
- private name;
17
- private options;
18
- private requestId;
19
- private pendingRequests;
14
+ #private;
20
15
  constructor(name: string, options?: PostMessageCacheOptions);
21
- private handleResponse;
22
- private sendRequest;
23
16
  match(request: Request, options?: CacheQueryOptions): Promise<Response | undefined>;
24
17
  put(request: Request, response: Response): Promise<void>;
25
18
  delete(request: Request, options?: CacheQueryOptions): Promise<boolean>;
26
- keys(request?: Request, options?: CacheQueryOptions): Promise<Request[]>;
19
+ keys(request?: Request, options?: CacheQueryOptions): Promise<readonly Request[]>;
27
20
  clear(): Promise<void>;
28
- dispose(): Promise<void>;
29
21
  }
@@ -1,60 +1,66 @@
1
1
  /// <reference types="./postmessage.d.ts" />
2
2
  // src/postmessage.ts
3
- import { Cache } from "./cache.js";
4
- var isMainThread = typeof self === "undefined";
5
- var parentPort = typeof self !== "undefined" ? self : null;
3
+ import { Cache } from "./index.js";
4
+ function getParentPort() {
5
+ return typeof self !== "undefined" ? self : null;
6
+ }
7
+ var messageHandlerSetup = false;
8
+ 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
+ function handleCacheResponse(message) {
24
+ const pending = pendingRequestsRegistry.get(message.requestID);
25
+ if (pending) {
26
+ pendingRequestsRegistry.delete(message.requestID);
27
+ if (message.type === "cache:error") {
28
+ pending.reject(new Error(message.error));
29
+ } else {
30
+ pending.resolve(message.result);
31
+ }
32
+ }
33
+ }
34
+ var globalRequestID = 0;
6
35
  var PostMessageCache = class extends Cache {
36
+ #name;
37
+ #timeout;
7
38
  constructor(name, options = {}) {
8
39
  super();
9
- this.name = name;
10
- this.options = options;
11
- if (isMainThread) {
12
- throw new Error(
13
- "PostMessageCache should only be used in worker threads"
14
- );
15
- }
16
- if (parentPort) {
17
- parentPort.on("message", (message) => {
18
- if (message.type === "cache:response" || message.type === "cache:error") {
19
- this.handleResponse(message);
20
- }
21
- });
22
- }
23
- }
24
- requestId = 0;
25
- pendingRequests = /* @__PURE__ */ new Map();
26
- handleResponse(message) {
27
- const pending = this.pendingRequests.get(message.requestId);
28
- if (pending) {
29
- this.pendingRequests.delete(message.requestId);
30
- if (message.type === "cache:error") {
31
- pending.reject(new Error(message.error));
32
- } else {
33
- pending.resolve(message.result);
34
- }
35
- }
40
+ this.#name = name;
41
+ this.#timeout = options.timeout ?? 3e4;
42
+ setupMessageHandler();
36
43
  }
37
- async sendRequest(type, data) {
44
+ async #sendRequest(type, data) {
45
+ const parentPort = getParentPort();
38
46
  if (!parentPort) {
39
- throw new Error(
40
- "PostMessageCache can only be used in worker threads"
41
- );
47
+ throw new Error("PostMessageCache can only be used in worker threads");
42
48
  }
43
- const requestId = ++this.requestId;
49
+ const requestID = ++globalRequestID;
44
50
  return new Promise((resolve, reject) => {
45
- this.pendingRequests.set(requestId, { resolve, reject });
51
+ pendingRequestsRegistry.set(requestID, { resolve, reject });
46
52
  parentPort.postMessage({
47
53
  type,
48
- requestId,
49
- cacheName: this.name,
54
+ requestID,
55
+ cacheName: this.#name,
50
56
  ...data
51
57
  });
52
58
  setTimeout(() => {
53
- if (this.pendingRequests.has(requestId)) {
54
- this.pendingRequests.delete(requestId);
59
+ if (pendingRequestsRegistry.has(requestID)) {
60
+ pendingRequestsRegistry.delete(requestID);
55
61
  reject(new Error("Cache operation timeout"));
56
62
  }
57
- }, 3e4);
63
+ }, this.#timeout);
58
64
  });
59
65
  }
60
66
  async match(request, options) {
@@ -64,7 +70,7 @@ var PostMessageCache = class extends Cache {
64
70
  headers: Object.fromEntries(request.headers),
65
71
  body: request.method !== "GET" && request.method !== "HEAD" ? await request.text() : void 0
66
72
  };
67
- const response = await this.sendRequest("cache:match", {
73
+ const response = await this.#sendRequest("cache:match", {
68
74
  request: serializedRequest,
69
75
  options
70
76
  });
@@ -90,7 +96,7 @@ var PostMessageCache = class extends Cache {
90
96
  headers: Object.fromEntries(response.headers),
91
97
  body: await response.clone().text()
92
98
  };
93
- await this.sendRequest("cache:put", {
99
+ await this.#sendRequest("cache:put", {
94
100
  request: serializedRequest,
95
101
  response: serializedResponse
96
102
  });
@@ -102,7 +108,7 @@ var PostMessageCache = class extends Cache {
102
108
  headers: Object.fromEntries(request.headers),
103
109
  body: request.method !== "GET" && request.method !== "HEAD" ? await request.text() : void 0
104
110
  };
105
- return await this.sendRequest("cache:delete", {
111
+ return await this.#sendRequest("cache:delete", {
106
112
  request: serializedRequest,
107
113
  options
108
114
  });
@@ -117,7 +123,7 @@ var PostMessageCache = class extends Cache {
117
123
  body: request.method !== "GET" && request.method !== "HEAD" ? await request.text() : void 0
118
124
  };
119
125
  }
120
- const keys = await this.sendRequest("cache:keys", {
126
+ const keys = await this.#sendRequest("cache:keys", {
121
127
  request: serializedRequest,
122
128
  options
123
129
  });
@@ -130,11 +136,7 @@ var PostMessageCache = class extends Cache {
130
136
  );
131
137
  }
132
138
  async clear() {
133
- await this.sendRequest("cache:clear", {});
134
- }
135
- async dispose() {
136
- await this.clear();
137
- this.pendingRequests.clear();
139
+ await this.#sendRequest("cache:clear", {});
138
140
  }
139
141
  };
140
142
  export {
@@ -1,44 +0,0 @@
1
- import type { Cache } from "./cache.js";
2
- /**
3
- * Factory function for creating Cache instances based on cache name
4
- */
5
- export type CacheFactory = (name: string) => Cache | Promise<Cache>;
6
- /**
7
- * CustomCacheStorage implements CacheStorage interface with a configurable factory
8
- * The factory function receives the cache name and can return different cache types
9
- */
10
- export declare class CustomCacheStorage {
11
- private factory;
12
- private instances;
13
- constructor(factory: CacheFactory);
14
- /**
15
- * Opens a cache with the given name
16
- * Returns existing instance if already opened, otherwise creates a new one
17
- */
18
- open(name: string): Promise<Cache>;
19
- /**
20
- * Returns true if a cache with the given name exists (has been opened)
21
- */
22
- has(name: string): Promise<boolean>;
23
- /**
24
- * Deletes a cache with the given name
25
- * Disposes of the instance if it exists
26
- */
27
- delete(name: string): Promise<boolean>;
28
- /**
29
- * Returns a list of all opened cache names
30
- */
31
- keys(): Promise<string[]>;
32
- /**
33
- * Get statistics about the cache storage
34
- */
35
- getStats(): {
36
- openInstances: number;
37
- cacheNames: string[];
38
- };
39
- /**
40
- * Dispose of all open cache instances
41
- * Useful for cleanup during shutdown
42
- */
43
- dispose(): Promise<void>;
44
- }
@@ -1,74 +0,0 @@
1
- /// <reference types="./cache-storage.d.ts" />
2
- // src/cache-storage.ts
3
- var CustomCacheStorage = class {
4
- constructor(factory) {
5
- this.factory = factory;
6
- }
7
- instances = /* @__PURE__ */ new Map();
8
- /**
9
- * Opens a cache with the given name
10
- * Returns existing instance if already opened, otherwise creates a new one
11
- */
12
- async open(name) {
13
- const existingInstance = this.instances.get(name);
14
- if (existingInstance) {
15
- return existingInstance;
16
- }
17
- const cache = await this.factory(name);
18
- this.instances.set(name, cache);
19
- return cache;
20
- }
21
- /**
22
- * Returns true if a cache with the given name exists (has been opened)
23
- */
24
- async has(name) {
25
- return this.instances.has(name);
26
- }
27
- /**
28
- * Deletes a cache with the given name
29
- * Disposes of the instance if it exists
30
- */
31
- async delete(name) {
32
- const instance = this.instances.get(name);
33
- if (instance) {
34
- if (instance.dispose) {
35
- await instance.dispose();
36
- }
37
- this.instances.delete(name);
38
- return true;
39
- }
40
- return false;
41
- }
42
- /**
43
- * Returns a list of all opened cache names
44
- */
45
- async keys() {
46
- return Array.from(this.instances.keys());
47
- }
48
- /**
49
- * Get statistics about the cache storage
50
- */
51
- getStats() {
52
- return {
53
- openInstances: this.instances.size,
54
- cacheNames: Array.from(this.instances.keys())
55
- };
56
- }
57
- /**
58
- * Dispose of all open cache instances
59
- * Useful for cleanup during shutdown
60
- */
61
- async dispose() {
62
- const disposePromises = [];
63
- for (const [_name, instance] of this.instances) {
64
- if (instance.dispose) {
65
- disposePromises.push(instance.dispose());
66
- }
67
- }
68
- await Promise.all(disposePromises);
69
- this.instances.clear();
70
- }
71
- };
72
- export {
73
- CustomCacheStorage
74
- };
package/src/cache.d.ts DELETED
@@ -1,66 +0,0 @@
1
- /**
2
- * Cache query options for matching requests
3
- * Based on the Cache API specification
4
- */
5
- export interface CacheQueryOptions {
6
- /** Ignore the search portion of the request URL */
7
- ignoreSearch?: boolean;
8
- /** Ignore the request method */
9
- ignoreMethod?: boolean;
10
- /** Ignore the Vary header */
11
- ignoreVary?: boolean;
12
- /** Custom cache name for scoped operations */
13
- cacheName?: string;
14
- }
15
- /**
16
- * Abstract Cache class implementing the Cache API interface
17
- * Provides shared implementations for add() and addAll() while requiring
18
- * concrete implementations to handle the core storage operations
19
- */
20
- export declare abstract class Cache {
21
- /**
22
- * Returns a Promise that resolves to the response associated with the first matching request
23
- */
24
- abstract match(request: Request, options?: CacheQueryOptions): Promise<Response | undefined>;
25
- /**
26
- * Puts a request/response pair into the cache
27
- */
28
- abstract put(request: Request, response: Response): Promise<void>;
29
- /**
30
- * Finds the cache entry whose key is the request, and if found, deletes it and returns true
31
- */
32
- abstract delete(request: Request, options?: CacheQueryOptions): Promise<boolean>;
33
- /**
34
- * Returns a Promise that resolves to an array of cache keys (Request objects)
35
- */
36
- abstract keys(request?: Request, options?: CacheQueryOptions): Promise<Request[]>;
37
- /**
38
- * Takes a URL, retrieves it and adds the resulting response object to the cache
39
- * Shared implementation using fetch() and put()
40
- */
41
- add(request: Request): Promise<void>;
42
- /**
43
- * Takes an array of URLs, retrieves them, and adds the resulting response objects to the cache
44
- * Shared implementation using add()
45
- */
46
- addAll(requests: Request[]): Promise<void>;
47
- /**
48
- * Returns a Promise that resolves to an array of all matching responses
49
- * Default implementation using keys() and match() - can be overridden for efficiency
50
- */
51
- matchAll(request?: Request, options?: CacheQueryOptions): Promise<Response[]>;
52
- /**
53
- * Optional cleanup method for implementations that need resource disposal
54
- */
55
- dispose?(): Promise<void>;
56
- }
57
- /**
58
- * Generate a cache key from a Request object
59
- * Normalizes the request for consistent cache key generation
60
- */
61
- export declare function generateCacheKey(request: Request, options?: CacheQueryOptions): string;
62
- /**
63
- * Clone a Response object for storage
64
- * Responses can only be consumed once, so we need to clone them for caching
65
- */
66
- export declare function cloneResponse(response: Response): Promise<Response>;
package/src/cache.js DELETED
@@ -1,55 +0,0 @@
1
- /// <reference types="./cache.d.ts" />
2
- // src/cache.ts
3
- var Cache = class {
4
- /**
5
- * Takes a URL, retrieves it and adds the resulting response object to the cache
6
- * Shared implementation using fetch() and put()
7
- */
8
- async add(request) {
9
- const response = await fetch(request);
10
- if (!response.ok) {
11
- throw new TypeError(
12
- `Failed to fetch ${request.url}: ${response.status} ${response.statusText}`
13
- );
14
- }
15
- await this.put(request, response);
16
- }
17
- /**
18
- * Takes an array of URLs, retrieves them, and adds the resulting response objects to the cache
19
- * Shared implementation using add()
20
- */
21
- async addAll(requests) {
22
- await Promise.all(requests.map((request) => this.add(request)));
23
- }
24
- /**
25
- * Returns a Promise that resolves to an array of all matching responses
26
- * Default implementation using keys() and match() - can be overridden for efficiency
27
- */
28
- async matchAll(request, options) {
29
- const keys = await this.keys(request, options);
30
- const responses = [];
31
- for (const key of keys) {
32
- const response = await this.match(key, options);
33
- if (response) {
34
- responses.push(response);
35
- }
36
- }
37
- return responses;
38
- }
39
- };
40
- function generateCacheKey(request, options) {
41
- const url = new URL(request.url);
42
- if (options?.ignoreSearch) {
43
- url.search = "";
44
- }
45
- const method = options?.ignoreMethod ? "GET" : request.method;
46
- return `${method}:${url.href}`;
47
- }
48
- async function cloneResponse(response) {
49
- return response.clone();
50
- }
51
- export {
52
- Cache,
53
- cloneResponse,
54
- generateCacheKey
55
- };