@b9g/cache-redis 0.1.2 → 0.1.3
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 +32 -32
- package/package.json +2 -22
- package/src/index.cjs +18792 -17
- package/src/index.d.ts +50 -8
- package/src/index.js +238 -10
- package/src/factory.cjs +0 -34
- package/src/factory.d.ts +0 -19
- package/src/factory.js +0 -11
- package/src/redis-cache.cjs +0 -18208
- package/src/redis-cache.d.ts +0 -71
- package/src/redis-cache.js +0 -228
package/src/index.d.ts
CHANGED
|
@@ -3,13 +3,55 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides Redis-backed caching with HTTP-aware storage and retrieval
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import { Cache, type CacheQueryOptions } from "@b9g/cache";
|
|
7
|
+
import { type RedisClientOptions } from "redis";
|
|
8
|
+
export interface RedisCacheOptions {
|
|
9
|
+
/** Redis connection options */
|
|
10
|
+
redis?: RedisClientOptions;
|
|
11
|
+
/** Cache name prefix for Redis keys */
|
|
12
|
+
prefix?: string;
|
|
13
|
+
/** Default TTL in seconds (0 = no expiration) */
|
|
14
|
+
defaultTTL?: number;
|
|
15
|
+
/** Maximum cache entry size in bytes */
|
|
16
|
+
maxEntrySize?: number;
|
|
17
|
+
}
|
|
9
18
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
19
|
+
* Redis-backed cache implementation
|
|
20
|
+
* Stores HTTP responses with proper serialization and TTL support
|
|
12
21
|
*/
|
|
13
|
-
export declare
|
|
14
|
-
|
|
15
|
-
|
|
22
|
+
export declare class RedisCache extends Cache {
|
|
23
|
+
#private;
|
|
24
|
+
constructor(name: string, options?: RedisCacheOptions);
|
|
25
|
+
/**
|
|
26
|
+
* Returns a Promise that resolves to the response associated with the first matching request
|
|
27
|
+
*/
|
|
28
|
+
match(request: Request, options?: CacheQueryOptions): Promise<Response | undefined>;
|
|
29
|
+
/**
|
|
30
|
+
* Puts a request/response pair into the cache
|
|
31
|
+
*/
|
|
32
|
+
put(request: Request, response: Response): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Finds the cache entry whose key is the request, and if found, deletes it and returns true
|
|
35
|
+
*/
|
|
36
|
+
delete(request: Request, options?: CacheQueryOptions): Promise<boolean>;
|
|
37
|
+
/**
|
|
38
|
+
* Returns a Promise that resolves to an array of cache keys (Request objects)
|
|
39
|
+
*/
|
|
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
|
+
/**
|
|
53
|
+
* Dispose of Redis client connection
|
|
54
|
+
* Call this during graceful shutdown to properly close Redis connections
|
|
55
|
+
*/
|
|
56
|
+
dispose(): Promise<void>;
|
|
57
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,14 +1,242 @@
|
|
|
1
1
|
/// <reference types="./index.d.ts" />
|
|
2
2
|
// src/index.ts
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
import { Cache, generateCacheKey } from "@b9g/cache";
|
|
4
|
+
import { createClient } from "redis";
|
|
5
|
+
import { getLogger } from "@logtape/logtape";
|
|
6
|
+
var logger = getLogger(["cache-redis"]);
|
|
7
|
+
var RedisCache = class extends Cache {
|
|
8
|
+
#client;
|
|
9
|
+
#prefix;
|
|
10
|
+
#defaultTTL;
|
|
11
|
+
#maxEntrySize;
|
|
12
|
+
#connected;
|
|
13
|
+
constructor(name, options = {}) {
|
|
14
|
+
super();
|
|
15
|
+
this.#client = createClient(options.redis || {});
|
|
16
|
+
this.#prefix = options.prefix ? `${options.prefix}:${name}` : `cache:${name}`;
|
|
17
|
+
this.#defaultTTL = options.defaultTTL || 0;
|
|
18
|
+
this.#maxEntrySize = options.maxEntrySize || 10 * 1024 * 1024;
|
|
19
|
+
this.#connected = false;
|
|
20
|
+
this.#client.on("error", (err) => {
|
|
21
|
+
logger.error("Redis error", { error: err });
|
|
22
|
+
});
|
|
23
|
+
this.#client.on("connect", () => {
|
|
24
|
+
logger.info("Connected to Redis", { cache: name });
|
|
25
|
+
this.#connected = true;
|
|
26
|
+
});
|
|
27
|
+
this.#client.on("disconnect", () => {
|
|
28
|
+
logger.warn("Disconnected from Redis", { cache: name });
|
|
29
|
+
this.#connected = false;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Ensure Redis client is connected
|
|
34
|
+
*/
|
|
35
|
+
async #ensureConnected() {
|
|
36
|
+
if (!this.#connected && !this.#client.isReady) {
|
|
37
|
+
await this.#client.connect();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Generate Redis key for cache entry
|
|
42
|
+
*/
|
|
43
|
+
#getRedisKey(request, options) {
|
|
44
|
+
const cacheKey = generateCacheKey(request, options);
|
|
45
|
+
return `${this.#prefix}:${cacheKey}`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Serialize Response to cache entry
|
|
49
|
+
*/
|
|
50
|
+
async #serializeResponse(response) {
|
|
51
|
+
const cloned = response.clone();
|
|
52
|
+
const body = await cloned.arrayBuffer();
|
|
53
|
+
if (body.byteLength > this.#maxEntrySize) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Response body too large: ${body.byteLength} bytes (max: ${this.#maxEntrySize})`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const headers = {};
|
|
59
|
+
response.headers.forEach((value, key) => {
|
|
60
|
+
headers[key] = value;
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
status: response.status,
|
|
64
|
+
statusText: response.statusText,
|
|
65
|
+
headers,
|
|
66
|
+
body: btoa(String.fromCharCode(...new Uint8Array(body))),
|
|
67
|
+
cachedAt: Date.now(),
|
|
68
|
+
TTL: this.#defaultTTL
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Deserialize cache entry to Response
|
|
73
|
+
*/
|
|
74
|
+
#deserializeResponse(entry) {
|
|
75
|
+
const body = Uint8Array.from(atob(entry.body), (c) => c.charCodeAt(0));
|
|
76
|
+
return new Response(body, {
|
|
77
|
+
status: entry.status,
|
|
78
|
+
statusText: entry.statusText,
|
|
79
|
+
headers: entry.headers
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Returns a Promise that resolves to the response associated with the first matching request
|
|
84
|
+
*/
|
|
85
|
+
async match(request, options) {
|
|
86
|
+
try {
|
|
87
|
+
await this.#ensureConnected();
|
|
88
|
+
const key = this.#getRedisKey(request, options);
|
|
89
|
+
const cached = await this.#client.get(key);
|
|
90
|
+
if (!cached) {
|
|
91
|
+
return void 0;
|
|
92
|
+
}
|
|
93
|
+
const entry = JSON.parse(cached);
|
|
94
|
+
if (entry.TTL > 0) {
|
|
95
|
+
const ageInSeconds = (Date.now() - entry.cachedAt) / 1e3;
|
|
96
|
+
if (ageInSeconds > entry.TTL) {
|
|
97
|
+
await this.#client.del(key);
|
|
98
|
+
return void 0;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return this.#deserializeResponse(entry);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
logger.error("Failed to match", { error });
|
|
104
|
+
return void 0;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Puts a request/response pair into the cache
|
|
109
|
+
*/
|
|
110
|
+
async put(request, response) {
|
|
111
|
+
try {
|
|
112
|
+
await this.#ensureConnected();
|
|
113
|
+
const key = this.#getRedisKey(request);
|
|
114
|
+
const entry = await this.#serializeResponse(response);
|
|
115
|
+
const serialized = JSON.stringify(entry);
|
|
116
|
+
if (entry.TTL > 0) {
|
|
117
|
+
await this.#client.setEx(key, entry.TTL, serialized);
|
|
118
|
+
} else {
|
|
119
|
+
await this.#client.set(key, serialized);
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
logger.error("Failed to put", { error });
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Finds the cache entry whose key is the request, and if found, deletes it and returns true
|
|
128
|
+
*/
|
|
129
|
+
async delete(request, options) {
|
|
130
|
+
try {
|
|
131
|
+
await this.#ensureConnected();
|
|
132
|
+
const key = this.#getRedisKey(request, options);
|
|
133
|
+
const result = await this.#client.del(key);
|
|
134
|
+
return result > 0;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
logger.error("Failed to delete", { error });
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Returns a Promise that resolves to an array of cache keys (Request objects)
|
|
142
|
+
*/
|
|
143
|
+
async keys(request, options) {
|
|
144
|
+
try {
|
|
145
|
+
await this.#ensureConnected();
|
|
146
|
+
if (request) {
|
|
147
|
+
const key = this.#getRedisKey(request, options);
|
|
148
|
+
const exists = await this.#client.exists(key);
|
|
149
|
+
return exists ? [request] : [];
|
|
150
|
+
}
|
|
151
|
+
const pattern = `${this.#prefix}:*`;
|
|
152
|
+
const keys = [];
|
|
153
|
+
for await (const key of this.#client.scanIterator({
|
|
154
|
+
MATCH: pattern,
|
|
155
|
+
COUNT: 100
|
|
156
|
+
})) {
|
|
157
|
+
keys.push(key);
|
|
158
|
+
}
|
|
159
|
+
const requests = [];
|
|
160
|
+
for (const key of keys) {
|
|
161
|
+
try {
|
|
162
|
+
const cacheKey = key.replace(`${this.#prefix}:`, "");
|
|
163
|
+
const [method, url] = cacheKey.split(":", 2);
|
|
164
|
+
if (method && url) {
|
|
165
|
+
requests.push(new Request(url, { method }));
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return requests;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
logger.error("Failed to get keys", { error });
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
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 });
|
|
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
|
+
/**
|
|
219
|
+
* Dispose of Redis client connection
|
|
220
|
+
* Call this during graceful shutdown to properly close Redis connections
|
|
221
|
+
*/
|
|
222
|
+
async dispose() {
|
|
223
|
+
if (this.#connected || this.#client.isReady) {
|
|
224
|
+
try {
|
|
225
|
+
await this.#client.quit();
|
|
226
|
+
logger.info("Redis connection closed", { prefix: this.#prefix });
|
|
227
|
+
} catch (error) {
|
|
228
|
+
logger.error("Error closing Redis connection", { error });
|
|
229
|
+
try {
|
|
230
|
+
await this.#client.disconnect();
|
|
231
|
+
} catch (disconnectError) {
|
|
232
|
+
logger.error("Error forcing Redis disconnect", {
|
|
233
|
+
error: disconnectError
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
};
|
|
10
240
|
export {
|
|
11
|
-
RedisCache
|
|
12
|
-
createCache,
|
|
13
|
-
createRedisFactory
|
|
241
|
+
RedisCache
|
|
14
242
|
};
|
package/src/factory.cjs
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
-
var __export = (target, all) => {
|
|
6
|
-
for (var name in all)
|
|
7
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
-
};
|
|
9
|
-
var __copyProps = (to, from, except, desc) => {
|
|
10
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
-
for (let key of __getOwnPropNames(from))
|
|
12
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
-
}
|
|
15
|
-
return to;
|
|
16
|
-
};
|
|
17
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
-
|
|
19
|
-
// src/factory.ts
|
|
20
|
-
var factory_exports = {};
|
|
21
|
-
__export(factory_exports, {
|
|
22
|
-
createRedisFactory: () => createRedisFactory
|
|
23
|
-
});
|
|
24
|
-
module.exports = __toCommonJS(factory_exports);
|
|
25
|
-
var import_redis_cache = require("./redis-cache.cjs");
|
|
26
|
-
function createRedisFactory(options = {}) {
|
|
27
|
-
return (name) => {
|
|
28
|
-
return new import_redis_cache.RedisCache(name, options);
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
32
|
-
0 && (module.exports = {
|
|
33
|
-
createRedisFactory
|
|
34
|
-
});
|
package/src/factory.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { type CacheFactory } from "@b9g/cache";
|
|
2
|
-
import { type RedisCacheOptions } from "./redis-cache.js";
|
|
3
|
-
/**
|
|
4
|
-
* Create a Redis cache factory for use with CustomCacheStorage
|
|
5
|
-
*
|
|
6
|
-
* Example usage:
|
|
7
|
-
* ```typescript
|
|
8
|
-
* import {CustomCacheStorage} from "@b9g/cache";
|
|
9
|
-
* import {createRedisFactory} from "@b9g/cache-redis";
|
|
10
|
-
*
|
|
11
|
-
* const cacheStorage = new CustomCacheStorage(createRedisFactory({
|
|
12
|
-
* redis: { url: "redis://localhost:6379" },
|
|
13
|
-
* defaultTTL: 3600 // 1 hour
|
|
14
|
-
* }));
|
|
15
|
-
*
|
|
16
|
-
* const cache = await cacheStorage.open("my-cache");
|
|
17
|
-
* ```
|
|
18
|
-
*/
|
|
19
|
-
export declare function createRedisFactory(options?: RedisCacheOptions): CacheFactory;
|
package/src/factory.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/// <reference types="./factory.d.ts" />
|
|
2
|
-
// src/factory.ts
|
|
3
|
-
import { RedisCache } from "./redis-cache.js";
|
|
4
|
-
function createRedisFactory(options = {}) {
|
|
5
|
-
return (name) => {
|
|
6
|
-
return new RedisCache(name, options);
|
|
7
|
-
};
|
|
8
|
-
}
|
|
9
|
-
export {
|
|
10
|
-
createRedisFactory
|
|
11
|
-
};
|