@b9g/cache-redis 0.1.4 → 0.2.0-beta.1

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
@@ -5,7 +5,8 @@ Redis cache adapter for Shovel's universal cache system.
5
5
  ## Features
6
6
 
7
7
  - **HTTP-aware caching**: Stores complete HTTP responses with headers and status codes
8
- - **TTL support**: Configurable time-to-live for cache entries
8
+ - **TTL support**: Respects `Cache-Control: max-age` headers, with configurable default TTL
9
+ - **Vary header support**: Full HTTP content negotiation with Vary header checking
9
10
  - **Size limits**: Configurable maximum entry size to prevent memory issues
10
11
  - **Connection pooling**: Uses the official Redis client with connection management
11
12
  - **Error resilience**: Graceful handling of Redis connection issues
@@ -44,11 +45,10 @@ const cached = await cache.match(request);
44
45
  ### Direct Cache Usage
45
46
 
46
47
  ```typescript
47
- import {createCache} from "@b9g/cache-redis";
48
+ import {RedisCache} from "@b9g/cache-redis";
48
49
 
49
50
  // Create a single Redis cache instance
50
- const cache = createCache({
51
- name: "my-cache",
51
+ const cache = new RedisCache("my-cache", {
52
52
  redis: {
53
53
  url: process.env.REDIS_URL || "redis://localhost:6379",
54
54
  password: process.env.REDIS_PASSWORD
@@ -157,23 +157,6 @@ The Redis cache gracefully handles connection issues:
157
157
  - Connection errors are logged but don't crash the application
158
158
  - Automatic reconnection when Redis becomes available
159
159
 
160
- ## Cache Statistics
161
-
162
- Get insights into cache performance:
163
-
164
- ```typescript
165
- const cache = new RedisCache("my-cache");
166
- const stats = await cache.getStats();
167
-
168
- console.log({
169
- connected: stats.connected,
170
- keyCount: stats.keyCount,
171
- totalSize: stats.totalSize,
172
- prefix: stats.prefix
173
- });
174
- ```
175
-
176
-
177
160
  ## Exports
178
161
 
179
162
  ### Classes
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "@b9g/cache-redis",
3
- "version": "0.1.4",
4
- "description": "Redis cache adapter for Shovel cache system",
3
+ "version": "0.2.0-beta.1",
4
+ "description": "Redis cache implementation for Shovel cache system",
5
5
  "keywords": [
6
6
  "cache",
7
7
  "redis",
8
- "shovel",
9
- "metaframework",
10
- "cache-adapter"
8
+ "service-workers",
9
+ "shovel"
11
10
  ],
12
11
  "author": "Shovel Team",
13
12
  "license": "MIT",
@@ -24,7 +23,7 @@
24
23
  "@b9g/libuild": "^0.1.18"
25
24
  },
26
25
  "peerDependencies": {
27
- "@b9g/cache": "^0.1.4"
26
+ "@b9g/cache": "^0.2.0-beta.0"
28
27
  },
29
28
  "type": "module",
30
29
  "types": "src/index.d.ts",
@@ -32,23 +31,19 @@
32
31
  "README.md",
33
32
  "src/"
34
33
  ],
35
- "main": "src/index.cjs",
36
34
  "module": "src/index.js",
37
35
  "exports": {
38
36
  ".": {
39
37
  "types": "./src/index.d.ts",
40
- "import": "./src/index.js",
41
- "require": "./src/index.cjs"
38
+ "import": "./src/index.js"
42
39
  },
43
40
  "./index": {
44
41
  "types": "./src/index.d.ts",
45
- "import": "./src/index.js",
46
- "require": "./src/index.cjs"
42
+ "import": "./src/index.js"
47
43
  },
48
44
  "./index.js": {
49
45
  "types": "./src/index.d.ts",
50
- "import": "./src/index.js",
51
- "require": "./src/index.cjs"
46
+ "import": "./src/index.js"
52
47
  },
53
48
  "./package.json": "./package.json"
54
49
  }
package/src/index.d.ts CHANGED
@@ -38,17 +38,6 @@ export declare class RedisCache extends Cache {
38
38
  * Returns a Promise that resolves to an array of cache keys (Request objects)
39
39
  */
40
40
  keys(request?: Request, options?: CacheQueryOptions): Promise<Request[]>;
41
- /**
42
- * Get cache statistics
43
- */
44
- getStats(): Promise<{
45
- connected: boolean;
46
- keyCount: number;
47
- totalSize: number;
48
- prefix: string;
49
- defaultTTL: number;
50
- maxEntrySize: number;
51
- }>;
52
41
  /**
53
42
  * Dispose of Redis client connection
54
43
  * Call this during graceful shutdown to properly close Redis connections
package/src/index.js CHANGED
@@ -3,7 +3,22 @@
3
3
  import { Cache, generateCacheKey } from "@b9g/cache";
4
4
  import { createClient } from "redis";
5
5
  import { getLogger } from "@logtape/logtape";
6
- var logger = getLogger(["cache-redis"]);
6
+ var logger = getLogger(["shovel", "cache"]);
7
+ function uint8ArrayToBase64(bytes) {
8
+ let binary = "";
9
+ for (let i = 0; i < bytes.length; i++) {
10
+ binary += String.fromCharCode(bytes[i]);
11
+ }
12
+ return btoa(binary);
13
+ }
14
+ function base64ToUint8Array(base64) {
15
+ const binary = atob(base64);
16
+ const bytes = new Uint8Array(binary.length);
17
+ for (let i = 0; i < binary.length; i++) {
18
+ bytes[i] = binary.charCodeAt(i);
19
+ }
20
+ return bytes;
21
+ }
7
22
  var RedisCache = class extends Cache {
8
23
  #client;
9
24
  #prefix;
@@ -33,7 +48,7 @@ var RedisCache = class extends Cache {
33
48
  * Ensure Redis client is connected
34
49
  */
35
50
  async #ensureConnected() {
36
- if (!this.#connected && !this.#client.isReady) {
51
+ if (!this.#client.isOpen && !this.#client.isReady) {
37
52
  await this.#client.connect();
38
53
  }
39
54
  }
@@ -47,7 +62,7 @@ var RedisCache = class extends Cache {
47
62
  /**
48
63
  * Serialize Response to cache entry
49
64
  */
50
- async #serializeResponse(response) {
65
+ async #serializeResponse(request, response) {
51
66
  const cloned = response.clone();
52
67
  const body = await cloned.arrayBuffer();
53
68
  if (body.byteLength > this.#maxEntrySize) {
@@ -59,26 +74,61 @@ var RedisCache = class extends Cache {
59
74
  response.headers.forEach((value, key) => {
60
75
  headers[key] = value;
61
76
  });
77
+ const requestHeaders = {};
78
+ request.headers.forEach((value, key) => {
79
+ requestHeaders[key] = value;
80
+ });
81
+ let ttl = this.#defaultTTL;
82
+ const cacheControl = response.headers.get("cache-control");
83
+ if (cacheControl) {
84
+ const maxAgeMatch = cacheControl.match(/max-age=(\d+)/);
85
+ if (maxAgeMatch) {
86
+ ttl = parseInt(maxAgeMatch[1], 10);
87
+ }
88
+ }
62
89
  return {
63
90
  status: response.status,
64
91
  statusText: response.statusText,
65
92
  headers,
66
- body: btoa(String.fromCharCode(...new Uint8Array(body))),
93
+ body: uint8ArrayToBase64(new Uint8Array(body)),
67
94
  cachedAt: Date.now(),
68
- TTL: this.#defaultTTL
95
+ TTL: ttl,
96
+ requestHeaders
69
97
  };
70
98
  }
71
99
  /**
72
100
  * Deserialize cache entry to Response
73
101
  */
74
102
  #deserializeResponse(entry) {
75
- const body = Uint8Array.from(atob(entry.body), (c) => c.charCodeAt(0));
103
+ const body = base64ToUint8Array(entry.body);
76
104
  return new Response(body, {
77
105
  status: entry.status,
78
106
  statusText: entry.statusText,
79
107
  headers: entry.headers
80
108
  });
81
109
  }
110
+ /**
111
+ * Check if a request matches the Vary header of a cached entry
112
+ * Returns true if the request matches or if there's no Vary header
113
+ */
114
+ #matchesVary(request, entry) {
115
+ const varyHeader = entry.headers["vary"] || entry.headers["Vary"];
116
+ if (!varyHeader) {
117
+ return true;
118
+ }
119
+ if (varyHeader === "*") {
120
+ return false;
121
+ }
122
+ const varyHeaders = varyHeader.split(",").map((h) => h.trim().toLowerCase());
123
+ for (const headerName of varyHeaders) {
124
+ const requestValue = request.headers.get(headerName);
125
+ const cachedValue = entry.requestHeaders[headerName] || null;
126
+ if (requestValue !== cachedValue) {
127
+ return false;
128
+ }
129
+ }
130
+ return true;
131
+ }
82
132
  /**
83
133
  * Returns a Promise that resolves to the response associated with the first matching request
84
134
  */
@@ -98,6 +148,9 @@ var RedisCache = class extends Cache {
98
148
  return void 0;
99
149
  }
100
150
  }
151
+ if (!options?.ignoreVary && !this.#matchesVary(request, entry)) {
152
+ return void 0;
153
+ }
101
154
  return this.#deserializeResponse(entry);
102
155
  } catch (error) {
103
156
  logger.error("Failed to match: {error}", { error });
@@ -111,7 +164,7 @@ var RedisCache = class extends Cache {
111
164
  try {
112
165
  await this.#ensureConnected();
113
166
  const key = this.#getRedisKey(request);
114
- const entry = await this.#serializeResponse(response);
167
+ const entry = await this.#serializeResponse(request, response);
115
168
  const serialized = JSON.stringify(entry);
116
169
  if (entry.TTL > 0) {
117
170
  await this.#client.setEx(key, entry.TTL, serialized);
@@ -160,11 +213,15 @@ var RedisCache = class extends Cache {
160
213
  for (const key of keys) {
161
214
  try {
162
215
  const cacheKey = key.replace(`${this.#prefix}:`, "");
163
- const [method, url] = cacheKey.split(":", 2);
216
+ const colonIndex = cacheKey.indexOf(":");
217
+ if (colonIndex === -1) continue;
218
+ const method = cacheKey.substring(0, colonIndex);
219
+ const url = cacheKey.substring(colonIndex + 1);
164
220
  if (method && url) {
165
221
  requests.push(new Request(url, { method }));
166
222
  }
167
- } catch {
223
+ } catch (err) {
224
+ if (!(err instanceof TypeError)) throw err;
168
225
  }
