@prismakit/redis 3.0.2 → 3.2.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/dist/index.cjs +99 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -15
- package/dist/index.d.ts +37 -15
- package/dist/index.js +97 -58
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/redis-cache-adapter.integration.spec.ts +46 -0
- package/src/__tests__/redis-cache-adapter.spec.ts +14 -0
- package/src/index.ts +4 -0
- package/src/redis-cache-adapter.ts +42 -50
- package/src/redis-json.ts +115 -17
package/dist/index.js
CHANGED
|
@@ -4,26 +4,83 @@ import { gzipSync, gunzipSync } from "zlib";
|
|
|
4
4
|
|
|
5
5
|
// src/redis-json.ts
|
|
6
6
|
var BIGINT_TAG = "__bigint";
|
|
7
|
+
var DATE_TAG = "__date";
|
|
8
|
+
var BYTES_TAG = "__bytes";
|
|
9
|
+
var DECIMAL_TAG = "__decimal";
|
|
10
|
+
function isPlainObject(value) {
|
|
11
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date) && !Buffer.isBuffer(value);
|
|
12
|
+
}
|
|
13
|
+
function isDecimalLike(value) {
|
|
14
|
+
return isPlainObject(value) && typeof value.constructor?.name === "string" && (value.constructor.name === "Decimal" || typeof value.toFixed === "function" && typeof value.d === "object");
|
|
15
|
+
}
|
|
7
16
|
function redisJsonReplacer(_key, value) {
|
|
8
17
|
if (typeof value === "bigint") {
|
|
9
18
|
return { [BIGINT_TAG]: value.toString() };
|
|
10
19
|
}
|
|
20
|
+
if (value instanceof Date) {
|
|
21
|
+
return { [DATE_TAG]: value.toISOString() };
|
|
22
|
+
}
|
|
23
|
+
if (Buffer.isBuffer(value)) {
|
|
24
|
+
return { [BYTES_TAG]: value.toString("base64") };
|
|
25
|
+
}
|
|
26
|
+
if (value && typeof value === "object" && value.type === "Buffer" && Array.isArray(value.data)) {
|
|
27
|
+
return {
|
|
28
|
+
[BYTES_TAG]: Buffer.from(
|
|
29
|
+
value.data
|
|
30
|
+
).toString("base64")
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (isDecimalLike(value)) {
|
|
34
|
+
return { [DECIMAL_TAG]: value.toFixed() };
|
|
35
|
+
}
|
|
11
36
|
return value;
|
|
12
37
|
}
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (
|
|
17
|
-
|
|
38
|
+
function createRedisJsonReviver(options = {}) {
|
|
39
|
+
const decimalFactory = options.decimalFactory ?? ((s) => s);
|
|
40
|
+
return (_key, value) => {
|
|
41
|
+
if (!isPlainObject(value)) return value;
|
|
42
|
+
const keys = Object.keys(value);
|
|
43
|
+
if (keys.length !== 1) return value;
|
|
44
|
+
if (BIGINT_TAG in value && typeof value[BIGINT_TAG] === "string") {
|
|
45
|
+
return BigInt(value[BIGINT_TAG]);
|
|
18
46
|
}
|
|
19
|
-
|
|
20
|
-
|
|
47
|
+
if (DATE_TAG in value && typeof value[DATE_TAG] === "string") {
|
|
48
|
+
return new Date(value[DATE_TAG]);
|
|
49
|
+
}
|
|
50
|
+
if (BYTES_TAG in value && typeof value[BYTES_TAG] === "string") {
|
|
51
|
+
return Buffer.from(value[BYTES_TAG], "base64");
|
|
52
|
+
}
|
|
53
|
+
if (DECIMAL_TAG in value && typeof value[DECIMAL_TAG] === "string") {
|
|
54
|
+
return decimalFactory(value[DECIMAL_TAG]);
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function redisJsonReviver(_key, value) {
|
|
60
|
+
return createRedisJsonReviver()(_key, value);
|
|
21
61
|
}
|
|
22
62
|
function redisJsonStringify(value) {
|
|
23
|
-
return JSON.stringify(value,
|
|
63
|
+
return JSON.stringify(value, function(key, val) {
|
|
64
|
+
const holder = this;
|
|
65
|
+
const raw = key === "" ? value : holder[key];
|
|
66
|
+
if (raw instanceof Date) {
|
|
67
|
+
return redisJsonReplacer(key, raw);
|
|
68
|
+
}
|
|
69
|
+
return redisJsonReplacer(key, val);
|
|
70
|
+
});
|
|
24
71
|
}
|
|
25
|
-
function redisJsonParse(raw) {
|
|
26
|
-
return JSON.parse(raw,
|
|
72
|
+
function redisJsonParse(raw, options) {
|
|
73
|
+
return JSON.parse(raw, createRedisJsonReviver(options));
|
|
74
|
+
}
|
|
75
|
+
function cloneWithCodec(value, options) {
|
|
76
|
+
if (value === null || value === void 0) return value;
|
|
77
|
+
if (typeof structuredClone === "function") {
|
|
78
|
+
try {
|
|
79
|
+
return structuredClone(value);
|
|
80
|
+
} catch {
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return redisJsonParse(redisJsonStringify(value), options);
|
|
27
84
|
}
|
|
28
85
|
|
|
29
86
|
// src/redis-cache-adapter.ts
|
|
@@ -43,6 +100,8 @@ var RedisCacheAdapter = class {
|
|
|
43
100
|
ready = false;
|
|
44
101
|
compression;
|
|
45
102
|
compressionThreshold;
|
|
103
|
+
jsonOptions;
|
|
104
|
+
onError;
|
|
46
105
|
constructor(options = {}) {
|
|
47
106
|
const {
|
|
48
107
|
url,
|
|
@@ -50,11 +109,15 @@ var RedisCacheAdapter = class {
|
|
|
50
109
|
port = 6379,
|
|
51
110
|
prefix = "prismakit",
|
|
52
111
|
compression = "none",
|
|
53
|
-
compressionThresholdBytes = 1024
|
|
112
|
+
compressionThresholdBytes = 1024,
|
|
113
|
+
decimalFactory,
|
|
114
|
+
onError
|
|
54
115
|
} = options;
|
|
55
116
|
this.prefix = prefix;
|
|
56
117
|
this.compression = compression;
|
|
57
118
|
this.compressionThreshold = compressionThresholdBytes;
|
|
119
|
+
this.jsonOptions = { decimalFactory };
|
|
120
|
+
this.onError = onError;
|
|
58
121
|
this.client = url ? new Redis(url, { lazyConnect: true }) : new Redis({ host, port, lazyConnect: true });
|
|
59
122
|
this.client.on("ready", () => {
|
|
60
123
|
this.ready = true;
|
|
@@ -68,6 +131,16 @@ var RedisCacheAdapter = class {
|
|
|
68
131
|
});
|
|
69
132
|
void this.connect();
|
|
70
133
|
}
|
|
134
|
+
report(op, err) {
|
|
135
|
+
console.warn(
|
|
136
|
+
`[RedisCacheAdapter] ${op} failed`,
|
|
137
|
+
err?.message ?? err
|
|
138
|
+
);
|
|
139
|
+
try {
|
|
140
|
+
this.onError?.(err, op);
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
}
|
|
71
144
|
async connect() {
|
|
72
145
|
if (this.client.status === "ready" || this.client.status === "connecting") {
|
|
73
146
|
return;
|
|
@@ -75,10 +148,7 @@ var RedisCacheAdapter = class {
|
|
|
75
148
|
try {
|
|
76
149
|
await this.client.connect();
|
|
77
150
|
} catch (err) {
|
|
78
|
-
|
|
79
|
-
"[RedisCacheAdapter] failed to connect",
|
|
80
|
-
err.message
|
|
81
|
-
);
|
|
151
|
+
this.report("connect", err);
|
|
82
152
|
}
|
|
83
153
|
}
|
|
84
154
|
async disconnect() {
|
|
@@ -103,11 +173,10 @@ var RedisCacheAdapter = class {
|
|
|
103
173
|
if (raw.startsWith(COMPRESSED_PREFIX)) {
|
|
104
174
|
const buf = Buffer.from(raw.slice(COMPRESSED_PREFIX.length), "base64");
|
|
105
175
|
const json = gunzipSync(buf).toString("utf8");
|
|
106
|
-
return redisJsonParse(json);
|
|
176
|
+
return redisJsonParse(json, this.jsonOptions);
|
|
107
177
|
}
|
|
108
|
-
return redisJsonParse(raw);
|
|
178
|
+
return redisJsonParse(raw, this.jsonOptions);
|
|
109
179
|
}
|
|
110
|
-
// --- low-level ops (may throw) ---
|
|
111
180
|
async get(key) {
|
|
112
181
|
const raw = await this.client.get(key);
|
|
113
182
|
if (raw === null) return null;
|
|
@@ -131,10 +200,6 @@ var RedisCacheAdapter = class {
|
|
|
131
200
|
const result = await this.client.set(key, "1", "EX", ttlSeconds, "NX");
|
|
132
201
|
return result === "OK";
|
|
133
202
|
}
|
|
134
|
-
/**
|
|
135
|
-
* Atomically SET a cache key + SADD into an index SET + EXPIRE the index
|
|
136
|
-
* via a single Redis pipeline.
|
|
137
|
-
*/
|
|
138
203
|
async setWithIndex(key, value, ttlSeconds, indexKey) {
|
|
139
204
|
const pipeline = this.client.pipeline();
|
|
140
205
|
pipeline.set(key, this.encode(value), "EX", ttlSeconds);
|
|
@@ -142,9 +207,6 @@ var RedisCacheAdapter = class {
|
|
|
142
207
|
pipeline.expire(indexKey, ttlSeconds + INDEX_TTL_BUFFER);
|
|
143
208
|
await pipeline.exec();
|
|
144
209
|
}
|
|
145
|
-
/**
|
|
146
|
-
* Atomic invalidation via Lua (SMEMBERS + DEL members + DEL index).
|
|
147
|
-
*/
|
|
148
210
|
async invalidateByIndex(indexKey) {
|
|
149
211
|
await this.client.eval(INVALIDATE_BY_INDEX_LUA, 1, indexKey);
|
|
150
212
|
}
|
|
@@ -155,15 +217,11 @@ var RedisCacheAdapter = class {
|
|
|
155
217
|
pipeline.expire(key, ttlSeconds + INDEX_TTL_BUFFER);
|
|
156
218
|
await pipeline.exec();
|
|
157
219
|
}
|
|
158
|
-
// --- safe wrappers (never throw — warn + return fallback) ---
|
|
159
220
|
async safeGet(key) {
|
|
160
221
|
try {
|
|
161
222
|
return await this.get(key);
|
|
162
223
|
} catch (err) {
|
|
163
|
-
|
|
164
|
-
`[RedisCacheAdapter] safeGet failed for key=${key}`,
|
|
165
|
-
err.message
|
|
166
|
-
);
|
|
224
|
+
this.report(`safeGet key=${key}`, err);
|
|
167
225
|
return null;
|
|
168
226
|
}
|
|
169
227
|
}
|
|
@@ -171,30 +229,21 @@ var RedisCacheAdapter = class {
|
|
|
171
229
|
try {
|
|
172
230
|
await this.set(key, value, ttlSeconds);
|
|
173
231
|
} catch (err) {
|
|
174
|
-
|
|
175
|
-
`[RedisCacheAdapter] safeSet failed for key=${key}`,
|
|
176
|
-
err.message
|
|
177
|
-
);
|
|
232
|
+
this.report(`safeSet key=${key}`, err);
|
|
178
233
|
}
|
|
179
234
|
}
|
|
180
235
|
async safeDel(...keys) {
|
|
181
236
|
try {
|
|
182
237
|
await this.del(...keys);
|
|
183
238
|
} catch (err) {
|
|
184
|
-
|
|
185
|
-
"[RedisCacheAdapter] safeDel failed",
|
|
186
|
-
err.message
|
|
187
|
-
);
|
|
239
|
+
this.report("safeDel", err);
|
|
188
240
|
}
|
|
189
241
|
}
|
|
190
242
|
async safeSetNx(key, ttlSeconds) {
|
|
191
243
|
try {
|
|
192
244
|
return await this.setNx(key, ttlSeconds);
|
|
193
245
|
} catch (err) {
|
|
194
|
-
|
|
195
|
-
`[RedisCacheAdapter] safeSetNx failed for key=${key}`,
|
|
196
|
-
err.message
|
|
197
|
-
);
|
|
246
|
+
this.report(`safeSetNx key=${key}`, err);
|
|
198
247
|
return false;
|
|
199
248
|
}
|
|
200
249
|
}
|
|
@@ -202,40 +251,28 @@ var RedisCacheAdapter = class {
|
|
|
202
251
|
try {
|
|
203
252
|
await this.setWithIndex(key, value, ttlSeconds, indexKey);
|
|
204
253
|
} catch (err) {
|
|
205
|
-
|
|
206
|
-
`[RedisCacheAdapter] safeSetWithIndex failed for key=${key}`,
|
|
207
|
-
err.message
|
|
208
|
-
);
|
|
254
|
+
this.report(`safeSetWithIndex key=${key}`, err);
|
|
209
255
|
}
|
|
210
256
|
}
|
|
211
257
|
async safeInvalidateByIndex(indexKey) {
|
|
212
258
|
try {
|
|
213
259
|
await this.invalidateByIndex(indexKey);
|
|
214
260
|
} catch (err) {
|
|
215
|
-
|
|
216
|
-
`[RedisCacheAdapter] safeInvalidateByIndex failed for idx=${indexKey}`,
|
|
217
|
-
err.message
|
|
218
|
-
);
|
|
261
|
+
this.report(`safeInvalidateByIndex idx=${indexKey}`, err);
|
|
219
262
|
}
|
|
220
263
|
}
|
|
221
264
|
async safeSaddAndExpire(key, members, ttlSeconds) {
|
|
222
265
|
try {
|
|
223
266
|
await this.saddAndExpire(key, members, ttlSeconds);
|
|
224
267
|
} catch (err) {
|
|
225
|
-
|
|
226
|
-
`[RedisCacheAdapter] safeSaddAndExpire failed for key=${key}`,
|
|
227
|
-
err.message
|
|
228
|
-
);
|
|
268
|
+
this.report(`safeSaddAndExpire key=${key}`, err);
|
|
229
269
|
}
|
|
230
270
|
}
|
|
231
271
|
async safeSmembers(key) {
|
|
232
272
|
try {
|
|
233
273
|
return await this.smembers(key);
|
|
234
274
|
} catch (err) {
|
|
235
|
-
|
|
236
|
-
`[RedisCacheAdapter] safeSmembers failed for key=${key}`,
|
|
237
|
-
err.message
|
|
238
|
-
);
|
|
275
|
+
this.report(`safeSmembers key=${key}`, err);
|
|
239
276
|
return [];
|
|
240
277
|
}
|
|
241
278
|
}
|
|
@@ -250,6 +287,8 @@ import {
|
|
|
250
287
|
export {
|
|
251
288
|
RedisCacheAdapter,
|
|
252
289
|
cacheDebugStorage,
|
|
290
|
+
cloneWithCodec,
|
|
291
|
+
createRedisJsonReviver,
|
|
253
292
|
isCacheDebugEnabled,
|
|
254
293
|
recordCacheDebug,
|
|
255
294
|
redisJsonParse,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/redis-cache-adapter.ts","../src/redis-json.ts","../src/index.ts"],"sourcesContent":["import Redis from 'ioredis';\nimport { gzipSync, gunzipSync } from 'zlib';\nimport type { CacheAdapter } from '@prismakit/core';\nimport { redisJsonParse, redisJsonStringify } from './redis-json';\n\nconst INDEX_TTL_BUFFER = 60;\n\n/** Lua: atomically SMEMBERS index + DEL members + index. */\nconst INVALIDATE_BY_INDEX_LUA = `\nlocal members = redis.call('SMEMBERS', KEYS[1])\nfor i, member in ipairs(members) do\n redis.call('DEL', member)\nend\nredis.call('DEL', KEYS[1])\nreturn #members\n`;\n\nexport type RedisCompression = 'none' | 'gzip';\n\nexport type RedisCacheAdapterOptions = {\n url?: string;\n host?: string;\n port?: number;\n prefix?: string;\n /**\n * Compress payloads larger than `compressionThresholdBytes` (default 1024).\n * Uses gzip (widely available; zstd/lz4 can be added later).\n */\n compression?: RedisCompression;\n /** Minimum payload size (bytes) before compression (default 1024). */\n compressionThresholdBytes?: number;\n};\n\nconst COMPRESSED_PREFIX = 'gz:';\n\n/**\n * Redis-backed {@link CacheAdapter}. Framework-agnostic — no NestJS / ConfigService.\n */\nexport class RedisCacheAdapter implements CacheAdapter {\n private readonly client: Redis;\n private readonly prefix: string;\n private ready = false;\n private readonly compression: RedisCompression;\n private readonly compressionThreshold: number;\n\n constructor(options: RedisCacheAdapterOptions = {}) {\n const {\n url,\n host = 'localhost',\n port = 6379,\n prefix = 'prismakit',\n compression = 'none',\n compressionThresholdBytes = 1024,\n } = options;\n\n this.prefix = prefix;\n this.compression = compression;\n this.compressionThreshold = compressionThresholdBytes;\n this.client = url\n ? new Redis(url, { lazyConnect: true })\n : new Redis({ host, port, lazyConnect: true });\n\n this.client.on('ready', () => {\n this.ready = true;\n });\n this.client.on('error', (err) => {\n this.ready = false;\n console.warn('[RedisCacheAdapter] connection error', err.message);\n });\n this.client.on('close', () => {\n this.ready = false;\n });\n\n void this.connect();\n }\n\n async connect(): Promise<void> {\n if (this.client.status === 'ready' || this.client.status === 'connecting') {\n return;\n }\n try {\n await this.client.connect();\n } catch (err) {\n console.warn(\n '[RedisCacheAdapter] failed to connect',\n (err as Error).message,\n );\n }\n }\n\n async disconnect(): Promise<void> {\n await this.client?.quit();\n this.ready = false;\n }\n\n isReady(): boolean {\n return this.ready;\n }\n\n getPrefix(): string {\n return this.prefix;\n }\n\n private encode(value: unknown): string {\n const json = redisJsonStringify(value);\n if (\n this.compression === 'gzip' &&\n Buffer.byteLength(json, 'utf8') >= this.compressionThreshold\n ) {\n const compressed = gzipSync(Buffer.from(json, 'utf8')).toString('base64');\n return COMPRESSED_PREFIX + compressed;\n }\n return json;\n }\n\n private decode<T>(raw: string): T {\n if (raw.startsWith(COMPRESSED_PREFIX)) {\n const buf = Buffer.from(raw.slice(COMPRESSED_PREFIX.length), 'base64');\n const json = gunzipSync(buf).toString('utf8');\n return redisJsonParse<T>(json);\n }\n return redisJsonParse<T>(raw);\n }\n\n // --- low-level ops (may throw) ---\n\n async get<T>(key: string): Promise<T | null> {\n const raw = await this.client.get(key);\n if (raw === null) return null;\n return this.decode<T>(raw);\n }\n\n async set(key: string, value: unknown, ttlSeconds: number): Promise<void> {\n await this.client.set(key, this.encode(value), 'EX', ttlSeconds);\n }\n\n async del(...keys: string[]): Promise<void> {\n if (keys.length === 0) return;\n await this.client.del(...keys);\n }\n\n async sadd(key: string, ...members: string[]): Promise<void> {\n if (members.length === 0) return;\n await this.client.sadd(key, ...members);\n }\n\n async smembers(key: string): Promise<string[]> {\n return this.client.smembers(key);\n }\n\n async setNx(key: string, ttlSeconds: number): Promise<boolean> {\n const result = await this.client.set(key, '1', 'EX', ttlSeconds, 'NX');\n return result === 'OK';\n }\n\n /**\n * Atomically SET a cache key + SADD into an index SET + EXPIRE the index\n * via a single Redis pipeline.\n */\n async setWithIndex(\n key: string,\n value: unknown,\n ttlSeconds: number,\n indexKey: string,\n ): Promise<void> {\n const pipeline = this.client.pipeline();\n pipeline.set(key, this.encode(value), 'EX', ttlSeconds);\n pipeline.sadd(indexKey, key);\n pipeline.expire(indexKey, ttlSeconds + INDEX_TTL_BUFFER);\n await pipeline.exec();\n }\n\n /**\n * Atomic invalidation via Lua (SMEMBERS + DEL members + DEL index).\n */\n async invalidateByIndex(indexKey: string): Promise<void> {\n await this.client.eval(INVALIDATE_BY_INDEX_LUA, 1, indexKey);\n }\n\n async saddAndExpire(\n key: string,\n members: string[],\n ttlSeconds: number,\n ): Promise<void> {\n if (members.length === 0) return;\n const pipeline = this.client.pipeline();\n pipeline.sadd(key, ...members);\n pipeline.expire(key, ttlSeconds + INDEX_TTL_BUFFER);\n await pipeline.exec();\n }\n\n // --- safe wrappers (never throw — warn + return fallback) ---\n\n async safeGet<T>(key: string): Promise<T | null> {\n try {\n return await this.get<T>(key);\n } catch (err) {\n console.warn(\n `[RedisCacheAdapter] safeGet failed for key=${key}`,\n (err as Error).message,\n );\n return null;\n }\n }\n\n async safeSet(\n key: string,\n value: unknown,\n ttlSeconds: number,\n ): Promise<void> {\n try {\n await this.set(key, value, ttlSeconds);\n } catch (err) {\n console.warn(\n `[RedisCacheAdapter] safeSet failed for key=${key}`,\n (err as Error).message,\n );\n }\n }\n\n async safeDel(...keys: string[]): Promise<void> {\n try {\n await this.del(...keys);\n } catch (err) {\n console.warn(\n '[RedisCacheAdapter] safeDel failed',\n (err as Error).message,\n );\n }\n }\n\n async safeSetNx(key: string, ttlSeconds: number): Promise<boolean> {\n try {\n return await this.setNx(key, ttlSeconds);\n } catch (err) {\n console.warn(\n `[RedisCacheAdapter] safeSetNx failed for key=${key}`,\n (err as Error).message,\n );\n return false;\n }\n }\n\n async safeSetWithIndex(\n key: string,\n value: unknown,\n ttlSeconds: number,\n indexKey: string,\n ): Promise<void> {\n try {\n await this.setWithIndex(key, value, ttlSeconds, indexKey);\n } catch (err) {\n console.warn(\n `[RedisCacheAdapter] safeSetWithIndex failed for key=${key}`,\n (err as Error).message,\n );\n }\n }\n\n async safeInvalidateByIndex(indexKey: string): Promise<void> {\n try {\n await this.invalidateByIndex(indexKey);\n } catch (err) {\n console.warn(\n `[RedisCacheAdapter] safeInvalidateByIndex failed for idx=${indexKey}`,\n (err as Error).message,\n );\n }\n }\n\n async safeSaddAndExpire(\n key: string,\n members: string[],\n ttlSeconds: number,\n ): Promise<void> {\n try {\n await this.saddAndExpire(key, members, ttlSeconds);\n } catch (err) {\n console.warn(\n `[RedisCacheAdapter] safeSaddAndExpire failed for key=${key}`,\n (err as Error).message,\n );\n }\n }\n\n async safeSmembers(key: string): Promise<string[]> {\n try {\n return await this.smembers(key);\n } catch (err) {\n console.warn(\n `[RedisCacheAdapter] safeSmembers failed for key=${key}`,\n (err as Error).message,\n );\n return [];\n }\n }\n}\n","const BIGINT_TAG = '__bigint';\n\n/** JSON.stringify replacer — Prisma `BigInt` fields are not JSON-serializable by default. */\nexport function redisJsonReplacer(_key: string, value: unknown): unknown {\n if (typeof value === 'bigint') {\n return { [BIGINT_TAG]: value.toString() };\n }\n return value;\n}\n\n/** JSON.parse reviver — restore tagged BigInt payloads from {@link redisJsonReplacer}. */\nexport function redisJsonReviver(_key: string, value: unknown): unknown {\n if (\n value !== null &&\n typeof value === 'object' &&\n !Array.isArray(value) &&\n Object.keys(value).length === 1 &&\n BIGINT_TAG in value\n ) {\n const raw = (value as Record<string, unknown>)[BIGINT_TAG];\n if (typeof raw === 'string') {\n return BigInt(raw);\n }\n }\n return value;\n}\n\nexport function redisJsonStringify(value: unknown): string {\n return JSON.stringify(value, redisJsonReplacer);\n}\n\nexport function redisJsonParse<T>(raw: string): T {\n return JSON.parse(raw, redisJsonReviver) as T;\n}\n","export {\n RedisCacheAdapter,\n type RedisCacheAdapterOptions,\n type RedisCompression,\n} from './redis-cache-adapter';\n\nexport {\n redisJsonParse,\n redisJsonStringify,\n redisJsonReplacer,\n redisJsonReviver,\n} from './redis-json';\n\n/** Re-export cache debug helpers from core — do not duplicate. */\nexport {\n type CacheDebugStatus,\n cacheDebugStorage,\n isCacheDebugEnabled,\n recordCacheDebug,\n} from '@prismakit/core';\n\nexport type { CacheAdapter } from '@prismakit/core';\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/redis-cache-adapter.ts","../src/redis-json.ts","../src/index.ts"],"sourcesContent":["import Redis from 'ioredis';\nimport { gzipSync, gunzipSync } from 'zlib';\nimport type { CacheAdapter } from '@prismakit/core';\nimport {\n redisJsonParse,\n redisJsonStringify,\n type DecimalFactory,\n type RedisJsonOptions,\n} from './redis-json';\n\nconst INDEX_TTL_BUFFER = 60;\n\n/** Lua: atomically SMEMBERS index + DEL members + index. */\nconst INVALIDATE_BY_INDEX_LUA = `\nlocal members = redis.call('SMEMBERS', KEYS[1])\nfor i, member in ipairs(members) do\n redis.call('DEL', member)\nend\nredis.call('DEL', KEYS[1])\nreturn #members\n`;\n\nexport type RedisCompression = 'none' | 'gzip';\n\nexport type RedisCacheAdapterOptions = {\n url?: string;\n host?: string;\n port?: number;\n prefix?: string;\n /**\n * Compress payloads larger than `compressionThresholdBytes` (default 1024).\n * Uses gzip (widely available; zstd/lz4 can be added later).\n */\n compression?: RedisCompression;\n /** Minimum payload size (bytes) before compression (default 1024). */\n compressionThresholdBytes?: number;\n /**\n * Reconstruct Prisma Decimal from tagged cache payloads.\n * @example decimalFactory: (s) => new Prisma.Decimal(s)\n */\n decimalFactory?: DecimalFactory;\n /** Optional error hook for safe* wrappers (also used by telemetry). */\n onError?: (err: unknown, op?: string) => void;\n};\n\nconst COMPRESSED_PREFIX = 'gz:';\n\n/**\n * Redis-backed {@link CacheAdapter}. Framework-agnostic — no NestJS / ConfigService.\n */\nexport class RedisCacheAdapter implements CacheAdapter {\n private readonly client: Redis;\n private readonly prefix: string;\n private ready = false;\n private readonly compression: RedisCompression;\n private readonly compressionThreshold: number;\n private readonly jsonOptions: RedisJsonOptions;\n onError?: (err: unknown, op?: string) => void;\n\n constructor(options: RedisCacheAdapterOptions = {}) {\n const {\n url,\n host = 'localhost',\n port = 6379,\n prefix = 'prismakit',\n compression = 'none',\n compressionThresholdBytes = 1024,\n decimalFactory,\n onError,\n } = options;\n\n this.prefix = prefix;\n this.compression = compression;\n this.compressionThreshold = compressionThresholdBytes;\n this.jsonOptions = { decimalFactory };\n this.onError = onError;\n this.client = url\n ? new Redis(url, { lazyConnect: true })\n : new Redis({ host, port, lazyConnect: true });\n\n this.client.on('ready', () => {\n this.ready = true;\n });\n this.client.on('error', (err) => {\n this.ready = false;\n console.warn('[RedisCacheAdapter] connection error', err.message);\n });\n this.client.on('close', () => {\n this.ready = false;\n });\n\n void this.connect();\n }\n\n private report(op: string, err: unknown): void {\n console.warn(\n `[RedisCacheAdapter] ${op} failed`,\n (err as Error)?.message ?? err,\n );\n try {\n this.onError?.(err, op);\n } catch {\n /* ignore hook errors */\n }\n }\n\n async connect(): Promise<void> {\n if (this.client.status === 'ready' || this.client.status === 'connecting') {\n return;\n }\n try {\n await this.client.connect();\n } catch (err) {\n this.report('connect', err);\n }\n }\n\n async disconnect(): Promise<void> {\n await this.client?.quit();\n this.ready = false;\n }\n\n isReady(): boolean {\n return this.ready;\n }\n\n getPrefix(): string {\n return this.prefix;\n }\n\n private encode(value: unknown): string {\n const json = redisJsonStringify(value);\n if (\n this.compression === 'gzip' &&\n Buffer.byteLength(json, 'utf8') >= this.compressionThreshold\n ) {\n const compressed = gzipSync(Buffer.from(json, 'utf8')).toString('base64');\n return COMPRESSED_PREFIX + compressed;\n }\n return json;\n }\n\n private decode<T>(raw: string): T {\n if (raw.startsWith(COMPRESSED_PREFIX)) {\n const buf = Buffer.from(raw.slice(COMPRESSED_PREFIX.length), 'base64');\n const json = gunzipSync(buf).toString('utf8');\n return redisJsonParse<T>(json, this.jsonOptions);\n }\n return redisJsonParse<T>(raw, this.jsonOptions);\n }\n\n async get<T>(key: string): Promise<T | null> {\n const raw = await this.client.get(key);\n if (raw === null) return null;\n return this.decode<T>(raw);\n }\n\n async set(key: string, value: unknown, ttlSeconds: number): Promise<void> {\n await this.client.set(key, this.encode(value), 'EX', ttlSeconds);\n }\n\n async del(...keys: string[]): Promise<void> {\n if (keys.length === 0) return;\n await this.client.del(...keys);\n }\n\n async sadd(key: string, ...members: string[]): Promise<void> {\n if (members.length === 0) return;\n await this.client.sadd(key, ...members);\n }\n\n async smembers(key: string): Promise<string[]> {\n return this.client.smembers(key);\n }\n\n async setNx(key: string, ttlSeconds: number): Promise<boolean> {\n const result = await this.client.set(key, '1', 'EX', ttlSeconds, 'NX');\n return result === 'OK';\n }\n\n async setWithIndex(\n key: string,\n value: unknown,\n ttlSeconds: number,\n indexKey: string,\n ): Promise<void> {\n const pipeline = this.client.pipeline();\n pipeline.set(key, this.encode(value), 'EX', ttlSeconds);\n pipeline.sadd(indexKey, key);\n pipeline.expire(indexKey, ttlSeconds + INDEX_TTL_BUFFER);\n await pipeline.exec();\n }\n\n async invalidateByIndex(indexKey: string): Promise<void> {\n await this.client.eval(INVALIDATE_BY_INDEX_LUA, 1, indexKey);\n }\n\n async saddAndExpire(\n key: string,\n members: string[],\n ttlSeconds: number,\n ): Promise<void> {\n if (members.length === 0) return;\n const pipeline = this.client.pipeline();\n pipeline.sadd(key, ...members);\n pipeline.expire(key, ttlSeconds + INDEX_TTL_BUFFER);\n await pipeline.exec();\n }\n\n async safeGet<T>(key: string): Promise<T | null> {\n try {\n return await this.get<T>(key);\n } catch (err) {\n this.report(`safeGet key=${key}`, err);\n return null;\n }\n }\n\n async safeSet(\n key: string,\n value: unknown,\n ttlSeconds: number,\n ): Promise<void> {\n try {\n await this.set(key, value, ttlSeconds);\n } catch (err) {\n this.report(`safeSet key=${key}`, err);\n }\n }\n\n async safeDel(...keys: string[]): Promise<void> {\n try {\n await this.del(...keys);\n } catch (err) {\n this.report('safeDel', err);\n }\n }\n\n async safeSetNx(key: string, ttlSeconds: number): Promise<boolean> {\n try {\n return await this.setNx(key, ttlSeconds);\n } catch (err) {\n this.report(`safeSetNx key=${key}`, err);\n return false;\n }\n }\n\n async safeSetWithIndex(\n key: string,\n value: unknown,\n ttlSeconds: number,\n indexKey: string,\n ): Promise<void> {\n try {\n await this.setWithIndex(key, value, ttlSeconds, indexKey);\n } catch (err) {\n this.report(`safeSetWithIndex key=${key}`, err);\n }\n }\n\n async safeInvalidateByIndex(indexKey: string): Promise<void> {\n try {\n await this.invalidateByIndex(indexKey);\n } catch (err) {\n this.report(`safeInvalidateByIndex idx=${indexKey}`, err);\n }\n }\n\n async safeSaddAndExpire(\n key: string,\n members: string[],\n ttlSeconds: number,\n ): Promise<void> {\n try {\n await this.saddAndExpire(key, members, ttlSeconds);\n } catch (err) {\n this.report(`safeSaddAndExpire key=${key}`, err);\n }\n }\n\n async safeSmembers(key: string): Promise<string[]> {\n try {\n return await this.smembers(key);\n } catch (err) {\n this.report(`safeSmembers key=${key}`, err);\n return [];\n }\n }\n}\n","const BIGINT_TAG = '__bigint';\nconst DATE_TAG = '__date';\nconst BYTES_TAG = '__bytes';\nconst DECIMAL_TAG = '__decimal';\n\nexport type DecimalFactory = (value: string) => unknown;\n\nexport type RedisJsonOptions = {\n /**\n * Reconstruct Prisma `Decimal` (or equivalent) from the tagged string.\n * Default keeps the precision-preserving string so values are not silently\n * coerced to numbers.\n *\n * @example\n * decimalFactory: (s) => new Prisma.Decimal(s)\n */\n decimalFactory?: DecimalFactory;\n};\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return (\n value !== null &&\n typeof value === 'object' &&\n !Array.isArray(value) &&\n !(value instanceof Date) &&\n !Buffer.isBuffer(value)\n );\n}\n\nfunction isDecimalLike(value: unknown): value is { toFixed: () => string } {\n return (\n isPlainObject(value) &&\n typeof (value as { constructor?: { name?: string } }).constructor?.name ===\n 'string' &&\n ((value as { constructor: { name: string } }).constructor.name ===\n 'Decimal' ||\n (typeof (value as { toFixed?: unknown }).toFixed === 'function' &&\n typeof (value as { d?: unknown }).d === 'object'))\n );\n}\n\n/** JSON.stringify replacer — Prisma BigInt / Date / Bytes / Decimal. */\nexport function redisJsonReplacer(_key: string, value: unknown): unknown {\n if (typeof value === 'bigint') {\n return { [BIGINT_TAG]: value.toString() };\n }\n if (value instanceof Date) {\n return { [DATE_TAG]: value.toISOString() };\n }\n if (Buffer.isBuffer(value)) {\n return { [BYTES_TAG]: value.toString('base64') };\n }\n if (\n value &&\n typeof value === 'object' &&\n (value as { type?: string }).type === 'Buffer' &&\n Array.isArray((value as { data?: unknown }).data)\n ) {\n return {\n [BYTES_TAG]: Buffer.from(\n (value as { data: number[] }).data,\n ).toString('base64'),\n };\n }\n if (isDecimalLike(value)) {\n return { [DECIMAL_TAG]: value.toFixed() };\n }\n return value;\n}\n\n/** JSON.parse reviver — restore tagged payloads from {@link redisJsonReplacer}. */\nexport function createRedisJsonReviver(options: RedisJsonOptions = {}) {\n const decimalFactory = options.decimalFactory ?? ((s: string) => s);\n return (_key: string, value: unknown): unknown => {\n if (!isPlainObject(value)) return value;\n const keys = Object.keys(value);\n if (keys.length !== 1) return value;\n\n if (BIGINT_TAG in value && typeof value[BIGINT_TAG] === 'string') {\n return BigInt(value[BIGINT_TAG] as string);\n }\n if (DATE_TAG in value && typeof value[DATE_TAG] === 'string') {\n return new Date(value[DATE_TAG] as string);\n }\n if (BYTES_TAG in value && typeof value[BYTES_TAG] === 'string') {\n return Buffer.from(value[BYTES_TAG] as string, 'base64');\n }\n if (DECIMAL_TAG in value && typeof value[DECIMAL_TAG] === 'string') {\n return decimalFactory(value[DECIMAL_TAG] as string);\n }\n return value;\n };\n}\n\n/** @deprecated Prefer {@link createRedisJsonReviver} when a decimalFactory is needed. */\nexport function redisJsonReviver(_key: string, value: unknown): unknown {\n return createRedisJsonReviver()(_key, value);\n}\n\nexport function redisJsonStringify(value: unknown): string {\n return JSON.stringify(value, function (key, val) {\n const holder = this as Record<string, unknown>;\n const raw = key === '' ? value : holder[key];\n if (raw instanceof Date) {\n return redisJsonReplacer(key, raw);\n }\n return redisJsonReplacer(key, val);\n });\n}\n\nexport function redisJsonParse<T>(\n raw: string,\n options?: RedisJsonOptions,\n): T {\n return JSON.parse(raw, createRedisJsonReviver(options)) as T;\n}\n\n/**\n * Deep clone via structuredClone, falling back to the tagged JSON codec so\n * Date / BigInt / Buffer / Decimal survive (unlike plain JSON.stringify).\n */\nexport function cloneWithCodec<T>(value: T, options?: RedisJsonOptions): T {\n if (value === null || value === undefined) return value;\n if (typeof structuredClone === 'function') {\n try {\n return structuredClone(value);\n } catch {\n // Fall through for non-cloneable values (e.g. Decimal)\n }\n }\n return redisJsonParse(redisJsonStringify(value), options);\n}\n","export {\n RedisCacheAdapter,\n type RedisCacheAdapterOptions,\n type RedisCompression,\n} from './redis-cache-adapter';\n\nexport {\n redisJsonParse,\n redisJsonStringify,\n redisJsonReplacer,\n redisJsonReviver,\n createRedisJsonReviver,\n cloneWithCodec,\n type DecimalFactory,\n type RedisJsonOptions,\n} from './redis-json';\n\n/** Re-export cache debug helpers from core — do not duplicate. */\nexport {\n type CacheDebugStatus,\n cacheDebugStorage,\n isCacheDebugEnabled,\n recordCacheDebug,\n} from '@prismakit/core';\n\nexport type { CacheAdapter } from '@prismakit/core';\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prismakit/redis",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Redis CacheAdapter for @prismakit/core",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"engines": {
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
"README.md"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@prismakit/core": "3.0
|
|
27
|
+
"@prismakit/core": "3.2.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@prismakit/core": ">=3.0
|
|
30
|
+
"@prismakit/core": ">=3.2.0 <4",
|
|
31
31
|
"ioredis": ">=5.0.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { afterAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { RedisCacheAdapter } from '../redis-cache-adapter';
|
|
3
|
+
|
|
4
|
+
function requireRedisUrl(): string | undefined {
|
|
5
|
+
const value = process.env.REDIS_URL;
|
|
6
|
+
if (value) return value;
|
|
7
|
+
if (process.env.FORCE_INTEGRATION === '1' || process.env.CI === 'true') {
|
|
8
|
+
throw new Error(
|
|
9
|
+
'[PrismaKit] REDIS_URL is required for integration tests when CI=true or FORCE_INTEGRATION=1',
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const redisUrl = requireRedisUrl();
|
|
16
|
+
|
|
17
|
+
describe.skipIf(!redisUrl)('RedisCacheAdapter (integration)', () => {
|
|
18
|
+
const prefix = `pk-it-${Date.now()}`;
|
|
19
|
+
const adapter = new RedisCacheAdapter({
|
|
20
|
+
url: redisUrl,
|
|
21
|
+
prefix,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterAll(async () => {
|
|
25
|
+
await adapter.disconnect();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('round-trips Date and BigInt via set/get', async () => {
|
|
29
|
+
const key = `${prefix}:roundtrip`;
|
|
30
|
+
const createdAt = new Date('2026-01-15T12:00:00.000Z');
|
|
31
|
+
const payload = { id: '1', qty: 9007199254740993n, createdAt };
|
|
32
|
+
|
|
33
|
+
await adapter.set(key, payload, 60);
|
|
34
|
+
await expect(adapter.get<typeof payload>(key)).resolves.toEqual(payload);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('setWithIndex and invalidateByIndex clear indexed keys', async () => {
|
|
38
|
+
const indexKey = `${prefix}:idx:users`;
|
|
39
|
+
const entityKey = `${prefix}:entity:1`;
|
|
40
|
+
await adapter.setWithIndex(entityKey, { id: '1' }, 60, indexKey);
|
|
41
|
+
await expect(adapter.get(entityKey)).resolves.toEqual({ id: '1' });
|
|
42
|
+
|
|
43
|
+
await adapter.invalidateByIndex(indexKey);
|
|
44
|
+
await expect(adapter.get(entityKey)).resolves.toBeNull();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -55,6 +55,20 @@ describe('RedisCacheAdapter', () => {
|
|
|
55
55
|
expect(parsed.nested.n).toBe(1n);
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
it('round-trips Date and Buffer via redisJson helpers', async () => {
|
|
59
|
+
const { redisJsonParse, redisJsonStringify } = await import('../redis-json');
|
|
60
|
+
const createdAt = new Date('2026-01-15T12:00:00.000Z');
|
|
61
|
+
const blob = Buffer.from('hello');
|
|
62
|
+
const payload = { createdAt, blob };
|
|
63
|
+
const raw = redisJsonStringify(payload);
|
|
64
|
+
expect(raw).toContain('__date');
|
|
65
|
+
expect(raw).toContain('__bytes');
|
|
66
|
+
const parsed = redisJsonParse<typeof payload>(raw);
|
|
67
|
+
expect(parsed.createdAt).toEqual(createdAt);
|
|
68
|
+
expect(Buffer.isBuffer(parsed.blob)).toBe(true);
|
|
69
|
+
expect(parsed.blob.toString()).toBe('hello');
|
|
70
|
+
});
|
|
71
|
+
|
|
58
72
|
it('set/get uses BigInt-safe JSON', async () => {
|
|
59
73
|
const { RedisCacheAdapter } = await import('../redis-cache-adapter');
|
|
60
74
|
let stored: string | undefined;
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,10 @@ export {
|
|
|
9
9
|
redisJsonStringify,
|
|
10
10
|
redisJsonReplacer,
|
|
11
11
|
redisJsonReviver,
|
|
12
|
+
createRedisJsonReviver,
|
|
13
|
+
cloneWithCodec,
|
|
14
|
+
type DecimalFactory,
|
|
15
|
+
type RedisJsonOptions,
|
|
12
16
|
} from './redis-json';
|
|
13
17
|
|
|
14
18
|
/** Re-export cache debug helpers from core — do not duplicate. */
|