@b9g/cache-redis 0.1.5 → 0.2.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 +4 -21
- package/package.json +5 -6
- package/src/index.d.ts +0 -11
- package/src/index.js +48 -51
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**:
|
|
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 {
|
|
48
|
+
import {RedisCache} from "@b9g/cache-redis";
|
|
48
49
|
|
|
49
50
|
// Create a single Redis cache instance
|
|
50
|
-
const cache =
|
|
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.
|
|
4
|
-
"description": "Redis cache
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Redis cache implementation for Shovel cache system",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cache",
|
|
7
7
|
"redis",
|
|
8
|
-
"
|
|
9
|
-
"
|
|
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.
|
|
26
|
+
"@b9g/cache": "^0.2.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(["
|
|
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.#
|
|
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:
|
|
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
|
|
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
|