@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.
@@ -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
+ };