@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/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
- export { RedisCache, type RedisCacheOptions } from "./redis-cache.js";
7
- export { createRedisFactory } from "./factory.js";
8
- import { RedisCache, type RedisCacheOptions } from "./redis-cache.js";
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
- * Platform adapter factory function
11
- * Creates a RedisCache instance with the given configuration
19
+ * Redis-backed cache implementation
20
+ * Stores HTTP responses with proper serialization and TTL support
12
21
  */
13
- export declare function createCache(config?: RedisCacheOptions & {
14
- name?: string;
15
- }): RedisCache;
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 { RedisCache } from "./redis-cache.js";
4
- import { createRedisFactory } from "./factory.js";
5
- import { RedisCache as RedisCache2 } from "./redis-cache.js";
6
- function createCache(config = {}) {
7
- const name = config.name || "default";
8
- return new RedisCache2(name, config);
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
- };