169
226
  }
170
227
  return requests;
@@ -173,48 +230,6 @@ var RedisCache = class extends Cache {
173
230
  return [];
174
231
  }
175
232
  }
176
- /**
177
- * Get cache statistics
178
- */
179
- async getStats() {
180
- try {
181
- await this.#ensureConnected();
182
- const pattern = `${this.#prefix}:*`;
183
- let keyCount = 0;
184
- let totalSize = 0;
185
- for await (const key of this.#client.scanIterator({
186
- MATCH: pattern,
187
- COUNT: 100
188
- })) {
189
- keyCount++;
190
- try {
191
- const value = await this.#client.get(key);
192
- if (value) {
193
- totalSize += new TextEncoder().encode(value).length;
194
- }
195
- } catch {
196
- }
197
- }
198
- return {
199
- connected: this.#connected,
200
- keyCount,
201
- totalSize,
202
- prefix: this.#prefix,
203
- defaultTTL: this.#defaultTTL,
204
- maxEntrySize: this.#maxEntrySize
205
- };
206
- } catch (error) {
207
- logger.error("Failed to get stats: {error}", { error });
208
- return {
209
- connected: false,
210
- keyCount: 0,
211
- totalSize: 0,
212
- prefix: this.#prefix,
213
- defaultTTL: this.#defaultTTL,
214
- maxEntrySize: this.#maxEntrySize
215
- };
216
- }
217
- }
218
233
  /**
219
234
  * Dispose of Redis client connection
220
235
  * Call this during graceful shutdown to properly close Redis connections
@@ -229,7 +244,9 @@ var RedisCache = class extends Cache {
229
244
  try {
230
245
  await this.#client.disconnect();
231
246
  } catch (disconnectError) {
232
- logger.error("Error forcing Redis disconnect: {error}", { error: disconnectError });
247
+ logger.error("Error forcing Redis disconnect: {error}", {
248
+ error: disconnectError
249
+ });
233
250
  }
234
251
  }
235
252
  }