@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 +4 -21
- package/package.json +8 -13
- package/src/index.d.ts +0 -11
- package/src/index.js +69 -52
- package/src/index.cjs +0 -18812
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.1
|
|
4
|
-
"description": "Redis cache
|
|
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
|
-
"
|
|
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-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
|
|
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.#
|
|
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:
|
|
93
|
+
body: uint8ArrayToBase64(new Uint8Array(body)),
|
|
67
94
|
cachedAt: Date.now(),
|
|
68
|
-
TTL:
|
|
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 =
|
|
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
|
|
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}", {
|
|
247
|
+
logger.error("Error forcing Redis disconnect: {error}", {
|
|
248
|
+
error: disconnectError
|
|
249
|
+
});
|
|
233
250
|
}
|
|
234
251
|
}
|
|
235
252
|
}
|