@prismakit/redis 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 redisJsonReviver(_key, value) {
14
- if (value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 1 && BIGINT_TAG in value) {
15
- const raw = value[BIGINT_TAG];
16
- if (typeof raw === "string") {
17
- return BigInt(raw);
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
- return value;
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, redisJsonReplacer);
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, redisJsonReviver);
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
- console.warn(
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
- console.warn(
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
- console.warn(
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
- console.warn(
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
- console.warn(
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
- console.warn(
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
- console.warn(
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
- console.warn(
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
- console.warn(
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":";AAAA,OAAO,WAAW;AAClB,SAAS,UAAU,kBAAkB;;;ACDrC,IAAM,aAAa;AACnB,IAAM,WAAW;AACjB,IAAM,YAAY;AAClB,IAAM,cAAc;AAgBpB,SAAS,cAAc,OAAkD;AACvE,SACE,UAAU,QACV,OAAO,UAAU,YACjB,CAAC,MAAM,QAAQ,KAAK,KACpB,EAAE,iBAAiB,SACnB,CAAC,OAAO,SAAS,KAAK;AAE1B;AAEA,SAAS,cAAc,OAAoD;AACzE,SACE,cAAc,KAAK,KACnB,OAAQ,MAA8C,aAAa,SACjE,aACA,MAA4C,YAAY,SACxD,aACC,OAAQ,MAAgC,YAAY,cACnD,OAAQ,MAA0B,MAAM;AAEhD;AAGO,SAAS,kBAAkB,MAAc,OAAyB;AACvE,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,EAAE,CAAC,UAAU,GAAG,MAAM,SAAS,EAAE;AAAA,EAC1C;AACA,MAAI,iBAAiB,MAAM;AACzB,WAAO,EAAE,CAAC,QAAQ,GAAG,MAAM,YAAY,EAAE;AAAA,EAC3C;AACA,MAAI,OAAO,SAAS,KAAK,GAAG;AAC1B,WAAO,EAAE,CAAC,SAAS,GAAG,MAAM,SAAS,QAAQ,EAAE;AAAA,EACjD;AACA,MACE,SACA,OAAO,UAAU,YAChB,MAA4B,SAAS,YACtC,MAAM,QAAS,MAA6B,IAAI,GAChD;AACA,WAAO;AAAA,MACL,CAAC,SAAS,GAAG,OAAO;AAAA,QACjB,MAA6B;AAAA,MAChC,EAAE,SAAS,QAAQ;AAAA,IACrB;AAAA,EACF;AACA,MAAI,cAAc,KAAK,GAAG;AACxB,WAAO,EAAE,CAAC,WAAW,GAAG,MAAM,QAAQ,EAAE;AAAA,EAC1C;AACA,SAAO;AACT;AAGO,SAAS,uBAAuB,UAA4B,CAAC,GAAG;AACrE,QAAM,iBAAiB,QAAQ,mBAAmB,CAAC,MAAc;AACjE,SAAO,CAAC,MAAc,UAA4B;AAChD,QAAI,CAAC,cAAc,KAAK,EAAG,QAAO;AAClC,UAAM,OAAO,OAAO,KAAK,KAAK;AAC9B,QAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,QAAI,cAAc,SAAS,OAAO,MAAM,UAAU,MAAM,UAAU;AAChE,aAAO,OAAO,MAAM,UAAU,CAAW;AAAA,IAC3C;AACA,QAAI,YAAY,SAAS,OAAO,MAAM,QAAQ,MAAM,UAAU;AAC5D,aAAO,IAAI,KAAK,MAAM,QAAQ,CAAW;AAAA,IAC3C;AACA,QAAI,aAAa,SAAS,OAAO,MAAM,SAAS,MAAM,UAAU;AAC9D,aAAO,OAAO,KAAK,MAAM,SAAS,GAAa,QAAQ;AAAA,IACzD;AACA,QAAI,eAAe,SAAS,OAAO,MAAM,WAAW,MAAM,UAAU;AAClE,aAAO,eAAe,MAAM,WAAW,CAAW;AAAA,IACpD;AACA,WAAO;AAAA,EACT;AACF;AAGO,SAAS,iBAAiB,MAAc,OAAyB;AACtE,SAAO,uBAAuB,EAAE,MAAM,KAAK;AAC7C;AAEO,SAAS,mBAAmB,OAAwB;AACzD,SAAO,KAAK,UAAU,OAAO,SAAU,KAAK,KAAK;AAC/C,UAAM,SAAS;AACf,UAAM,MAAM,QAAQ,KAAK,QAAQ,OAAO,GAAG;AAC3C,QAAI,eAAe,MAAM;AACvB,aAAO,kBAAkB,KAAK,GAAG;AAAA,IACnC;AACA,WAAO,kBAAkB,KAAK,GAAG;AAAA,EACnC,CAAC;AACH;AAEO,SAAS,eACd,KACA,SACG;AACH,SAAO,KAAK,MAAM,KAAK,uBAAuB,OAAO,CAAC;AACxD;AAMO,SAAS,eAAkB,OAAU,SAA+B;AACzE,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,MAAI,OAAO,oBAAoB,YAAY;AACzC,QAAI;AACF,aAAO,gBAAgB,KAAK;AAAA,IAC9B,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO,eAAe,mBAAmB,KAAK,GAAG,OAAO;AAC1D;;;ADzHA,IAAM,mBAAmB;AAGzB,IAAM,0BAA0B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgChC,IAAM,oBAAoB;AAKnB,IAAM,oBAAN,MAAgD;AAAA,EACpC;AAAA,EACA;AAAA,EACT,QAAQ;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACjB;AAAA,EAEA,YAAY,UAAoC,CAAC,GAAG;AAClD,UAAM;AAAA,MACJ;AAAA,MACA,OAAO;AAAA,MACP,OAAO;AAAA,MACP,SAAS;AAAA,MACT,cAAc;AAAA,MACd,4BAA4B;AAAA,MAC5B;AAAA,MACA;AAAA,IACF,IAAI;AAEJ,SAAK,SAAS;AACd,SAAK,cAAc;AACnB,SAAK,uBAAuB;AAC5B,SAAK,cAAc,EAAE,eAAe;AACpC,SAAK,UAAU;AACf,SAAK,SAAS,MACV,IAAI,MAAM,KAAK,EAAE,aAAa,KAAK,CAAC,IACpC,IAAI,MAAM,EAAE,MAAM,MAAM,aAAa,KAAK,CAAC;AAE/C,SAAK,OAAO,GAAG,SAAS,MAAM;AAC5B,WAAK,QAAQ;AAAA,IACf,CAAC;AACD,SAAK,OAAO,GAAG,SAAS,CAAC,QAAQ;AAC/B,WAAK,QAAQ;AACb,cAAQ,KAAK,wCAAwC,IAAI,OAAO;AAAA,IAClE,CAAC;AACD,SAAK,OAAO,GAAG,SAAS,MAAM;AAC5B,WAAK,QAAQ;AAAA,IACf,CAAC;AAED,SAAK,KAAK,QAAQ;AAAA,EACpB;AAAA,EAEQ,OAAO,IAAY,KAAoB;AAC7C,YAAQ;AAAA,MACN,uBAAuB,EAAE;AAAA,MACxB,KAAe,WAAW;AAAA,IAC7B;AACA,QAAI;AACF,WAAK,UAAU,KAAK,EAAE;AAAA,IACxB,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEA,MAAM,UAAyB;AAC7B,QAAI,KAAK,OAAO,WAAW,WAAW,KAAK,OAAO,WAAW,cAAc;AACzE;AAAA,IACF;AACA,QAAI;AACF,YAAM,KAAK,OAAO,QAAQ;AAAA,IAC5B,SAAS,KAAK;AACZ,WAAK,OAAO,WAAW,GAAG;AAAA,IAC5B;AAAA,EACF;AAAA,EAEA,MAAM,aAA4B;AAChC,UAAM,KAAK,QAAQ,KAAK;AACxB,SAAK,QAAQ;AAAA,EACf;AAAA,EAEA,UAAmB;AACjB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,YAAoB;AAClB,WAAO,KAAK;AAAA,EACd;AAAA,EAEQ,OAAO,OAAwB;AACrC,UAAM,OAAO,mBAAmB,KAAK;AACrC,QACE,KAAK,gBAAgB,UACrB,OAAO,WAAW,MAAM,MAAM,KAAK,KAAK,sBACxC;AACA,YAAM,aAAa,SAAS,OAAO,KAAK,MAAM,MAAM,CAAC,EAAE,SAAS,QAAQ;AACxE,aAAO,oBAAoB;AAAA,IAC7B;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,OAAU,KAAgB;AAChC,QAAI,IAAI,WAAW,iBAAiB,GAAG;AACrC,YAAM,MAAM,OAAO,KAAK,IAAI,MAAM,kBAAkB,MAAM,GAAG,QAAQ;AACrE,YAAM,OAAO,WAAW,GAAG,EAAE,SAAS,MAAM;AAC5C,aAAO,eAAkB,MAAM,KAAK,WAAW;AAAA,IACjD;AACA,WAAO,eAAkB,KAAK,KAAK,WAAW;AAAA,EAChD;AAAA,EAEA,MAAM,IAAO,KAAgC;AAC3C,UAAM,MAAM,MAAM,KAAK,OAAO,IAAI,GAAG;AACrC,QAAI,QAAQ,KAAM,QAAO;AACzB,WAAO,KAAK,OAAU,GAAG;AAAA,EAC3B;AAAA,EAEA,MAAM,IAAI,KAAa,OAAgB,YAAmC;AACxE,UAAM,KAAK,OAAO,IAAI,KAAK,KAAK,OAAO,KAAK,GAAG,MAAM,UAAU;AAAA,EACjE;AAAA,EAEA,MAAM,OAAO,MAA+B;AAC1C,QAAI,KAAK,WAAW,EAAG;AACvB,UAAM,KAAK,OAAO,IAAI,GAAG,IAAI;AAAA,EAC/B;AAAA,EAEA,MAAM,KAAK,QAAgB,SAAkC;AAC3D,QAAI,QAAQ,WAAW,EAAG;AAC1B,UAAM,KAAK,OAAO,KAAK,KAAK,GAAG,OAAO;AAAA,EACxC;AAAA,EAEA,MAAM,SAAS,KAAgC;AAC7C,WAAO,KAAK,OAAO,SAAS,GAAG;AAAA,EACjC;AAAA,EAEA,MAAM,MAAM,KAAa,YAAsC;AAC7D,UAAM,SAAS,MAAM,KAAK,OAAO,IAAI,KAAK,KAAK,MAAM,YAAY,IAAI;AACrE,WAAO,WAAW;AAAA,EACpB;AAAA,EAEA,MAAM,aACJ,KACA,OACA,YACA,UACe;AACf,UAAM,WAAW,KAAK,OAAO,SAAS;AACtC,aAAS,IAAI,KAAK,KAAK,OAAO,KAAK,GAAG,MAAM,UAAU;AACtD,aAAS,KAAK,UAAU,GAAG;AAC3B,aAAS,OAAO,UAAU,aAAa,gBAAgB;AACvD,UAAM,SAAS,KAAK;AAAA,EACtB;AAAA,EAEA,MAAM,kBAAkB,UAAiC;AACvD,UAAM,KAAK,OAAO,KAAK,yBAAyB,GAAG,QAAQ;AAAA,EAC7D;AAAA,EAEA,MAAM,cACJ,KACA,SACA,YACe;AACf,QAAI,QAAQ,WAAW,EAAG;AAC1B,UAAM,WAAW,KAAK,OAAO,SAAS;AACtC,aAAS,KAAK,KAAK,GAAG,OAAO;AAC7B,aAAS,OAAO,KAAK,aAAa,gBAAgB;AAClD,UAAM,SAAS,KAAK;AAAA,EACtB;AAAA,EAEA,MAAM,QAAW,KAAgC;AAC/C,QAAI;AACF,aAAO,MAAM,KAAK,IAAO,GAAG;AAAA,IAC9B,SAAS,KAAK;AACZ,WAAK,OAAO,eAAe,GAAG,IAAI,GAAG;AACrC,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,QACJ,KACA,OACA,YACe;AACf,QAAI;AACF,YAAM,KAAK,IAAI,KAAK,OAAO,UAAU;AAAA,IACvC,SAAS,KAAK;AACZ,WAAK,OAAO,eAAe,GAAG,IAAI,GAAG;AAAA,IACvC;AAAA,EACF;AAAA,EAEA,MAAM,WAAW,MAA+B;AAC9C,QAAI;AACF,YAAM,KAAK,IAAI,GAAG,IAAI;AAAA,IACxB,SAAS,KAAK;AACZ,WAAK,OAAO,WAAW,GAAG;AAAA,IAC5B;AAAA,EACF;AAAA,EAEA,MAAM,UAAU,KAAa,YAAsC;AACjE,QAAI;AACF,aAAO,MAAM,KAAK,MAAM,KAAK,UAAU;AAAA,IACzC,SAAS,KAAK;AACZ,WAAK,OAAO,iBAAiB,GAAG,IAAI,GAAG;AACvC,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,iBACJ,KACA,OACA,YACA,UACe;AACf,QAAI;AACF,YAAM,KAAK,aAAa,KAAK,OAAO,YAAY,QAAQ;AAAA,IAC1D,SAAS,KAAK;AACZ,WAAK,OAAO,wBAAwB,GAAG,IAAI,GAAG;AAAA,IAChD;AAAA,EACF;AAAA,EAEA,MAAM,sBAAsB,UAAiC;AAC3D,QAAI;AACF,YAAM,KAAK,kBAAkB,QAAQ;AAAA,IACvC,SAAS,KAAK;AACZ,WAAK,OAAO,6BAA6B,QAAQ,IAAI,GAAG;AAAA,IAC1D;AAAA,EACF;AAAA,EAEA,MAAM,kBACJ,KACA,SACA,YACe;AACf,QAAI;AACF,YAAM,KAAK,cAAc,KAAK,SAAS,UAAU;AAAA,IACnD,SAAS,KAAK;AACZ,WAAK,OAAO,yBAAyB,GAAG,IAAI,GAAG;AAAA,IACjD;AAAA,EACF;AAAA,EAEA,MAAM,aAAa,KAAgC;AACjD,QAAI;AACF,aAAO,MAAM,KAAK,SAAS,GAAG;AAAA,IAChC,SAAS,KAAK;AACZ,WAAK,OAAO,oBAAoB,GAAG,IAAI,GAAG;AAC1C,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AACF;;;AE9QA;AAAA,EAEE;AAAA,EACA;AAAA,EACA;AAAA,OACK;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prismakit/redis",
3
- "version": "3.0.0",
3
+ "version": "3.1.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.0"
27
+ "@prismakit/core": "3.1.0"
28
28
  },
29
29
  "peerDependencies": {
30
- "@prismakit/core": ">=3.0.0",
30
+ "@prismakit/core": ">=3.1.0 <4",
31
31
  "ioredis": ">=5.0.0"
32
32
  },
33
33
  "devDependencies": {
@@ -0,0 +1,35 @@
1
+ import { afterAll, describe, expect, it } from 'vitest';
2
+ import { RedisCacheAdapter } from '../redis-cache-adapter';
3
+
4
+ const hasRedis = Boolean(process.env.REDIS_URL);
5
+
6
+ describe.skipIf(!hasRedis)('RedisCacheAdapter (integration)', () => {
7
+ const prefix = `pk-it-${Date.now()}`;
8
+ const adapter = new RedisCacheAdapter({
9
+ url: process.env.REDIS_URL,
10
+ prefix,
11
+ });
12
+
13
+ afterAll(async () => {
14
+ await adapter.disconnect();
15
+ });
16
+
17
+ it('round-trips Date and BigInt via set/get', async () => {
18
+ const key = `${prefix}:roundtrip`;
19
+ const createdAt = new Date('2026-01-15T12:00:00.000Z');
20
+ const payload = { id: '1', qty: 9007199254740993n, createdAt };
21
+
22
+ await adapter.set(key, payload, 60);
23
+ await expect(adapter.get<typeof payload>(key)).resolves.toEqual(payload);
24
+ });
25
+
26
+ it('setWithIndex and invalidateByIndex clear indexed keys', async () => {
27
+ const indexKey = `${prefix}:idx:users`;
28
+ const entityKey = `${prefix}:entity:1`;
29
+ await adapter.setWithIndex(entityKey, { id: '1' }, 60, indexKey);
30
+ await expect(adapter.get(entityKey)).resolves.toEqual({ id: '1' });
31
+
32
+ await adapter.invalidateByIndex(indexKey);
33
+ await expect(adapter.get(entityKey)).resolves.toBeNull();
34
+ });
35
+ });
@@ -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. */
@@ -1,7 +1,12 @@
1
1
  import Redis from 'ioredis';
2
2
  import { gzipSync, gunzipSync } from 'zlib';
3
3
  import type { CacheAdapter } from '@prismakit/core';
4
- import { redisJsonParse, redisJsonStringify } from './redis-json';
4
+ import {
5
+ redisJsonParse,
6
+ redisJsonStringify,
7
+ type DecimalFactory,
8
+ type RedisJsonOptions,
9
+ } from './redis-json';
5
10
 
6
11
  const INDEX_TTL_BUFFER = 60;
7
12
 
@@ -29,6 +34,13 @@ export type RedisCacheAdapterOptions = {
29
34
  compression?: RedisCompression;
30
35
  /** Minimum payload size (bytes) before compression (default 1024). */
31
36
  compressionThresholdBytes?: number;
37
+ /**
38
+ * Reconstruct Prisma Decimal from tagged cache payloads.
39
+ * @example decimalFactory: (s) => new Prisma.Decimal(s)
40
+ */
41
+ decimalFactory?: DecimalFactory;
42
+ /** Optional error hook for safe* wrappers (also used by telemetry). */
43
+ onError?: (err: unknown, op?: string) => void;
32
44
  };
33
45
 
34
46
  const COMPRESSED_PREFIX = 'gz:';
@@ -42,6 +54,8 @@ export class RedisCacheAdapter implements CacheAdapter {
42
54
  private ready = false;
43
55
  private readonly compression: RedisCompression;
44
56
  private readonly compressionThreshold: number;
57
+ private readonly jsonOptions: RedisJsonOptions;
58
+ onError?: (err: unknown, op?: string) => void;
45
59
 
46
60
  constructor(options: RedisCacheAdapterOptions = {}) {
47
61
  const {
@@ -51,11 +65,15 @@ export class RedisCacheAdapter implements CacheAdapter {
51
65
  prefix = 'prismakit',
52
66
  compression = 'none',
53
67
  compressionThresholdBytes = 1024,
68
+ decimalFactory,
69
+ onError,
54
70
  } = options;
55
71
 
56
72
  this.prefix = prefix;
57
73
  this.compression = compression;
58
74
  this.compressionThreshold = compressionThresholdBytes;
75
+ this.jsonOptions = { decimalFactory };
76
+ this.onError = onError;
59
77
  this.client = url
60
78
  ? new Redis(url, { lazyConnect: true })
61
79
  : new Redis({ host, port, lazyConnect: true });
@@ -74,6 +92,18 @@ export class RedisCacheAdapter implements CacheAdapter {
74
92
  void this.connect();
75
93
  }
76
94
 
95
+ private report(op: string, err: unknown): void {
96
+ console.warn(
97
+ `[RedisCacheAdapter] ${op} failed`,
98
+ (err as Error)?.message ?? err,
99
+ );
100
+ try {
101
+ this.onError?.(err, op);
102
+ } catch {
103
+ /* ignore hook errors */
104
+ }
105
+ }
106
+
77
107
  async connect(): Promise<void> {
78
108
  if (this.client.status === 'ready' || this.client.status === 'connecting') {
79
109
  return;
@@ -81,10 +111,7 @@ export class RedisCacheAdapter implements CacheAdapter {
81
111
  try {
82
112
  await this.client.connect();
83
113
  } catch (err) {
84
- console.warn(
85
- '[RedisCacheAdapter] failed to connect',
86
- (err as Error).message,
87
- );
114
+ this.report('connect', err);
88
115
  }
89
116
  }
90
117
 
@@ -117,13 +144,11 @@ export class RedisCacheAdapter implements CacheAdapter {
117
144
  if (raw.startsWith(COMPRESSED_PREFIX)) {
118
145
  const buf = Buffer.from(raw.slice(COMPRESSED_PREFIX.length), 'base64');
119
146
  const json = gunzipSync(buf).toString('utf8');
120
- return redisJsonParse<T>(json);
147
+ return redisJsonParse<T>(json, this.jsonOptions);
121
148
  }
122
- return redisJsonParse<T>(raw);
149
+ return redisJsonParse<T>(raw, this.jsonOptions);
123
150
  }
124
151
 
125
- // --- low-level ops (may throw) ---
126
-
127
152
  async get<T>(key: string): Promise<T | null> {
128
153
  const raw = await this.client.get(key);
129
154
  if (raw === null) return null;
@@ -153,10 +178,6 @@ export class RedisCacheAdapter implements CacheAdapter {
153
178
  return result === 'OK';
154
179
  }
155
180
 
156
- /**
157
- * Atomically SET a cache key + SADD into an index SET + EXPIRE the index
158
- * via a single Redis pipeline.
159
- */
160
181
  async setWithIndex(
161
182
  key: string,
162
183
  value: unknown,
@@ -170,9 +191,6 @@ export class RedisCacheAdapter implements CacheAdapter {
170
191
  await pipeline.exec();
171
192
  }
172
193
 
173
- /**
174
- * Atomic invalidation via Lua (SMEMBERS + DEL members + DEL index).
175
- */
176
194
  async invalidateByIndex(indexKey: string): Promise<void> {
177
195
  await this.client.eval(INVALIDATE_BY_INDEX_LUA, 1, indexKey);
178
196
  }
@@ -189,16 +207,11 @@ export class RedisCacheAdapter implements CacheAdapter {
189
207
  await pipeline.exec();
190
208
  }
191
209
 
192
- // --- safe wrappers (never throw — warn + return fallback) ---
193
-
194
210
  async safeGet<T>(key: string): Promise<T | null> {
195
211
  try {
196
212
  return await this.get<T>(key);
197
213
  } catch (err) {
198
- console.warn(
199
- `[RedisCacheAdapter] safeGet failed for key=${key}`,
200
- (err as Error).message,
201
- );
214
+ this.report(`safeGet key=${key}`, err);
202
215
  return null;
203
216
  }
204
217
  }
@@ -211,10 +224,7 @@ export class RedisCacheAdapter implements CacheAdapter {
211
224
  try {
212
225
  await this.set(key, value, ttlSeconds);
213
226
  } catch (err) {
214
- console.warn(
215
- `[RedisCacheAdapter] safeSet failed for key=${key}`,
216
- (err as Error).message,
217
- );
227
+ this.report(`safeSet key=${key}`, err);
218
228
  }
219
229
  }
220
230
 
@@ -222,10 +232,7 @@ export class RedisCacheAdapter implements CacheAdapter {
222
232
  try {
223
233
  await this.del(...keys);
224
234
  } catch (err) {
225
- console.warn(
226
- '[RedisCacheAdapter] safeDel failed',
227
- (err as Error).message,
228
- );
235
+ this.report('safeDel', err);
229
236
  }
230
237
  }
231
238
 
@@ -233,10 +240,7 @@ export class RedisCacheAdapter implements CacheAdapter {
233
240
  try {
234
241
  return await this.setNx(key, ttlSeconds);
235
242
  } catch (err) {
236
- console.warn(
237
- `[RedisCacheAdapter] safeSetNx failed for key=${key}`,
238
- (err as Error).message,
239
- );
243
+ this.report(`safeSetNx key=${key}`, err);
240
244
  return false;
241
245
  }
242
246
  }
@@ -250,10 +254,7 @@ export class RedisCacheAdapter implements CacheAdapter {
250
254
  try {
251
255
  await this.setWithIndex(key, value, ttlSeconds, indexKey);
252
256
  } catch (err) {
253
- console.warn(
254
- `[RedisCacheAdapter] safeSetWithIndex failed for key=${key}`,
255
- (err as Error).message,
256
- );
257
+ this.report(`safeSetWithIndex key=${key}`, err);
257
258
  }
258
259
  }
259
260
 
@@ -261,10 +262,7 @@ export class RedisCacheAdapter implements CacheAdapter {
261
262
  try {
262
263
  await this.invalidateByIndex(indexKey);
263
264
  } catch (err) {
264
- console.warn(
265
- `[RedisCacheAdapter] safeInvalidateByIndex failed for idx=${indexKey}`,
266
- (err as Error).message,
267
- );
265
+ this.report(`safeInvalidateByIndex idx=${indexKey}`, err);
268
266
  }
269
267
  }
270
268
 
@@ -276,10 +274,7 @@ export class RedisCacheAdapter implements CacheAdapter {
276
274
  try {
277
275
  await this.saddAndExpire(key, members, ttlSeconds);
278
276
  } catch (err) {
279
- console.warn(
280
- `[RedisCacheAdapter] safeSaddAndExpire failed for key=${key}`,
281
- (err as Error).message,
282
- );
277
+ this.report(`safeSaddAndExpire key=${key}`, err);
283
278
  }
284
279
  }
285
280
 
@@ -287,10 +282,7 @@ export class RedisCacheAdapter implements CacheAdapter {
287
282
  try {
288
283
  return await this.smembers(key);
289
284
  } catch (err) {
290
- console.warn(
291
- `[RedisCacheAdapter] safeSmembers failed for key=${key}`,
292
- (err as Error).message,
293
- );
285
+ this.report(`safeSmembers key=${key}`, err);
294
286
  return [];
295
287
  }
296
288
  }