@happyvertical/cache 0.80.0 → 0.80.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.
@@ -0,0 +1,268 @@
1
+ import { CacheConnectionError, CacheError, CacheKeyError, CacheSerializationError, deserialize, formatKey, isValidKey, serialize } from "../index.js";
2
+ import { promisify } from "node:util";
3
+ import { gunzip, gzip } from "node:zlib";
4
+ import { createClient } from "redis";
5
+ //#region src/providers/redis.ts
6
+ /**
7
+ * Redis cache provider implementation
8
+ */
9
+ var gzipAsync = promisify(gzip);
10
+ var gunzipAsync = promisify(gunzip);
11
+ /**
12
+ * Redis cache provider implementation
13
+ * Uses official redis client with optional compression
14
+ */
15
+ var RedisProvider = class {
16
+ options;
17
+ client;
18
+ namespace;
19
+ defaultTTL;
20
+ enableCompression;
21
+ compressionThreshold;
22
+ stats;
23
+ connected = false;
24
+ constructor(options) {
25
+ this.options = options;
26
+ this.namespace = options.namespace || options.keyPrefix;
27
+ this.defaultTTL = options.defaultTTL;
28
+ this.enableCompression = options.enableCompression ?? false;
29
+ this.compressionThreshold = options.compressionThreshold || 1024;
30
+ this.stats = {
31
+ hits: 0,
32
+ misses: 0
33
+ };
34
+ this.client = createClient({
35
+ socket: {
36
+ host: options.host || "localhost",
37
+ port: options.port || 6379,
38
+ connectTimeout: options.connectTimeout || 5e3
39
+ },
40
+ password: options.password,
41
+ database: options.db || 0,
42
+ commandsQueueMaxLength: 1e3
43
+ });
44
+ this.client.on("error", (error) => {
45
+ console.error("Redis connection error:", error);
46
+ });
47
+ this.client.on("connect", () => {
48
+ this.connected = true;
49
+ });
50
+ this.client.on("end", () => {
51
+ this.connected = false;
52
+ });
53
+ }
54
+ /**
55
+ * Ensures the client is connected
56
+ */
57
+ async ensureConnected() {
58
+ if (!this.connected) try {
59
+ await this.client.connect();
60
+ this.connected = true;
61
+ } catch (error) {
62
+ throw new CacheConnectionError(`Failed to connect to Redis: ${error.message}`, "redis");
63
+ }
64
+ }
65
+ async get(key) {
66
+ if (!isValidKey(key)) throw new CacheKeyError(key, "redis");
67
+ await this.ensureConnected();
68
+ const fullKey = formatKey(this.namespace, key);
69
+ try {
70
+ const value = await this.client.get(fullKey);
71
+ if (value === null) {
72
+ this.stats.misses++;
73
+ return;
74
+ }
75
+ this.stats.hits++;
76
+ let data = value;
77
+ if (this.enableCompression && value.startsWith("gzip:")) data = (await gunzipAsync(Buffer.from(value.slice(5), "base64"))).toString("utf-8");
78
+ return deserialize(data);
79
+ } catch (error) {
80
+ throw new CacheError(`Failed to get cache entry: ${error.message}`, "GET_ERROR", "redis");
81
+ }
82
+ }
83
+ async set(key, value, ttl) {
84
+ if (!isValidKey(key)) throw new CacheKeyError(key, "redis");
85
+ await this.ensureConnected();
86
+ const fullKey = formatKey(this.namespace, key);
87
+ try {
88
+ let data = serialize(value);
89
+ if (this.enableCompression && data.length > this.compressionThreshold) data = `gzip:${(await gzipAsync(Buffer.from(data, "utf-8"))).toString("base64")}`;
90
+ const effectiveTTL = ttl ?? this.defaultTTL;
91
+ if (effectiveTTL !== void 0 && effectiveTTL > 0) await this.client.setEx(fullKey, effectiveTTL, data);
92
+ else await this.client.set(fullKey, data);
93
+ } catch (error) {
94
+ if (error.message?.includes("serialize")) throw new CacheSerializationError(`Failed to serialize value: ${error.message}`, "redis");
95
+ throw new CacheError(`Failed to set cache entry: ${error.message}`, "SET_ERROR", "redis");
96
+ }
97
+ }
98
+ async has(key) {
99
+ if (!isValidKey(key)) throw new CacheKeyError(key, "redis");
100
+ await this.ensureConnected();
101
+ const fullKey = formatKey(this.namespace, key);
102
+ try {
103
+ return await this.client.exists(fullKey) === 1;
104
+ } catch (error) {
105
+ throw new CacheError(`Failed to check cache entry: ${error.message}`, "EXISTS_ERROR", "redis");
106
+ }
107
+ }
108
+ async delete(key) {
109
+ if (!isValidKey(key)) throw new CacheKeyError(key, "redis");
110
+ await this.ensureConnected();
111
+ const fullKey = formatKey(this.namespace, key);
112
+ try {
113
+ return await this.client.del(fullKey) === 1;
114
+ } catch (error) {
115
+ throw new CacheError(`Failed to delete cache entry: ${error.message}`, "DELETE_ERROR", "redis");
116
+ }
117
+ }
118
+ async clear(namespace) {
119
+ await this.ensureConnected();
120
+ try {
121
+ if (namespace) {
122
+ const pattern = `${namespace}:*`;
123
+ let cursor = "0";
124
+ do {
125
+ const reply = await this.client.scan(cursor, {
126
+ MATCH: pattern,
127
+ COUNT: 100
128
+ });
129
+ cursor = reply.cursor;
130
+ if (reply.keys.length > 0) await this.client.del(reply.keys);
131
+ } while (cursor !== "0");
132
+ } else {
133
+ await this.client.flushDb();
134
+ this.stats.hits = 0;
135
+ this.stats.misses = 0;
136
+ }
137
+ } catch (error) {
138
+ throw new CacheError(`Failed to clear cache: ${error.message}`, "CLEAR_ERROR", "redis");
139
+ }
140
+ }
141
+ async keys(pattern) {
142
+ await this.ensureConnected();
143
+ try {
144
+ const searchPattern = pattern ? formatKey(this.namespace, pattern) : formatKey(this.namespace, "*");
145
+ const keys = [];
146
+ let cursor = "0";
147
+ do {
148
+ const reply = await this.client.scan(cursor, {
149
+ MATCH: searchPattern,
150
+ COUNT: 100
151
+ });
152
+ cursor = reply.cursor;
153
+ keys.push(...reply.keys);
154
+ } while (cursor !== "0");
155
+ if (this.namespace) {
156
+ const prefix = `${this.namespace}:`;
157
+ return keys.map((key) => key.startsWith(prefix) ? key.slice(prefix.length) : key);
158
+ }
159
+ return keys;
160
+ } catch (error) {
161
+ throw new CacheError(`Failed to get cache keys: ${error.message}`, "KEYS_ERROR", "redis");
162
+ }
163
+ }
164
+ async getMany(keys) {
165
+ if (keys.length === 0) return /* @__PURE__ */ new Map();
166
+ await this.ensureConnected();
167
+ const fullKeys = keys.map((key) => formatKey(this.namespace, key));
168
+ try {
169
+ const values = await this.client.mGet(fullKeys);
170
+ const result = /* @__PURE__ */ new Map();
171
+ for (let i = 0; i < keys.length; i++) {
172
+ const value = values[i];
173
+ if (value !== null) {
174
+ let data = value;
175
+ if (this.enableCompression && value.startsWith("gzip:")) data = (await gunzipAsync(Buffer.from(value.slice(5), "base64"))).toString("utf-8");
176
+ result.set(keys[i], deserialize(data));
177
+ this.stats.hits++;
178
+ } else this.stats.misses++;
179
+ }
180
+ return result;
181
+ } catch (error) {
182
+ throw new CacheError(`Failed to get multiple cache entries: ${error.message}`, "MGET_ERROR", "redis");
183
+ }
184
+ }
185
+ async setMany(entries) {
186
+ if (entries.length === 0) return;
187
+ await this.ensureConnected();
188
+ try {
189
+ const pipeline = this.client.multi();
190
+ for (const entry of entries) {
191
+ const fullKey = formatKey(this.namespace, entry.key);
192
+ let data = serialize(entry.value);
193
+ if (this.enableCompression && data.length > this.compressionThreshold) data = `gzip:${(await gzipAsync(Buffer.from(data, "utf-8"))).toString("base64")}`;
194
+ const effectiveTTL = entry.ttl ?? this.defaultTTL;
195
+ if (effectiveTTL !== void 0 && effectiveTTL > 0) pipeline.setEx(fullKey, effectiveTTL, data);
196
+ else pipeline.set(fullKey, data);
197
+ }
198
+ await pipeline.exec();
199
+ } catch (error) {
200
+ throw new CacheError(`Failed to set multiple cache entries: ${error.message}`, "MSET_ERROR", "redis");
201
+ }
202
+ }
203
+ async deleteMany(keys) {
204
+ if (keys.length === 0) return 0;
205
+ await this.ensureConnected();
206
+ const fullKeys = keys.map((key) => formatKey(this.namespace, key));
207
+ try {
208
+ return await this.client.del(fullKeys);
209
+ } catch (error) {
210
+ throw new CacheError(`Failed to delete multiple cache entries: ${error.message}`, "MDEL_ERROR", "redis");
211
+ }
212
+ }
213
+ async getStats() {
214
+ await this.ensureConnected();
215
+ try {
216
+ const info = await this.client.info("stats");
217
+ const dbMatch = (await this.client.info("keyspace")).match(/db\d+:keys=(\d+)/);
218
+ const entries = dbMatch ? parseInt(dbMatch[1], 10) : 0;
219
+ const hitsMatch = info.match(/keyspace_hits:(\d+)/);
220
+ const missesMatch = info.match(/keyspace_misses:(\d+)/);
221
+ const redisHits = hitsMatch ? parseInt(hitsMatch[1], 10) : 0;
222
+ const redisMisses = missesMatch ? parseInt(missesMatch[1], 10) : 0;
223
+ const totalHits = this.stats.hits + redisHits;
224
+ const totalMisses = this.stats.misses + redisMisses;
225
+ const totalAccesses = totalHits + totalMisses;
226
+ return {
227
+ entries,
228
+ totalSize: 0,
229
+ hits: totalHits,
230
+ misses: totalMisses,
231
+ hitRate: totalAccesses > 0 ? totalHits / totalAccesses : 0,
232
+ evictions: 0,
233
+ backend: {
234
+ type: "redis",
235
+ host: this.options.host || "localhost",
236
+ port: this.options.port || 6379,
237
+ db: this.options.db || 0,
238
+ compression: this.enableCompression
239
+ }
240
+ };
241
+ } catch (error) {
242
+ throw new CacheError(`Failed to get cache stats: ${error.message}`, "STATS_ERROR", "redis");
243
+ }
244
+ }
245
+ async touch(key, ttl) {
246
+ if (!isValidKey(key)) throw new CacheKeyError(key, "redis");
247
+ await this.ensureConnected();
248
+ const fullKey = formatKey(this.namespace, key);
249
+ try {
250
+ return await this.client.expire(fullKey, ttl) === 1;
251
+ } catch (error) {
252
+ throw new CacheError(`Failed to touch cache entry: ${error.message}`, "TOUCH_ERROR", "redis");
253
+ }
254
+ }
255
+ async close() {
256
+ if (this.connected) try {
257
+ await this.client.quit();
258
+ this.connected = false;
259
+ } catch (_error) {
260
+ await this.client.disconnect();
261
+ this.connected = false;
262
+ }
263
+ }
264
+ };
265
+ //#endregion
266
+ export { RedisProvider };
267
+
268
+ //# sourceMappingURL=redis-BMACvZJq.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-BMACvZJq.js","names":[],"sources":["../../src/providers/redis.ts"],"sourcesContent":["/**\n * Redis cache provider implementation\n */\n\nimport { promisify } from 'node:util';\nimport { gunzip, gzip } from 'node:zlib';\nimport { createClient, type RedisClientType } from 'redis';\nimport type { CacheProvider, CacheStats, RedisOptions } from '../shared/types';\nimport {\n CacheConnectionError,\n CacheError,\n CacheKeyError,\n CacheSerializationError,\n} from '../shared/types';\nimport { deserialize, formatKey, isValidKey, serialize } from '../shared/utils';\n\nconst gzipAsync = promisify(gzip);\nconst gunzipAsync = promisify(gunzip);\n\n/**\n * Redis cache provider implementation\n * Uses official redis client with optional compression\n */\nexport class RedisProvider implements CacheProvider {\n private client: RedisClientType;\n private namespace?: string;\n private defaultTTL?: number;\n private enableCompression: boolean;\n private compressionThreshold: number;\n private stats: {\n hits: number;\n misses: number;\n };\n private connected: boolean = false;\n\n constructor(private options: RedisOptions) {\n this.namespace = options.namespace || options.keyPrefix;\n this.defaultTTL = options.defaultTTL;\n this.enableCompression = options.enableCompression ?? false;\n this.compressionThreshold = options.compressionThreshold || 1024;\n this.stats = {\n hits: 0,\n misses: 0,\n };\n\n // Create Redis client\n this.client = createClient({\n socket: {\n host: options.host || 'localhost',\n port: options.port || 6379,\n connectTimeout: options.connectTimeout || 5000,\n },\n password: options.password,\n database: options.db || 0,\n commandsQueueMaxLength: 1000,\n }) as RedisClientType;\n\n // Setup error handling\n this.client.on('error', (error) => {\n console.error('Redis connection error:', error);\n });\n\n this.client.on('connect', () => {\n this.connected = true;\n });\n\n this.client.on('end', () => {\n this.connected = false;\n });\n }\n\n /**\n * Ensures the client is connected\n */\n private async ensureConnected(): Promise<void> {\n if (!this.connected) {\n try {\n await this.client.connect();\n this.connected = true;\n } catch (error: any) {\n throw new CacheConnectionError(\n `Failed to connect to Redis: ${error.message}`,\n 'redis',\n );\n }\n }\n }\n\n async get<T = any>(key: string): Promise<T | undefined> {\n if (!isValidKey(key)) {\n throw new CacheKeyError(key, 'redis');\n }\n\n await this.ensureConnected();\n\n const fullKey = formatKey(this.namespace, key);\n\n try {\n const value = await this.client.get(fullKey);\n\n if (value === null) {\n this.stats.misses++;\n return undefined;\n }\n\n this.stats.hits++;\n\n // Decompress if needed (check for compression marker)\n let data = value;\n if (this.enableCompression && value.startsWith('gzip:')) {\n const compressed = Buffer.from(value.slice(5), 'base64');\n const decompressed = await gunzipAsync(compressed);\n data = decompressed.toString('utf-8');\n }\n\n return deserialize<T>(data);\n } catch (error: any) {\n throw new CacheError(\n `Failed to get cache entry: ${error.message}`,\n 'GET_ERROR',\n 'redis',\n );\n }\n }\n\n async set<T = any>(key: string, value: T, ttl?: number): Promise<void> {\n if (!isValidKey(key)) {\n throw new CacheKeyError(key, 'redis');\n }\n\n await this.ensureConnected();\n\n const fullKey = formatKey(this.namespace, key);\n\n try {\n let data = serialize(value);\n\n // Compress if enabled and value is large enough\n if (this.enableCompression && data.length > this.compressionThreshold) {\n const compressed = await gzipAsync(Buffer.from(data, 'utf-8'));\n data = `gzip:${compressed.toString('base64')}`;\n }\n\n const effectiveTTL = ttl ?? this.defaultTTL;\n\n if (effectiveTTL !== undefined && effectiveTTL > 0) {\n await this.client.setEx(fullKey, effectiveTTL, data);\n } else {\n await this.client.set(fullKey, data);\n }\n } catch (error: any) {\n if (error.message?.includes('serialize')) {\n throw new CacheSerializationError(\n `Failed to serialize value: ${error.message}`,\n 'redis',\n );\n }\n throw new CacheError(\n `Failed to set cache entry: ${error.message}`,\n 'SET_ERROR',\n 'redis',\n );\n }\n }\n\n async has(key: string): Promise<boolean> {\n if (!isValidKey(key)) {\n throw new CacheKeyError(key, 'redis');\n }\n\n await this.ensureConnected();\n\n const fullKey = formatKey(this.namespace, key);\n\n try {\n const exists = await this.client.exists(fullKey);\n return exists === 1;\n } catch (error: any) {\n throw new CacheError(\n `Failed to check cache entry: ${error.message}`,\n 'EXISTS_ERROR',\n 'redis',\n );\n }\n }\n\n async delete(key: string): Promise<boolean> {\n if (!isValidKey(key)) {\n throw new CacheKeyError(key, 'redis');\n }\n\n await this.ensureConnected();\n\n const fullKey = formatKey(this.namespace, key);\n\n try {\n const deleted = await this.client.del(fullKey);\n return deleted === 1;\n } catch (error: any) {\n throw new CacheError(\n `Failed to delete cache entry: ${error.message}`,\n 'DELETE_ERROR',\n 'redis',\n );\n }\n }\n\n async clear(namespace?: string): Promise<void> {\n await this.ensureConnected();\n\n try {\n if (namespace) {\n // Clear specific namespace\n const pattern = `${namespace}:*`;\n let cursor = '0';\n\n do {\n const reply = await this.client.scan(cursor, {\n MATCH: pattern,\n COUNT: 100,\n });\n\n cursor = reply.cursor;\n\n if (reply.keys.length > 0) {\n await this.client.del(reply.keys);\n }\n } while (cursor !== '0');\n } else {\n // Clear all keys (use with caution!)\n await this.client.flushDb();\n this.stats.hits = 0;\n this.stats.misses = 0;\n }\n } catch (error: any) {\n throw new CacheError(\n `Failed to clear cache: ${error.message}`,\n 'CLEAR_ERROR',\n 'redis',\n );\n }\n }\n\n async keys(pattern?: string): Promise<string[]> {\n await this.ensureConnected();\n\n try {\n const searchPattern = pattern\n ? formatKey(this.namespace, pattern)\n : formatKey(this.namespace, '*');\n\n const keys: string[] = [];\n let cursor = '0';\n\n do {\n const reply = await this.client.scan(cursor, {\n MATCH: searchPattern,\n COUNT: 100,\n });\n\n cursor = reply.cursor;\n keys.push(...reply.keys);\n } while (cursor !== '0');\n\n // Remove namespace prefix if present\n if (this.namespace) {\n const prefix = `${this.namespace}:`;\n return keys.map((key) =>\n key.startsWith(prefix) ? key.slice(prefix.length) : key,\n );\n }\n\n return keys;\n } catch (error: any) {\n throw new CacheError(\n `Failed to get cache keys: ${error.message}`,\n 'KEYS_ERROR',\n 'redis',\n );\n }\n }\n\n async getMany<T = any>(keys: string[]): Promise<Map<string, T>> {\n if (keys.length === 0) {\n return new Map();\n }\n\n await this.ensureConnected();\n\n const fullKeys = keys.map((key) => formatKey(this.namespace, key));\n\n try {\n const values = await this.client.mGet(fullKeys);\n const result = new Map<string, T>();\n\n for (let i = 0; i < keys.length; i++) {\n const value = values[i];\n if (value !== null) {\n // Decompress if needed\n let data = value;\n if (this.enableCompression && value.startsWith('gzip:')) {\n const compressed = Buffer.from(value.slice(5), 'base64');\n const decompressed = await gunzipAsync(compressed);\n data = decompressed.toString('utf-8');\n }\n\n result.set(keys[i], deserialize<T>(data));\n this.stats.hits++;\n } else {\n this.stats.misses++;\n }\n }\n\n return result;\n } catch (error: any) {\n throw new CacheError(\n `Failed to get multiple cache entries: ${error.message}`,\n 'MGET_ERROR',\n 'redis',\n );\n }\n }\n\n async setMany<T = any>(\n entries: Array<{ key: string; value: T; ttl?: number }>,\n ): Promise<void> {\n if (entries.length === 0) {\n return;\n }\n\n await this.ensureConnected();\n\n try {\n // Use pipeline for efficiency\n const pipeline = this.client.multi();\n\n for (const entry of entries) {\n const fullKey = formatKey(this.namespace, entry.key);\n let data = serialize(entry.value);\n\n // Compress if enabled and value is large enough\n if (this.enableCompression && data.length > this.compressionThreshold) {\n const compressed = await gzipAsync(Buffer.from(data, 'utf-8'));\n data = `gzip:${compressed.toString('base64')}`;\n }\n\n const effectiveTTL = entry.ttl ?? this.defaultTTL;\n\n if (effectiveTTL !== undefined && effectiveTTL > 0) {\n pipeline.setEx(fullKey, effectiveTTL, data);\n } else {\n pipeline.set(fullKey, data);\n }\n }\n\n await pipeline.exec();\n } catch (error: any) {\n throw new CacheError(\n `Failed to set multiple cache entries: ${error.message}`,\n 'MSET_ERROR',\n 'redis',\n );\n }\n }\n\n async deleteMany(keys: string[]): Promise<number> {\n if (keys.length === 0) {\n return 0;\n }\n\n await this.ensureConnected();\n\n const fullKeys = keys.map((key) => formatKey(this.namespace, key));\n\n try {\n const deleted = await this.client.del(fullKeys);\n return deleted;\n } catch (error: any) {\n throw new CacheError(\n `Failed to delete multiple cache entries: ${error.message}`,\n 'MDEL_ERROR',\n 'redis',\n );\n }\n }\n\n async getStats(): Promise<CacheStats> {\n await this.ensureConnected();\n\n try {\n // Get Redis INFO stats\n const info = await this.client.info('stats');\n const dbInfo = await this.client.info('keyspace');\n\n // Parse keyspace info to get entry count\n const dbMatch = dbInfo.match(/db\\d+:keys=(\\d+)/);\n const entries = dbMatch ? parseInt(dbMatch[1], 10) : 0;\n\n // Parse stats info\n const hitsMatch = info.match(/keyspace_hits:(\\d+)/);\n const missesMatch = info.match(/keyspace_misses:(\\d+)/);\n const redisHits = hitsMatch ? parseInt(hitsMatch[1], 10) : 0;\n const redisMisses = missesMatch ? parseInt(missesMatch[1], 10) : 0;\n\n const totalHits = this.stats.hits + redisHits;\n const totalMisses = this.stats.misses + redisMisses;\n const totalAccesses = totalHits + totalMisses;\n const hitRate = totalAccesses > 0 ? totalHits / totalAccesses : 0;\n\n return {\n entries,\n totalSize: 0, // Redis doesn't easily expose total memory per keyspace\n hits: totalHits,\n misses: totalMisses,\n hitRate,\n evictions: 0, // Would need to parse evicted_keys from stats\n backend: {\n type: 'redis',\n host: this.options.host || 'localhost',\n port: this.options.port || 6379,\n db: this.options.db || 0,\n compression: this.enableCompression,\n },\n };\n } catch (error: any) {\n throw new CacheError(\n `Failed to get cache stats: ${error.message}`,\n 'STATS_ERROR',\n 'redis',\n );\n }\n }\n\n async touch(key: string, ttl: number): Promise<boolean> {\n if (!isValidKey(key)) {\n throw new CacheKeyError(key, 'redis');\n }\n\n await this.ensureConnected();\n\n const fullKey = formatKey(this.namespace, key);\n\n try {\n const result = await this.client.expire(fullKey, ttl);\n return result === 1;\n } catch (error: any) {\n throw new CacheError(\n `Failed to touch cache entry: ${error.message}`,\n 'TOUCH_ERROR',\n 'redis',\n );\n }\n }\n\n async close(): Promise<void> {\n if (this.connected) {\n try {\n await this.client.quit();\n this.connected = false;\n } catch (_error: any) {\n // Force disconnect if graceful quit fails\n await this.client.disconnect();\n this.connected = false;\n }\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,348 @@
1
+ import { CacheError, CacheKeyError, CacheSerializationError, calculateExpiration, calculateSize, deserialize, extractKey, formatKey, isExpired, isValidKey, matchesPattern, serialize } from "../index.js";
2
+ import { promisify } from "node:util";
3
+ import { gunzip, gzip } from "node:zlib";
4
+ //#region src/providers/s3.ts
5
+ /**
6
+ * S3 cache provider implementation
7
+ * Stores cache entries in S3 for persistence across CI runs
8
+ */
9
+ var gzipAsync = promisify(gzip);
10
+ var gunzipAsync = promisify(gunzip);
11
+ var S3Client;
12
+ var GetObjectCommand;
13
+ var PutObjectCommand;
14
+ var DeleteObjectCommand;
15
+ var DeleteObjectsCommand;
16
+ var ListObjectsV2Command;
17
+ var HeadObjectCommand;
18
+ async function loadS3SDK() {
19
+ if (!S3Client) {
20
+ const sdk = await import("@aws-sdk/client-s3");
21
+ S3Client = sdk.S3Client;
22
+ GetObjectCommand = sdk.GetObjectCommand;
23
+ PutObjectCommand = sdk.PutObjectCommand;
24
+ DeleteObjectCommand = sdk.DeleteObjectCommand;
25
+ DeleteObjectsCommand = sdk.DeleteObjectsCommand;
26
+ ListObjectsV2Command = sdk.ListObjectsV2Command;
27
+ HeadObjectCommand = sdk.HeadObjectCommand;
28
+ }
29
+ }
30
+ /**
31
+ * Converts a readable stream to a buffer
32
+ */
33
+ async function streamToBuffer(stream) {
34
+ const chunks = [];
35
+ for await (const chunk of stream) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
36
+ return Buffer.concat(chunks);
37
+ }
38
+ /**
39
+ * S3 cache provider implementation
40
+ * Stores cache entries as S3 objects with optional compression
41
+ */
42
+ var S3Provider = class {
43
+ client;
44
+ bucket;
45
+ prefix;
46
+ namespace;
47
+ defaultTTL;
48
+ compression;
49
+ compressionThreshold;
50
+ region;
51
+ initialized = false;
52
+ stats;
53
+ constructor(options) {
54
+ this.bucket = options.bucket;
55
+ this.prefix = options.prefix || "cache/";
56
+ this.namespace = options.namespace;
57
+ this.defaultTTL = options.defaultTTL;
58
+ this.compression = options.compression ?? true;
59
+ this.compressionThreshold = options.compressionThreshold ?? 1024;
60
+ this.region = options.region || process.env.AWS_REGION || "us-east-1";
61
+ this.stats = {
62
+ hits: 0,
63
+ misses: 0,
64
+ evictions: 0
65
+ };
66
+ }
67
+ /**
68
+ * Lazily initialize the S3 client
69
+ */
70
+ async ensureInitialized() {
71
+ if (this.initialized) return;
72
+ await loadS3SDK();
73
+ const credentials = process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY ? {
74
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
75
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
76
+ } : void 0;
77
+ this.client = new S3Client({
78
+ region: this.region,
79
+ credentials
80
+ });
81
+ this.initialized = true;
82
+ }
83
+ async get(key) {
84
+ if (!isValidKey(key)) throw new CacheKeyError(key, "s3");
85
+ await this.ensureInitialized();
86
+ const s3Key = this.getS3Key(key);
87
+ try {
88
+ const response = await this.client.send(new GetObjectCommand({
89
+ Bucket: this.bucket,
90
+ Key: s3Key
91
+ }));
92
+ const bodyBuffer = await streamToBuffer(response.Body);
93
+ const isCompressed = response.Metadata?.["compressed"] === "true";
94
+ let data;
95
+ if (isCompressed) data = await gunzipAsync(bodyBuffer);
96
+ else data = bodyBuffer;
97
+ const entry = deserialize(data.toString("utf-8"));
98
+ if (isExpired(entry.expiresAt)) {
99
+ this.stats.misses++;
100
+ this.delete(key).catch(() => {});
101
+ return;
102
+ }
103
+ this.stats.hits++;
104
+ return entry.value;
105
+ } catch (error) {
106
+ if (error.name === "NoSuchKey" || error.$metadata?.httpStatusCode === 404) {
107
+ this.stats.misses++;
108
+ return;
109
+ }
110
+ throw new CacheError(`Failed to read S3 cache entry: ${error.message}`, "READ_ERROR", "s3");
111
+ }
112
+ }
113
+ async set(key, value, ttl) {
114
+ if (!isValidKey(key)) throw new CacheKeyError(key, "s3");
115
+ await this.ensureInitialized();
116
+ const s3Key = this.getS3Key(key);
117
+ const expiresAt = calculateExpiration(ttl ?? this.defaultTTL);
118
+ const entry = {
119
+ value,
120
+ createdAt: Date.now(),
121
+ expiresAt,
122
+ size: calculateSize(value),
123
+ hits: 0,
124
+ metadata: {
125
+ compressed: false,
126
+ namespace: this.namespace
127
+ }
128
+ };
129
+ let data = Buffer.from(serialize(entry), "utf-8");
130
+ let isCompressed = false;
131
+ if (this.compression && data.length > this.compressionThreshold) {
132
+ data = await gzipAsync(data);
133
+ isCompressed = true;
134
+ }
135
+ const metadata = {
136
+ "created-at": entry.createdAt.toString(),
137
+ compressed: isCompressed.toString()
138
+ };
139
+ if (expiresAt) metadata["expires-at"] = expiresAt.toString();
140
+ if (this.namespace) metadata["namespace"] = this.namespace;
141
+ try {
142
+ await this.client.send(new PutObjectCommand({
143
+ Bucket: this.bucket,
144
+ Key: s3Key,
145
+ Body: data,
146
+ Metadata: metadata,
147
+ ContentType: "application/json"
148
+ }));
149
+ } catch (error) {
150
+ throw new CacheSerializationError(`Failed to write S3 cache entry: ${error.message}`, "s3");
151
+ }
152
+ }
153
+ async has(key) {
154
+ if (!isValidKey(key)) throw new CacheKeyError(key, "s3");
155
+ await this.ensureInitialized();
156
+ const s3Key = this.getS3Key(key);
157
+ try {
158
+ const expiresAt = (await this.client.send(new HeadObjectCommand({
159
+ Bucket: this.bucket,
160
+ Key: s3Key
161
+ }))).Metadata?.["expires-at"];
162
+ if (expiresAt && Date.now() >= parseInt(expiresAt, 10)) return false;
163
+ return true;
164
+ } catch (error) {
165
+ if (error.name === "NotFound" || error.$metadata?.httpStatusCode === 404) return false;
166
+ throw new CacheError(`Failed to check S3 cache entry: ${error.message}`, "CHECK_ERROR", "s3");
167
+ }
168
+ }
169
+ async delete(key) {
170
+ if (!isValidKey(key)) throw new CacheKeyError(key, "s3");
171
+ await this.ensureInitialized();
172
+ const s3Key = this.getS3Key(key);
173
+ try {
174
+ await this.client.send(new DeleteObjectCommand({
175
+ Bucket: this.bucket,
176
+ Key: s3Key
177
+ }));
178
+ return true;
179
+ } catch (error) {
180
+ return true;
181
+ }
182
+ }
183
+ async clear(namespace) {
184
+ await this.ensureInitialized();
185
+ const targetNamespace = namespace || this.namespace;
186
+ const prefix = targetNamespace ? `${this.prefix}${this.sanitizeKey(`${targetNamespace}:`)}` : this.prefix;
187
+ try {
188
+ let continuationToken;
189
+ do {
190
+ const listResponse = await this.client.send(new ListObjectsV2Command({
191
+ Bucket: this.bucket,
192
+ Prefix: prefix,
193
+ ContinuationToken: continuationToken
194
+ }));
195
+ if (listResponse.Contents && listResponse.Contents.length > 0) {
196
+ const objects = listResponse.Contents.map((obj) => ({ Key: obj.Key }));
197
+ await this.client.send(new DeleteObjectsCommand({
198
+ Bucket: this.bucket,
199
+ Delete: { Objects: objects }
200
+ }));
201
+ }
202
+ continuationToken = listResponse.NextContinuationToken;
203
+ } while (continuationToken);
204
+ this.stats.hits = 0;
205
+ this.stats.misses = 0;
206
+ this.stats.evictions = 0;
207
+ } catch (error) {
208
+ throw new CacheError(`Failed to clear S3 cache: ${error.message}`, "CLEAR_ERROR", "s3");
209
+ }
210
+ }
211
+ async keys(pattern) {
212
+ await this.ensureInitialized();
213
+ const keys = [];
214
+ let continuationToken;
215
+ try {
216
+ do {
217
+ const listResponse = await this.client.send(new ListObjectsV2Command({
218
+ Bucket: this.bucket,
219
+ Prefix: this.prefix,
220
+ ContinuationToken: continuationToken
221
+ }));
222
+ if (listResponse.Contents) for (const obj of listResponse.Contents) {
223
+ const s3Key = obj.Key;
224
+ if (!s3Key.endsWith(".cache")) continue;
225
+ const rawKey = s3Key.slice(this.prefix.length).replace(/\.cache$/, "");
226
+ const desanitized = this.desanitizeKey(rawKey);
227
+ const extractedKey = extractKey(this.namespace, desanitized);
228
+ keys.push(extractedKey);
229
+ }
230
+ continuationToken = listResponse.NextContinuationToken;
231
+ } while (continuationToken);
232
+ } catch (error) {
233
+ throw new CacheError(`Failed to list S3 cache keys: ${error.message}`, "LIST_ERROR", "s3");
234
+ }
235
+ if (pattern) return keys.filter((key) => matchesPattern(pattern, key));
236
+ return keys;
237
+ }
238
+ async getMany(keys) {
239
+ const result = /* @__PURE__ */ new Map();
240
+ const results = await Promise.allSettled(keys.map(async (key) => {
241
+ return {
242
+ key,
243
+ value: await this.get(key)
244
+ };
245
+ }));
246
+ for (const result of results) if (result.status === "fulfilled" && result.value.value !== void 0) result.value;
247
+ for (const r of results) if (r.status === "fulfilled" && r.value.value !== void 0) result.set(r.value.key, r.value.value);
248
+ return result;
249
+ }
250
+ async setMany(entries) {
251
+ await Promise.all(entries.map((entry) => this.set(entry.key, entry.value, entry.ttl)));
252
+ }
253
+ async deleteMany(keys) {
254
+ await this.ensureInitialized();
255
+ if (keys.length === 0) return 0;
256
+ const s3Keys = keys.map((key) => ({ Key: this.getS3Key(key) }));
257
+ try {
258
+ let deleted = 0;
259
+ for (let i = 0; i < s3Keys.length; i += 1e3) {
260
+ const batch = s3Keys.slice(i, i + 1e3);
261
+ const response = await this.client.send(new DeleteObjectsCommand({
262
+ Bucket: this.bucket,
263
+ Delete: { Objects: batch }
264
+ }));
265
+ deleted += response.Deleted?.length || 0;
266
+ }
267
+ return deleted;
268
+ } catch (error) {
269
+ throw new CacheError(`Failed to delete S3 cache entries: ${error.message}`, "DELETE_ERROR", "s3");
270
+ }
271
+ }
272
+ async getStats() {
273
+ await this.ensureInitialized();
274
+ let entries = 0;
275
+ let totalSize = 0;
276
+ let continuationToken;
277
+ try {
278
+ do {
279
+ const listResponse = await this.client.send(new ListObjectsV2Command({
280
+ Bucket: this.bucket,
281
+ Prefix: this.prefix,
282
+ ContinuationToken: continuationToken
283
+ }));
284
+ if (listResponse.Contents) for (const obj of listResponse.Contents) {
285
+ entries++;
286
+ totalSize += obj.Size || 0;
287
+ }
288
+ continuationToken = listResponse.NextContinuationToken;
289
+ } while (continuationToken);
290
+ } catch (error) {
291
+ entries = 0;
292
+ totalSize = 0;
293
+ }
294
+ const totalAccesses = this.stats.hits + this.stats.misses;
295
+ const hitRate = totalAccesses > 0 ? this.stats.hits / totalAccesses : 0;
296
+ return {
297
+ entries,
298
+ totalSize,
299
+ hits: this.stats.hits,
300
+ misses: this.stats.misses,
301
+ hitRate,
302
+ evictions: this.stats.evictions,
303
+ backend: {
304
+ type: "s3",
305
+ bucket: this.bucket,
306
+ prefix: this.prefix,
307
+ region: this.region,
308
+ compression: this.compression
309
+ }
310
+ };
311
+ }
312
+ async touch(key, ttl) {
313
+ if (!isValidKey(key)) throw new CacheKeyError(key, "s3");
314
+ const value = await this.get(key);
315
+ if (value === void 0) return false;
316
+ await this.set(key, value, ttl);
317
+ return true;
318
+ }
319
+ async close() {
320
+ if (this.client?.destroy) this.client.destroy();
321
+ this.initialized = false;
322
+ }
323
+ /**
324
+ * Gets the S3 key for a cache key
325
+ */
326
+ getS3Key(key) {
327
+ const fullKey = formatKey(this.namespace, key);
328
+ const sanitized = this.sanitizeKey(fullKey);
329
+ return `${this.prefix}${sanitized}.cache`;
330
+ }
331
+ /**
332
+ * Sanitizes a key for use as an S3 key
333
+ * S3 keys can contain most characters, but we sanitize for consistency
334
+ */
335
+ sanitizeKey(key) {
336
+ return key.replace(/[^a-zA-Z0-9_:-]/g, "_");
337
+ }
338
+ /**
339
+ * Desanitizes an S3 key back to the original format
340
+ */
341
+ desanitizeKey(sanitized) {
342
+ return sanitized;
343
+ }
344
+ };
345
+ //#endregion
346
+ export { S3Provider };
347
+
348
+ //# sourceMappingURL=s3-DG191QH0.js.map