@b9g/cache-redis 0.1.5 → 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.5",
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.5"
26
+ "@b9g/cache": "^0.2.0-beta.0"
28
27
  },
29
28
  "type": "module",
30
29
  "types": "src/index.d.ts",
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,7 @@
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(["platform"]);
6
+ var logger = getLogger(["shovel", "cache"]);
7
7
  function uint8ArrayToBase64(bytes) {
8
8
  let binary = "";
9
9
  for (let i = 0; i < bytes.length; i++) {
@@ -48,7 +48,7 @@ var RedisCache = class extends Cache {
48
48
  * Ensure Redis client is connected
49
49
  */
50
50
  async #ensureConnected() {
51
- if (!this.#connected && !this.#client.isReady) {
51
+ if (!this.#client.isOpen && !this.#client.isReady) {
52
52
  await this.#client.connect();
53
53
  }
54
54
  }
@@ -62,7 +62,7 @@ var RedisCache = class extends Cache {
62
62
  /**
63
63
  * Serialize Response to cache entry
64
64
  */
65
- async #serializeResponse(response) {
65
+ async #serializeResponse(request, response) {
66
66
  const cloned = response.clone();
67
67
  const body = await cloned.arrayBuffer();
68
68
  if (body.byteLength > this.#maxEntrySize) {
@@ -74,13 +74,26 @@ var RedisCache = class extends Cache {
74
74
  response.headers.forEach((value, key) => {
75
75
  headers[key] = value;
76
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
+ }
77
89
  return {
78
90
  status: response.status,
79
91
  statusText: response.statusText,
80
92
  headers,
81
93
  body: uint8ArrayToBase64(new Uint8Array(body)),
82
94
  cachedAt: Date.now(),
83
- TTL: this.#defaultTTL
95
+ TTL: ttl,
96
+ requestHeaders
84
97
  };
85
98
  }
86
99
  /**
@@ -94,6 +107,28 @@ var RedisCache = class extends Cache {
94
107
  headers: entry.headers
95
108
  });
96
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
+ }
97
132
  /**
98
133
  * Returns a Promise that resolves to the response associated with the first matching request
99
134
  */
@@ -113,6 +148,9 @@ var RedisCache = class extends Cache {
113
148
  return void 0;
114
149
  }
115
150
  }
151
+ if (!options?.ignoreVary && !this.#matchesVary(request, entry)) {
152
+ return void 0;
153
+ }
116
154
  return this.#deserializeResponse(entry);
117
155
  } catch (error) {
118
156
  logger.error("Failed to match: {error}", { error });
@@ -126,7 +164,7 @@ var RedisCache = class extends Cache {
126
164
  try {
127
165
  await this.#ensureConnected();
128
166
  const key = this.#getRedisKey(request);
129
- const entry = await this.#serializeResponse(response);
167
+ const entry = await this.#serializeResponse(request, response);
130
168
  const serialized = JSON.stringify(entry);
131
169
  if (entry.TTL > 0) {
132
170
  await this.#client.setEx(key, entry.TTL, serialized);
@@ -175,13 +213,15 @@ var RedisCache = class extends Cache {
175
213
  for (const key of keys) {
176
214
  try {
177
215
  const cacheKey = key.replace(`${this.#prefix}:`, "");
178
- 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);
179
220
  if (method && url) {
180
221
  requests.push(new Request(url, { method }));
181
222
  }
182
223
  } catch (err) {
183
- if (!(err instanceof TypeError))
184
- throw err;
224
+ if (!(err instanceof TypeError)) throw err;
185
225
  }
186
226
  }
187
227
  return requests;
@@ -190,49 +230,6 @@ var RedisCache = class extends Cache {
190
230
  return [];
191
231
  }
192
232
  }
193
- /**
194
- * Get cache statistics
195
- */
196
- async getStats() {
197
- try {
198
- await this.#ensureConnected();
199
- const pattern = `${this.#prefix}:*`;
200
- let keyCount = 0;
201
- let totalSize = 0;
202
- for await (const key of this.#client.scanIterator({
203
- MATCH: pattern,
204
- COUNT: 100
205
- })) {
206
- keyCount++;
207
- try {
208
- const value = await this.#client.get(key);
209
- if (value) {
210
- totalSize += new TextEncoder().encode(value).length;
211
- }
212
- } catch (err) {
213
- logger.debug("Error reading key {key}: {error}", { key, error: err });
214
- }
215
- }
216
- return {
217
- connected: this.#connected,
218
- keyCount,
219
- totalSize,
220
- prefix: this.#prefix,
221
- defaultTTL: this.#defaultTTL,
222
- maxEntrySize: this.#maxEntrySize
223
- };
224
- } catch (error) {
225
- logger.error("Failed to get stats: {error}", { error });
226
- return {
227
- connected: false,
228
- keyCount: 0,
229
- totalSize: 0,
230
- prefix: this.#prefix,
231
- defaultTTL: this.#defaultTTL,
232
- maxEntrySize: this.#maxEntrySize
233
- };
234
- }
235
- }
236
233
  /**
237
234
  * Dispose of Redis client connection
238
235
  * Call this during graceful shutdown to properly close Redis connections