@b9g/cache-redis 0.1.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 +195 -0
- package/package.json +74 -0
- package/src/factory.cjs +34 -0
- package/src/factory.d.ts +19 -0
- package/src/factory.js +11 -0
- package/src/index.cjs +32 -0
- package/src/index.d.ts +7 -0
- package/src/index.js +8 -0
- package/src/redis-cache.cjs +18208 -0
- package/src/redis-cache.d.ts +71 -0
- package/src/redis-cache.js +228 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Cache, type CacheQueryOptions } from "@b9g/cache";
|
|
2
|
+
import { type RedisClientOptions } from "redis";
|
|
3
|
+
export interface RedisCacheOptions {
|
|
4
|
+
/** Redis connection options */
|
|
5
|
+
redis?: RedisClientOptions;
|
|
6
|
+
/** Cache name prefix for Redis keys */
|
|
7
|
+
prefix?: string;
|
|
8
|
+
/** Default TTL in seconds (0 = no expiration) */
|
|
9
|
+
defaultTTL?: number;
|
|
10
|
+
/** Maximum cache entry size in bytes */
|
|
11
|
+
maxEntrySize?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Redis-backed cache implementation
|
|
15
|
+
* Stores HTTP responses with proper serialization and TTL support
|
|
16
|
+
*/
|
|
17
|
+
export declare class RedisCache extends Cache {
|
|
18
|
+
private client;
|
|
19
|
+
private prefix;
|
|
20
|
+
private defaultTTL;
|
|
21
|
+
private maxEntrySize;
|
|
22
|
+
private connected;
|
|
23
|
+
constructor(name: string, options?: RedisCacheOptions);
|
|
24
|
+
/**
|
|
25
|
+
* Ensure Redis client is connected
|
|
26
|
+
*/
|
|
27
|
+
private ensureConnected;
|
|
28
|
+
/**
|
|
29
|
+
* Generate Redis key for cache entry
|
|
30
|
+
*/
|
|
31
|
+
private getRedisKey;
|
|
32
|
+
/**
|
|
33
|
+
* Serialize Response to cache entry
|
|
34
|
+
*/
|
|
35
|
+
private serializeResponse;
|
|
36
|
+
/**
|
|
37
|
+
* Deserialize cache entry to Response
|
|
38
|
+
*/
|
|
39
|
+
private deserializeResponse;
|
|
40
|
+
/**
|
|
41
|
+
* Returns a Promise that resolves to the response associated with the first matching request
|
|
42
|
+
*/
|
|
43
|
+
match(request: Request, options?: CacheQueryOptions): Promise<Response | undefined>;
|
|
44
|
+
/**
|
|
45
|
+
* Puts a request/response pair into the cache
|
|
46
|
+
*/
|
|
47
|
+
put(request: Request, response: Response): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Finds the cache entry whose key is the request, and if found, deletes it and returns true
|
|
50
|
+
*/
|
|
51
|
+
delete(request: Request, options?: CacheQueryOptions): Promise<boolean>;
|
|
52
|
+
/**
|
|
53
|
+
* Returns a Promise that resolves to an array of cache keys (Request objects)
|
|
54
|
+
*/
|
|
55
|
+
keys(request?: Request, options?: CacheQueryOptions): Promise<Request[]>;
|
|
56
|
+
/**
|
|
57
|
+
* Get cache statistics
|
|
58
|
+
*/
|
|
59
|
+
getStats(): Promise<{
|
|
60
|
+
connected: boolean;
|
|
61
|
+
keyCount: number;
|
|
62
|
+
totalSize: number;
|
|
63
|
+
prefix: string;
|
|
64
|
+
defaultTTL: number;
|
|
65
|
+
maxEntrySize: number;
|
|
66
|
+
}>;
|
|
67
|
+
/**
|
|
68
|
+
* Cleanup method for implementations that need resource disposal
|
|
69
|
+
*/
|
|
70
|
+
dispose(): Promise<void>;
|
|
71
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/// <reference types="./redis-cache.d.ts" />
|
|
2
|
+
// src/redis-cache.ts
|
|
3
|
+
import { Cache, generateCacheKey } from "@b9g/cache";
|
|
4
|
+
import { createClient } from "redis";
|
|
5
|
+
var RedisCache = class extends Cache {
|
|
6
|
+
client;
|
|
7
|
+
prefix;
|
|
8
|
+
defaultTTL;
|
|
9
|
+
maxEntrySize;
|
|
10
|
+
connected = false;
|
|
11
|
+
constructor(name, options = {}) {
|
|
12
|
+
super();
|
|
13
|
+
this.client = createClient(options.redis || {});
|
|
14
|
+
this.prefix = options.prefix ? `${options.prefix}:${name}` : `cache:${name}`;
|
|
15
|
+
this.defaultTTL = options.defaultTTL || 0;
|
|
16
|
+
this.maxEntrySize = options.maxEntrySize || 10 * 1024 * 1024;
|
|
17
|
+
this.client.on("error", (err) => {
|
|
18
|
+
console.error("[RedisCache] Redis error:", err);
|
|
19
|
+
});
|
|
20
|
+
this.client.on("connect", () => {
|
|
21
|
+
console.info(`[RedisCache] Connected to Redis for cache: ${name}`);
|
|
22
|
+
this.connected = true;
|
|
23
|
+
});
|
|
24
|
+
this.client.on("disconnect", () => {
|
|
25
|
+
console.warn(`[RedisCache] Disconnected from Redis for cache: ${name}`);
|
|
26
|
+
this.connected = false;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Ensure Redis client is connected
|
|
31
|
+
*/
|
|
32
|
+
async ensureConnected() {
|
|
33
|
+
if (!this.connected && !this.client.isReady) {
|
|
34
|
+
await this.client.connect();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Generate Redis key for cache entry
|
|
39
|
+
*/
|
|
40
|
+
getRedisKey(request, options) {
|
|
41
|
+
const cacheKey = generateCacheKey(request, options);
|
|
42
|
+
return `${this.prefix}:${cacheKey}`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Serialize Response to cache entry
|
|
46
|
+
*/
|
|
47
|
+
async serializeResponse(response) {
|
|
48
|
+
const cloned = response.clone();
|
|
49
|
+
const body = await cloned.arrayBuffer();
|
|
50
|
+
if (body.byteLength > this.maxEntrySize) {
|
|
51
|
+
throw new Error(`Response body too large: ${body.byteLength} bytes (max: ${this.maxEntrySize})`);
|
|
52
|
+
}
|
|
53
|
+
const headers = {};
|
|
54
|
+
response.headers.forEach((value, key) => {
|
|
55
|
+
headers[key] = value;
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
status: response.status,
|
|
59
|
+
statusText: response.statusText,
|
|
60
|
+
headers,
|
|
61
|
+
body: Buffer.from(body).toString("base64"),
|
|
62
|
+
cachedAt: Date.now(),
|
|
63
|
+
ttl: this.defaultTTL
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Deserialize cache entry to Response
|
|
68
|
+
*/
|
|
69
|
+
deserializeResponse(entry) {
|
|
70
|
+
const body = Buffer.from(entry.body, "base64");
|
|
71
|
+
return new Response(body, {
|
|
72
|
+
status: entry.status,
|
|
73
|
+
statusText: entry.statusText,
|
|
74
|
+
headers: entry.headers
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Returns a Promise that resolves to the response associated with the first matching request
|
|
79
|
+
*/
|
|
80
|
+
async match(request, options) {
|
|
81
|
+
try {
|
|
82
|
+
await this.ensureConnected();
|
|
83
|
+
const key = this.getRedisKey(request, options);
|
|
84
|
+
const cached = await this.client.get(key);
|
|
85
|
+
if (!cached) {
|
|
86
|
+
return void 0;
|
|
87
|
+
}
|
|
88
|
+
const entry = JSON.parse(cached);
|
|
89
|
+
if (entry.ttl > 0) {
|
|
90
|
+
const ageInSeconds = (Date.now() - entry.cachedAt) / 1e3;
|
|
91
|
+
if (ageInSeconds > entry.ttl) {
|
|
92
|
+
await this.client.del(key);
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return this.deserializeResponse(entry);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error("[RedisCache] Failed to match:", error);
|
|
99
|
+
return void 0;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Puts a request/response pair into the cache
|
|
104
|
+
*/
|
|
105
|
+
async put(request, response) {
|
|
106
|
+
try {
|
|
107
|
+
await this.ensureConnected();
|
|
108
|
+
const key = this.getRedisKey(request);
|
|
109
|
+
const entry = await this.serializeResponse(response);
|
|
110
|
+
const serialized = JSON.stringify(entry);
|
|
111
|
+
if (entry.ttl > 0) {
|
|
112
|
+
await this.client.setEx(key, entry.ttl, serialized);
|
|
113
|
+
} else {
|
|
114
|
+
await this.client.set(key, serialized);
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error("[RedisCache] Failed to put:", error);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Finds the cache entry whose key is the request, and if found, deletes it and returns true
|
|
123
|
+
*/
|
|
124
|
+
async delete(request, options) {
|
|
125
|
+
try {
|
|
126
|
+
await this.ensureConnected();
|
|
127
|
+
const key = this.getRedisKey(request, options);
|
|
128
|
+
const result = await this.client.del(key);
|
|
129
|
+
return result > 0;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error("[RedisCache] Failed to delete:", error);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Returns a Promise that resolves to an array of cache keys (Request objects)
|
|
137
|
+
*/
|
|
138
|
+
async keys(request, options) {
|
|
139
|
+
try {
|
|
140
|
+
await this.ensureConnected();
|
|
141
|
+
if (request) {
|
|
142
|
+
const key = this.getRedisKey(request, options);
|
|
143
|
+
const exists = await this.client.exists(key);
|
|
144
|
+
return exists ? [request] : [];
|
|
145
|
+
}
|
|
146
|
+
const pattern = `${this.prefix}:*`;
|
|
147
|
+
const keys = [];
|
|
148
|
+
for await (const key of this.client.scanIterator({
|
|
149
|
+
MATCH: pattern,
|
|
150
|
+
COUNT: 100
|
|
151
|
+
})) {
|
|
152
|
+
keys.push(key);
|
|
153
|
+
}
|
|
154
|
+
const requests = [];
|
|
155
|
+
for (const key of keys) {
|
|
156
|
+
try {
|
|
157
|
+
const cacheKey = key.replace(`${this.prefix}:`, "");
|
|
158
|
+
const [method, url] = cacheKey.split(":", 2);
|
|
159
|
+
if (method && url) {
|
|
160
|
+
requests.push(new Request(url, { method }));
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return requests;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error("[RedisCache] Failed to get keys:", error);
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Get cache statistics
|
|
173
|
+
*/
|
|
174
|
+
async getStats() {
|
|
175
|
+
try {
|
|
176
|
+
await this.ensureConnected();
|
|
177
|
+
const pattern = `${this.prefix}:*`;
|
|
178
|
+
let keyCount = 0;
|
|
179
|
+
let totalSize = 0;
|
|
180
|
+
for await (const key of this.client.scanIterator({
|
|
181
|
+
MATCH: pattern,
|
|
182
|
+
COUNT: 100
|
|
183
|
+
})) {
|
|
184
|
+
keyCount++;
|
|
185
|
+
try {
|
|
186
|
+
const value = await this.client.get(key);
|
|
187
|
+
if (value) {
|
|
188
|
+
totalSize += Buffer.byteLength(value, "utf8");
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
connected: this.connected,
|
|
195
|
+
keyCount,
|
|
196
|
+
totalSize,
|
|
197
|
+
prefix: this.prefix,
|
|
198
|
+
defaultTTL: this.defaultTTL,
|
|
199
|
+
maxEntrySize: this.maxEntrySize
|
|
200
|
+
};
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error("[RedisCache] Failed to get stats:", error);
|
|
203
|
+
return {
|
|
204
|
+
connected: false,
|
|
205
|
+
keyCount: 0,
|
|
206
|
+
totalSize: 0,
|
|
207
|
+
prefix: this.prefix,
|
|
208
|
+
defaultTTL: this.defaultTTL,
|
|
209
|
+
maxEntrySize: this.maxEntrySize
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Cleanup method for implementations that need resource disposal
|
|
215
|
+
*/
|
|
216
|
+
async dispose() {
|
|
217
|
+
try {
|
|
218
|
+
if (this.client.isReady) {
|
|
219
|
+
await this.client.disconnect();
|
|
220
|
+
}
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error("[RedisCache] Failed to dispose:", error);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
export {
|
|
227
|
+
RedisCache
|
|
228
|
+
};
|