@prismakit/redis 2.1.1 → 2.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 +42 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -2
- package/dist/index.d.ts +18 -2
- package/dist/index.js +42 -11
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/__tests__/redis-cache-adapter.spec.ts +74 -0
- package/src/index.ts +22 -0
- package/src/redis-cache-adapter.ts +297 -0
- package/src/redis-json.ts +34 -0
package/dist/index.cjs
CHANGED
|
@@ -43,6 +43,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
43
43
|
|
|
44
44
|
// src/redis-cache-adapter.ts
|
|
45
45
|
var import_ioredis = __toESM(require("ioredis"), 1);
|
|
46
|
+
var import_zlib = require("zlib");
|
|
46
47
|
|
|
47
48
|
// src/redis-json.ts
|
|
48
49
|
var BIGINT_TAG = "__bigint";
|
|
@@ -70,18 +71,33 @@ function redisJsonParse(raw) {
|
|
|
70
71
|
|
|
71
72
|
// src/redis-cache-adapter.ts
|
|
72
73
|
var INDEX_TTL_BUFFER = 60;
|
|
74
|
+
var INVALIDATE_BY_INDEX_LUA = `
|
|
75
|
+
local members = redis.call('SMEMBERS', KEYS[1])
|
|
76
|
+
for i, member in ipairs(members) do
|
|
77
|
+
redis.call('DEL', member)
|
|
78
|
+
end
|
|
79
|
+
redis.call('DEL', KEYS[1])
|
|
80
|
+
return #members
|
|
81
|
+
`;
|
|
82
|
+
var COMPRESSED_PREFIX = "gz:";
|
|
73
83
|
var RedisCacheAdapter = class {
|
|
74
84
|
client;
|
|
75
85
|
prefix;
|
|
76
86
|
ready = false;
|
|
87
|
+
compression;
|
|
88
|
+
compressionThreshold;
|
|
77
89
|
constructor(options = {}) {
|
|
78
90
|
const {
|
|
79
91
|
url,
|
|
80
92
|
host = "localhost",
|
|
81
93
|
port = 6379,
|
|
82
|
-
prefix = "prismakit"
|
|
94
|
+
prefix = "prismakit",
|
|
95
|
+
compression = "none",
|
|
96
|
+
compressionThresholdBytes = 1024
|
|
83
97
|
} = options;
|
|
84
98
|
this.prefix = prefix;
|
|
99
|
+
this.compression = compression;
|
|
100
|
+
this.compressionThreshold = compressionThresholdBytes;
|
|
85
101
|
this.client = url ? new import_ioredis.default(url, { lazyConnect: true }) : new import_ioredis.default({ host, port, lazyConnect: true });
|
|
86
102
|
this.client.on("ready", () => {
|
|
87
103
|
this.ready = true;
|
|
@@ -118,14 +134,30 @@ var RedisCacheAdapter = class {
|
|
|
118
134
|
getPrefix() {
|
|
119
135
|
return this.prefix;
|
|
120
136
|
}
|
|
137
|
+
encode(value) {
|
|
138
|
+
const json = redisJsonStringify(value);
|
|
139
|
+
if (this.compression === "gzip" && Buffer.byteLength(json, "utf8") >= this.compressionThreshold) {
|
|
140
|
+
const compressed = (0, import_zlib.gzipSync)(Buffer.from(json, "utf8")).toString("base64");
|
|
141
|
+
return COMPRESSED_PREFIX + compressed;
|
|
142
|
+
}
|
|
143
|
+
return json;
|
|
144
|
+
}
|
|
145
|
+
decode(raw) {
|
|
146
|
+
if (raw.startsWith(COMPRESSED_PREFIX)) {
|
|
147
|
+
const buf = Buffer.from(raw.slice(COMPRESSED_PREFIX.length), "base64");
|
|
148
|
+
const json = (0, import_zlib.gunzipSync)(buf).toString("utf8");
|
|
149
|
+
return redisJsonParse(json);
|
|
150
|
+
}
|
|
151
|
+
return redisJsonParse(raw);
|
|
152
|
+
}
|
|
121
153
|
// --- low-level ops (may throw) ---
|
|
122
154
|
async get(key) {
|
|
123
155
|
const raw = await this.client.get(key);
|
|
124
156
|
if (raw === null) return null;
|
|
125
|
-
return
|
|
157
|
+
return this.decode(raw);
|
|
126
158
|
}
|
|
127
159
|
async set(key, value, ttlSeconds) {
|
|
128
|
-
await this.client.set(key,
|
|
160
|
+
await this.client.set(key, this.encode(value), "EX", ttlSeconds);
|
|
129
161
|
}
|
|
130
162
|
async del(...keys) {
|
|
131
163
|
if (keys.length === 0) return;
|
|
@@ -143,22 +175,21 @@ var RedisCacheAdapter = class {
|
|
|
143
175
|
return result === "OK";
|
|
144
176
|
}
|
|
145
177
|
/**
|
|
146
|
-
* Atomically SET a cache key + SADD into an index SET + EXPIRE the index
|
|
178
|
+
* Atomically SET a cache key + SADD into an index SET + EXPIRE the index
|
|
179
|
+
* via a single Redis pipeline.
|
|
147
180
|
*/
|
|
148
181
|
async setWithIndex(key, value, ttlSeconds, indexKey) {
|
|
149
182
|
const pipeline = this.client.pipeline();
|
|
150
|
-
pipeline.set(key,
|
|
183
|
+
pipeline.set(key, this.encode(value), "EX", ttlSeconds);
|
|
151
184
|
pipeline.sadd(indexKey, key);
|
|
152
185
|
pipeline.expire(indexKey, ttlSeconds + INDEX_TTL_BUFFER);
|
|
153
186
|
await pipeline.exec();
|
|
154
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Atomic invalidation via Lua (SMEMBERS + DEL members + DEL index).
|
|
190
|
+
*/
|
|
155
191
|
async invalidateByIndex(indexKey) {
|
|
156
|
-
|
|
157
|
-
if (keys.length > 0) {
|
|
158
|
-
await this.del(...keys, indexKey);
|
|
159
|
-
} else {
|
|
160
|
-
await this.del(indexKey);
|
|
161
|
-
}
|
|
192
|
+
await this.client.eval(INVALIDATE_BY_INDEX_LUA, 1, indexKey);
|
|
162
193
|
}
|
|
163
194
|
async saddAndExpire(key, members, ttlSeconds) {
|
|
164
195
|
if (members.length === 0) return;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/redis-cache-adapter.ts","../src/redis-json.ts"],"sourcesContent":["export {\n RedisCacheAdapter,\n type RedisCacheAdapterOptions,\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","import Redis from 'ioredis';\nimport type { CacheAdapter } from '@prismakit/core';\nimport { redisJsonParse, redisJsonStringify } from './redis-json';\n\nconst INDEX_TTL_BUFFER = 60;\n\nexport type RedisCacheAdapterOptions = {\n url?: string;\n host?: string;\n port?: number;\n prefix?: string;\n};\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\n constructor(options: RedisCacheAdapterOptions = {}) {\n const {\n url,\n host = 'localhost',\n port = 6379,\n prefix = 'prismakit',\n } = options;\n\n this.prefix = prefix;\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 // --- 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 redisJsonParse<T>(raw);\n }\n\n async set(key: string, value: unknown, ttlSeconds: number): Promise<void> {\n await this.client.set(key, redisJsonStringify(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 */\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, redisJsonStringify(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 const keys = await this.smembers(indexKey);\n if (keys.length > 0) {\n await this.del(...keys, indexKey);\n } else {\n await this.del(indexKey);\n }\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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,qBAAkB;;;ACAlB,IAAM,aAAa;AAGZ,SAAS,kBAAkB,MAAc,OAAyB;AACvE,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,EAAE,CAAC,UAAU,GAAG,MAAM,SAAS,EAAE;AAAA,EAC1C;AACA,SAAO;AACT;AAGO,SAAS,iBAAiB,MAAc,OAAyB;AACtE,MACE,UAAU,QACV,OAAO,UAAU,YACjB,CAAC,MAAM,QAAQ,KAAK,KACpB,OAAO,KAAK,KAAK,EAAE,WAAW,KAC9B,cAAc,OACd;AACA,UAAM,MAAO,MAAkC,UAAU;AACzD,QAAI,OAAO,QAAQ,UAAU;AAC3B,aAAO,OAAO,GAAG;AAAA,IACnB;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,mBAAmB,OAAwB;AACzD,SAAO,KAAK,UAAU,OAAO,iBAAiB;AAChD;AAEO,SAAS,eAAkB,KAAgB;AAChD,SAAO,KAAK,MAAM,KAAK,gBAAgB;AACzC;;;AD7BA,IAAM,mBAAmB;AAYlB,IAAM,oBAAN,MAAgD;AAAA,EACpC;AAAA,EACA;AAAA,EACT,QAAQ;AAAA,EAEhB,YAAY,UAAoC,CAAC,GAAG;AAClD,UAAM;AAAA,MACJ;AAAA,MACA,OAAO;AAAA,MACP,OAAO;AAAA,MACP,SAAS;AAAA,IACX,IAAI;AAEJ,SAAK,SAAS;AACd,SAAK,SAAS,MACV,IAAI,eAAAA,QAAM,KAAK,EAAE,aAAa,KAAK,CAAC,IACpC,IAAI,eAAAA,QAAM,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,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,cAAQ;AAAA,QACN;AAAA,QACC,IAAc;AAAA,MACjB;AAAA,IACF;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;AAAA,EAIA,MAAM,IAAO,KAAgC;AAC3C,UAAM,MAAM,MAAM,KAAK,OAAO,IAAI,GAAG;AACrC,QAAI,QAAQ,KAAM,QAAO;AACzB,WAAO,eAAkB,GAAG;AAAA,EAC9B;AAAA,EAEA,MAAM,IAAI,KAAa,OAAgB,YAAmC;AACxE,UAAM,KAAK,OAAO,IAAI,KAAK,mBAAmB,KAAK,GAAG,MAAM,UAAU;AAAA,EACxE;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;AAAA;AAAA;AAAA,EAKA,MAAM,aACJ,KACA,OACA,YACA,UACe;AACf,UAAM,WAAW,KAAK,OAAO,SAAS;AACtC,aAAS,IAAI,KAAK,mBAAmB,KAAK,GAAG,MAAM,UAAU;AAC7D,aAAS,KAAK,UAAU,GAAG;AAC3B,aAAS,OAAO,UAAU,aAAa,gBAAgB;AACvD,UAAM,SAAS,KAAK;AAAA,EACtB;AAAA,EAEA,MAAM,kBAAkB,UAAiC;AACvD,UAAM,OAAO,MAAM,KAAK,SAAS,QAAQ;AACzC,QAAI,KAAK,SAAS,GAAG;AACnB,YAAM,KAAK,IAAI,GAAG,MAAM,QAAQ;AAAA,IAClC,OAAO;AACL,YAAM,KAAK,IAAI,QAAQ;AAAA,IACzB;AAAA,EACF;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;AAAA,EAIA,MAAM,QAAW,KAAgC;AAC/C,QAAI;AACF,aAAO,MAAM,KAAK,IAAO,GAAG;AAAA,IAC9B,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN,8CAA8C,GAAG;AAAA,QAChD,IAAc;AAAA,MACjB;AACA,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,cAAQ;AAAA,QACN,8CAA8C,GAAG;AAAA,QAChD,IAAc;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,WAAW,MAA+B;AAC9C,QAAI;AACF,YAAM,KAAK,IAAI,GAAG,IAAI;AAAA,IACxB,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN;AAAA,QACC,IAAc;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,UAAU,KAAa,YAAsC;AACjE,QAAI;AACF,aAAO,MAAM,KAAK,MAAM,KAAK,UAAU;AAAA,IACzC,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN,gDAAgD,GAAG;AAAA,QAClD,IAAc;AAAA,MACjB;AACA,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,cAAQ;AAAA,QACN,uDAAuD,GAAG;AAAA,QACzD,IAAc;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,sBAAsB,UAAiC;AAC3D,QAAI;AACF,YAAM,KAAK,kBAAkB,QAAQ;AAAA,IACvC,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN,4DAA4D,QAAQ;AAAA,QACnE,IAAc;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,kBACJ,KACA,SACA,YACe;AACf,QAAI;AACF,YAAM,KAAK,cAAc,KAAK,SAAS,UAAU;AAAA,IACnD,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN,wDAAwD,GAAG;AAAA,QAC1D,IAAc;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,aAAa,KAAgC;AACjD,QAAI;AACF,aAAO,MAAM,KAAK,SAAS,GAAG;AAAA,IAChC,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN,mDAAmD,GAAG;AAAA,QACrD,IAAc;AAAA,MACjB;AACA,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AACF;;;AD3OA,kBAKO;","names":["Redis"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/redis-cache-adapter.ts","../src/redis-json.ts"],"sourcesContent":["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","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"],"mappings":"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","names":["Redis"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { CacheAdapter } from '@prismakit/core';
|
|
2
2
|
export { CacheAdapter, CacheDebugStatus, cacheDebugStorage, isCacheDebugEnabled, recordCacheDebug } from '@prismakit/core';
|
|
3
3
|
|
|
4
|
+
type RedisCompression = 'none' | 'gzip';
|
|
4
5
|
type RedisCacheAdapterOptions = {
|
|
5
6
|
url?: string;
|
|
6
7
|
host?: string;
|
|
7
8
|
port?: number;
|
|
8
9
|
prefix?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Compress payloads larger than `compressionThresholdBytes` (default 1024).
|
|
12
|
+
* Uses gzip (widely available; zstd/lz4 can be added later).
|
|
13
|
+
*/
|
|
14
|
+
compression?: RedisCompression;
|
|
15
|
+
/** Minimum payload size (bytes) before compression (default 1024). */
|
|
16
|
+
compressionThresholdBytes?: number;
|
|
9
17
|
};
|
|
10
18
|
/**
|
|
11
19
|
* Redis-backed {@link CacheAdapter}. Framework-agnostic — no NestJS / ConfigService.
|
|
@@ -14,11 +22,15 @@ declare class RedisCacheAdapter implements CacheAdapter {
|
|
|
14
22
|
private readonly client;
|
|
15
23
|
private readonly prefix;
|
|
16
24
|
private ready;
|
|
25
|
+
private readonly compression;
|
|
26
|
+
private readonly compressionThreshold;
|
|
17
27
|
constructor(options?: RedisCacheAdapterOptions);
|
|
18
28
|
connect(): Promise<void>;
|
|
19
29
|
disconnect(): Promise<void>;
|
|
20
30
|
isReady(): boolean;
|
|
21
31
|
getPrefix(): string;
|
|
32
|
+
private encode;
|
|
33
|
+
private decode;
|
|
22
34
|
get<T>(key: string): Promise<T | null>;
|
|
23
35
|
set(key: string, value: unknown, ttlSeconds: number): Promise<void>;
|
|
24
36
|
del(...keys: string[]): Promise<void>;
|
|
@@ -26,9 +38,13 @@ declare class RedisCacheAdapter implements CacheAdapter {
|
|
|
26
38
|
smembers(key: string): Promise<string[]>;
|
|
27
39
|
setNx(key: string, ttlSeconds: number): Promise<boolean>;
|
|
28
40
|
/**
|
|
29
|
-
* Atomically SET a cache key + SADD into an index SET + EXPIRE the index
|
|
41
|
+
* Atomically SET a cache key + SADD into an index SET + EXPIRE the index
|
|
42
|
+
* via a single Redis pipeline.
|
|
30
43
|
*/
|
|
31
44
|
setWithIndex(key: string, value: unknown, ttlSeconds: number, indexKey: string): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Atomic invalidation via Lua (SMEMBERS + DEL members + DEL index).
|
|
47
|
+
*/
|
|
32
48
|
invalidateByIndex(indexKey: string): Promise<void>;
|
|
33
49
|
saddAndExpire(key: string, members: string[], ttlSeconds: number): Promise<void>;
|
|
34
50
|
safeGet<T>(key: string): Promise<T | null>;
|
|
@@ -48,4 +64,4 @@ declare function redisJsonReviver(_key: string, value: unknown): unknown;
|
|
|
48
64
|
declare function redisJsonStringify(value: unknown): string;
|
|
49
65
|
declare function redisJsonParse<T>(raw: string): T;
|
|
50
66
|
|
|
51
|
-
export { RedisCacheAdapter, type RedisCacheAdapterOptions, redisJsonParse, redisJsonReplacer, redisJsonReviver, redisJsonStringify };
|
|
67
|
+
export { RedisCacheAdapter, type RedisCacheAdapterOptions, type RedisCompression, redisJsonParse, redisJsonReplacer, redisJsonReviver, redisJsonStringify };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { CacheAdapter } from '@prismakit/core';
|
|
2
2
|
export { CacheAdapter, CacheDebugStatus, cacheDebugStorage, isCacheDebugEnabled, recordCacheDebug } from '@prismakit/core';
|
|
3
3
|
|
|
4
|
+
type RedisCompression = 'none' | 'gzip';
|
|
4
5
|
type RedisCacheAdapterOptions = {
|
|
5
6
|
url?: string;
|
|
6
7
|
host?: string;
|
|
7
8
|
port?: number;
|
|
8
9
|
prefix?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Compress payloads larger than `compressionThresholdBytes` (default 1024).
|
|
12
|
+
* Uses gzip (widely available; zstd/lz4 can be added later).
|
|
13
|
+
*/
|
|
14
|
+
compression?: RedisCompression;
|
|
15
|
+
/** Minimum payload size (bytes) before compression (default 1024). */
|
|
16
|
+
compressionThresholdBytes?: number;
|
|
9
17
|
};
|
|
10
18
|
/**
|
|
11
19
|
* Redis-backed {@link CacheAdapter}. Framework-agnostic — no NestJS / ConfigService.
|
|
@@ -14,11 +22,15 @@ declare class RedisCacheAdapter implements CacheAdapter {
|
|
|
14
22
|
private readonly client;
|
|
15
23
|
private readonly prefix;
|
|
16
24
|
private ready;
|
|
25
|
+
private readonly compression;
|
|
26
|
+
private readonly compressionThreshold;
|
|
17
27
|
constructor(options?: RedisCacheAdapterOptions);
|
|
18
28
|
connect(): Promise<void>;
|
|
19
29
|
disconnect(): Promise<void>;
|
|
20
30
|
isReady(): boolean;
|
|
21
31
|
getPrefix(): string;
|
|
32
|
+
private encode;
|
|
33
|
+
private decode;
|
|
22
34
|
get<T>(key: string): Promise<T | null>;
|
|
23
35
|
set(key: string, value: unknown, ttlSeconds: number): Promise<void>;
|
|
24
36
|
del(...keys: string[]): Promise<void>;
|
|
@@ -26,9 +38,13 @@ declare class RedisCacheAdapter implements CacheAdapter {
|
|
|
26
38
|
smembers(key: string): Promise<string[]>;
|
|
27
39
|
setNx(key: string, ttlSeconds: number): Promise<boolean>;
|
|
28
40
|
/**
|
|
29
|
-
* Atomically SET a cache key + SADD into an index SET + EXPIRE the index
|
|
41
|
+
* Atomically SET a cache key + SADD into an index SET + EXPIRE the index
|
|
42
|
+
* via a single Redis pipeline.
|
|
30
43
|
*/
|
|
31
44
|
setWithIndex(key: string, value: unknown, ttlSeconds: number, indexKey: string): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Atomic invalidation via Lua (SMEMBERS + DEL members + DEL index).
|
|
47
|
+
*/
|
|
32
48
|
invalidateByIndex(indexKey: string): Promise<void>;
|
|
33
49
|
saddAndExpire(key: string, members: string[], ttlSeconds: number): Promise<void>;
|
|
34
50
|
safeGet<T>(key: string): Promise<T | null>;
|
|
@@ -48,4 +64,4 @@ declare function redisJsonReviver(_key: string, value: unknown): unknown;
|
|
|
48
64
|
declare function redisJsonStringify(value: unknown): string;
|
|
49
65
|
declare function redisJsonParse<T>(raw: string): T;
|
|
50
66
|
|
|
51
|
-
export { RedisCacheAdapter, type RedisCacheAdapterOptions, redisJsonParse, redisJsonReplacer, redisJsonReviver, redisJsonStringify };
|
|
67
|
+
export { RedisCacheAdapter, type RedisCacheAdapterOptions, type RedisCompression, redisJsonParse, redisJsonReplacer, redisJsonReviver, redisJsonStringify };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/redis-cache-adapter.ts
|
|
2
2
|
import Redis from "ioredis";
|
|
3
|
+
import { gzipSync, gunzipSync } from "zlib";
|
|
3
4
|
|
|
4
5
|
// src/redis-json.ts
|
|
5
6
|
var BIGINT_TAG = "__bigint";
|
|
@@ -27,18 +28,33 @@ function redisJsonParse(raw) {
|
|
|
27
28
|
|
|
28
29
|
// src/redis-cache-adapter.ts
|
|
29
30
|
var INDEX_TTL_BUFFER = 60;
|
|
31
|
+
var INVALIDATE_BY_INDEX_LUA = `
|
|
32
|
+
local members = redis.call('SMEMBERS', KEYS[1])
|
|
33
|
+
for i, member in ipairs(members) do
|
|
34
|
+
redis.call('DEL', member)
|
|
35
|
+
end
|
|
36
|
+
redis.call('DEL', KEYS[1])
|
|
37
|
+
return #members
|
|
38
|
+
`;
|
|
39
|
+
var COMPRESSED_PREFIX = "gz:";
|
|
30
40
|
var RedisCacheAdapter = class {
|
|
31
41
|
client;
|
|
32
42
|
prefix;
|
|
33
43
|
ready = false;
|
|
44
|
+
compression;
|
|
45
|
+
compressionThreshold;
|
|
34
46
|
constructor(options = {}) {
|
|
35
47
|
const {
|
|
36
48
|
url,
|
|
37
49
|
host = "localhost",
|
|
38
50
|
port = 6379,
|
|
39
|
-
prefix = "prismakit"
|
|
51
|
+
prefix = "prismakit",
|
|
52
|
+
compression = "none",
|
|
53
|
+
compressionThresholdBytes = 1024
|
|
40
54
|
} = options;
|
|
41
55
|
this.prefix = prefix;
|
|
56
|
+
this.compression = compression;
|
|
57
|
+
this.compressionThreshold = compressionThresholdBytes;
|
|
42
58
|
this.client = url ? new Redis(url, { lazyConnect: true }) : new Redis({ host, port, lazyConnect: true });
|
|
43
59
|
this.client.on("ready", () => {
|
|
44
60
|
this.ready = true;
|
|
@@ -75,14 +91,30 @@ var RedisCacheAdapter = class {
|
|
|
75
91
|
getPrefix() {
|
|
76
92
|
return this.prefix;
|
|
77
93
|
}
|
|
94
|
+
encode(value) {
|
|
95
|
+
const json = redisJsonStringify(value);
|
|
96
|
+
if (this.compression === "gzip" && Buffer.byteLength(json, "utf8") >= this.compressionThreshold) {
|
|
97
|
+
const compressed = gzipSync(Buffer.from(json, "utf8")).toString("base64");
|
|
98
|
+
return COMPRESSED_PREFIX + compressed;
|
|
99
|
+
}
|
|
100
|
+
return json;
|
|
101
|
+
}
|
|
102
|
+
decode(raw) {
|
|
103
|
+
if (raw.startsWith(COMPRESSED_PREFIX)) {
|
|
104
|
+
const buf = Buffer.from(raw.slice(COMPRESSED_PREFIX.length), "base64");
|
|
105
|
+
const json = gunzipSync(buf).toString("utf8");
|
|
106
|
+
return redisJsonParse(json);
|
|
107
|
+
}
|
|
108
|
+
return redisJsonParse(raw);
|
|
109
|
+
}
|
|
78
110
|
// --- low-level ops (may throw) ---
|
|
79
111
|
async get(key) {
|
|
80
112
|
const raw = await this.client.get(key);
|
|
81
113
|
if (raw === null) return null;
|
|
82
|
-
return
|
|
114
|
+
return this.decode(raw);
|
|
83
115
|
}
|
|
84
116
|
async set(key, value, ttlSeconds) {
|
|
85
|
-
await this.client.set(key,
|
|
117
|
+
await this.client.set(key, this.encode(value), "EX", ttlSeconds);
|
|
86
118
|
}
|
|
87
119
|
async del(...keys) {
|
|
88
120
|
if (keys.length === 0) return;
|
|
@@ -100,22 +132,21 @@ var RedisCacheAdapter = class {
|
|
|
100
132
|
return result === "OK";
|
|
101
133
|
}
|
|
102
134
|
/**
|
|
103
|
-
* Atomically SET a cache key + SADD into an index SET + EXPIRE the index
|
|
135
|
+
* Atomically SET a cache key + SADD into an index SET + EXPIRE the index
|
|
136
|
+
* via a single Redis pipeline.
|
|
104
137
|
*/
|
|
105
138
|
async setWithIndex(key, value, ttlSeconds, indexKey) {
|
|
106
139
|
const pipeline = this.client.pipeline();
|
|
107
|
-
pipeline.set(key,
|
|
140
|
+
pipeline.set(key, this.encode(value), "EX", ttlSeconds);
|
|
108
141
|
pipeline.sadd(indexKey, key);
|
|
109
142
|
pipeline.expire(indexKey, ttlSeconds + INDEX_TTL_BUFFER);
|
|
110
143
|
await pipeline.exec();
|
|
111
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Atomic invalidation via Lua (SMEMBERS + DEL members + DEL index).
|
|
147
|
+
*/
|
|
112
148
|
async invalidateByIndex(indexKey) {
|
|
113
|
-
|
|
114
|
-
if (keys.length > 0) {
|
|
115
|
-
await this.del(...keys, indexKey);
|
|
116
|
-
} else {
|
|
117
|
-
await this.del(indexKey);
|
|
118
|
-
}
|
|
149
|
+
await this.client.eval(INVALIDATE_BY_INDEX_LUA, 1, indexKey);
|
|
119
150
|
}
|
|
120
151
|
async saddAndExpire(key, members, ttlSeconds) {
|
|
121
152
|
if (members.length === 0) return;
|
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 type { CacheAdapter } from '@prismakit/core';\nimport { redisJsonParse, redisJsonStringify } from './redis-json';\n\nconst INDEX_TTL_BUFFER = 60;\n\nexport type RedisCacheAdapterOptions = {\n url?: string;\n host?: string;\n port?: number;\n prefix?: string;\n};\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\n constructor(options: RedisCacheAdapterOptions = {}) {\n const {\n url,\n host = 'localhost',\n port = 6379,\n prefix = 'prismakit',\n } = options;\n\n this.prefix = prefix;\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 // --- 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 redisJsonParse<T>(raw);\n }\n\n async set(key: string, value: unknown, ttlSeconds: number): Promise<void> {\n await this.client.set(key, redisJsonStringify(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 */\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, redisJsonStringify(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 const keys = await this.smembers(indexKey);\n if (keys.length > 0) {\n await this.del(...keys, indexKey);\n } else {\n await this.del(indexKey);\n }\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} 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 { 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":";AAAA,OAAO,WAAW;AAClB,SAAS,UAAU,kBAAkB;;;ACDrC,IAAM,aAAa;AAGZ,SAAS,kBAAkB,MAAc,OAAyB;AACvE,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,EAAE,CAAC,UAAU,GAAG,MAAM,SAAS,EAAE;AAAA,EAC1C;AACA,SAAO;AACT;AAGO,SAAS,iBAAiB,MAAc,OAAyB;AACtE,MACE,UAAU,QACV,OAAO,UAAU,YACjB,CAAC,MAAM,QAAQ,KAAK,KACpB,OAAO,KAAK,KAAK,EAAE,WAAW,KAC9B,cAAc,OACd;AACA,UAAM,MAAO,MAAkC,UAAU;AACzD,QAAI,OAAO,QAAQ,UAAU;AAC3B,aAAO,OAAO,GAAG;AAAA,IACnB;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,mBAAmB,OAAwB;AACzD,SAAO,KAAK,UAAU,OAAO,iBAAiB;AAChD;AAEO,SAAS,eAAkB,KAAgB;AAChD,SAAO,KAAK,MAAM,KAAK,gBAAgB;AACzC;;;AD5BA,IAAM,mBAAmB;AAGzB,IAAM,0BAA0B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyBhC,IAAM,oBAAoB;AAKnB,IAAM,oBAAN,MAAgD;AAAA,EACpC;AAAA,EACA;AAAA,EACT,QAAQ;AAAA,EACC;AAAA,EACA;AAAA,EAEjB,YAAY,UAAoC,CAAC,GAAG;AAClD,UAAM;AAAA,MACJ;AAAA,MACA,OAAO;AAAA,MACP,OAAO;AAAA,MACP,SAAS;AAAA,MACT,cAAc;AAAA,MACd,4BAA4B;AAAA,IAC9B,IAAI;AAEJ,SAAK,SAAS;AACd,SAAK,cAAc;AACnB,SAAK,uBAAuB;AAC5B,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,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,cAAQ;AAAA,QACN;AAAA,QACC,IAAc;AAAA,MACjB;AAAA,IACF;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,IAAI;AAAA,IAC/B;AACA,WAAO,eAAkB,GAAG;AAAA,EAC9B;AAAA;AAAA,EAIA,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;AAAA;AAAA;AAAA;AAAA,EAMA,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;AAAA;AAAA;AAAA,EAKA,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;AAAA,EAIA,MAAM,QAAW,KAAgC;AAC/C,QAAI;AACF,aAAO,MAAM,KAAK,IAAO,GAAG;AAAA,IAC9B,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN,8CAA8C,GAAG;AAAA,QAChD,IAAc;AAAA,MACjB;AACA,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,cAAQ;AAAA,QACN,8CAA8C,GAAG;AAAA,QAChD,IAAc;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,WAAW,MAA+B;AAC9C,QAAI;AACF,YAAM,KAAK,IAAI,GAAG,IAAI;AAAA,IACxB,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN;AAAA,QACC,IAAc;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,UAAU,KAAa,YAAsC;AACjE,QAAI;AACF,aAAO,MAAM,KAAK,MAAM,KAAK,UAAU;AAAA,IACzC,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN,gDAAgD,GAAG;AAAA,QAClD,IAAc;AAAA,MACjB;AACA,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,cAAQ;AAAA,QACN,uDAAuD,GAAG;AAAA,QACzD,IAAc;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,sBAAsB,UAAiC;AAC3D,QAAI;AACF,YAAM,KAAK,kBAAkB,QAAQ;AAAA,IACvC,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN,4DAA4D,QAAQ;AAAA,QACnE,IAAc;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,kBACJ,KACA,SACA,YACe;AACf,QAAI;AACF,YAAM,KAAK,cAAc,KAAK,SAAS,UAAU;AAAA,IACnD,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN,wDAAwD,GAAG;AAAA,QAC1D,IAAc;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,aAAa,KAAgC;AACjD,QAAI;AACF,aAAO,MAAM,KAAK,SAAS,GAAG;AAAA,IAChC,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN,mDAAmD,GAAG;AAAA,QACrD,IAAc;AAAA,MACjB;AACA,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AACF;;;AE1RA;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": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Redis CacheAdapter for @prismakit/core",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"engines": {
|
|
@@ -19,11 +19,12 @@
|
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"dist",
|
|
22
|
+
"src",
|
|
22
23
|
"LICENSE",
|
|
23
24
|
"README.md"
|
|
24
25
|
],
|
|
25
26
|
"dependencies": {
|
|
26
|
-
"@prismakit/core": "2.
|
|
27
|
+
"@prismakit/core": "2.2.0"
|
|
27
28
|
},
|
|
28
29
|
"peerDependencies": {
|
|
29
30
|
"@prismakit/core": ">=2.1.1",
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const mockClient = {
|
|
4
|
+
status: 'wait',
|
|
5
|
+
on: vi.fn(),
|
|
6
|
+
connect: vi.fn().mockResolvedValue(undefined),
|
|
7
|
+
quit: vi.fn().mockResolvedValue(undefined),
|
|
8
|
+
get: vi.fn(),
|
|
9
|
+
set: vi.fn(),
|
|
10
|
+
del: vi.fn(),
|
|
11
|
+
sadd: vi.fn(),
|
|
12
|
+
smembers: vi.fn(),
|
|
13
|
+
pipeline: vi.fn(),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
vi.mock('ioredis', () => ({
|
|
17
|
+
default: vi.fn().mockImplementation(() => mockClient),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
describe('RedisCacheAdapter', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
mockClient.status = 'wait';
|
|
24
|
+
mockClient.on.mockImplementation(() => mockClient);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('connects with lazyConnect and exposes prefix', async () => {
|
|
28
|
+
const { RedisCacheAdapter } = await import('../redis-cache-adapter');
|
|
29
|
+
const adapter = new RedisCacheAdapter({
|
|
30
|
+
host: '127.0.0.1',
|
|
31
|
+
port: 6379,
|
|
32
|
+
prefix: 'test',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(adapter.getPrefix()).toBe('test');
|
|
36
|
+
expect(mockClient.connect).toHaveBeenCalled();
|
|
37
|
+
await adapter.disconnect();
|
|
38
|
+
expect(mockClient.quit).toHaveBeenCalled();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('safeGet returns null on failure', async () => {
|
|
42
|
+
const { RedisCacheAdapter } = await import('../redis-cache-adapter');
|
|
43
|
+
mockClient.get.mockRejectedValue(new Error('down'));
|
|
44
|
+
const adapter = new RedisCacheAdapter({ prefix: 't' });
|
|
45
|
+
await expect(adapter.safeGet('k')).resolves.toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('round-trips BigInt via redisJson helpers', async () => {
|
|
49
|
+
const { redisJsonParse, redisJsonStringify } = await import('../redis-json');
|
|
50
|
+
const payload = { id: '1', qty: 9007199254740993n, nested: { n: 1n } };
|
|
51
|
+
const raw = redisJsonStringify(payload);
|
|
52
|
+
expect(raw).toContain('__bigint');
|
|
53
|
+
const parsed = redisJsonParse<typeof payload>(raw);
|
|
54
|
+
expect(parsed.qty).toBe(9007199254740993n);
|
|
55
|
+
expect(parsed.nested.n).toBe(1n);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('set/get uses BigInt-safe JSON', async () => {
|
|
59
|
+
const { RedisCacheAdapter } = await import('../redis-cache-adapter');
|
|
60
|
+
let stored: string | undefined;
|
|
61
|
+
mockClient.set.mockImplementation((_k: string, v: string) => {
|
|
62
|
+
stored = v;
|
|
63
|
+
return Promise.resolve('OK');
|
|
64
|
+
});
|
|
65
|
+
mockClient.get.mockImplementation(() => Promise.resolve(stored ?? null));
|
|
66
|
+
|
|
67
|
+
const adapter = new RedisCacheAdapter({ prefix: 't' });
|
|
68
|
+
await adapter.set('k', { amount: 42n }, 60);
|
|
69
|
+
expect(stored).toContain('__bigint');
|
|
70
|
+
await expect(adapter.get<{ amount: bigint }>('k')).resolves.toEqual({
|
|
71
|
+
amount: 42n,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export {
|
|
2
|
+
RedisCacheAdapter,
|
|
3
|
+
type RedisCacheAdapterOptions,
|
|
4
|
+
type RedisCompression,
|
|
5
|
+
} from './redis-cache-adapter';
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
redisJsonParse,
|
|
9
|
+
redisJsonStringify,
|
|
10
|
+
redisJsonReplacer,
|
|
11
|
+
redisJsonReviver,
|
|
12
|
+
} from './redis-json';
|
|
13
|
+
|
|
14
|
+
/** Re-export cache debug helpers from core — do not duplicate. */
|
|
15
|
+
export {
|
|
16
|
+
type CacheDebugStatus,
|
|
17
|
+
cacheDebugStorage,
|
|
18
|
+
isCacheDebugEnabled,
|
|
19
|
+
recordCacheDebug,
|
|
20
|
+
} from '@prismakit/core';
|
|
21
|
+
|
|
22
|
+
export type { CacheAdapter } from '@prismakit/core';
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import Redis from 'ioredis';
|
|
2
|
+
import { gzipSync, gunzipSync } from 'zlib';
|
|
3
|
+
import type { CacheAdapter } from '@prismakit/core';
|
|
4
|
+
import { redisJsonParse, redisJsonStringify } from './redis-json';
|
|
5
|
+
|
|
6
|
+
const INDEX_TTL_BUFFER = 60;
|
|
7
|
+
|
|
8
|
+
/** Lua: atomically SMEMBERS index + DEL members + index. */
|
|
9
|
+
const INVALIDATE_BY_INDEX_LUA = `
|
|
10
|
+
local members = redis.call('SMEMBERS', KEYS[1])
|
|
11
|
+
for i, member in ipairs(members) do
|
|
12
|
+
redis.call('DEL', member)
|
|
13
|
+
end
|
|
14
|
+
redis.call('DEL', KEYS[1])
|
|
15
|
+
return #members
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
export type RedisCompression = 'none' | 'gzip';
|
|
19
|
+
|
|
20
|
+
export type RedisCacheAdapterOptions = {
|
|
21
|
+
url?: string;
|
|
22
|
+
host?: string;
|
|
23
|
+
port?: number;
|
|
24
|
+
prefix?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Compress payloads larger than `compressionThresholdBytes` (default 1024).
|
|
27
|
+
* Uses gzip (widely available; zstd/lz4 can be added later).
|
|
28
|
+
*/
|
|
29
|
+
compression?: RedisCompression;
|
|
30
|
+
/** Minimum payload size (bytes) before compression (default 1024). */
|
|
31
|
+
compressionThresholdBytes?: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const COMPRESSED_PREFIX = 'gz:';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Redis-backed {@link CacheAdapter}. Framework-agnostic — no NestJS / ConfigService.
|
|
38
|
+
*/
|
|
39
|
+
export class RedisCacheAdapter implements CacheAdapter {
|
|
40
|
+
private readonly client: Redis;
|
|
41
|
+
private readonly prefix: string;
|
|
42
|
+
private ready = false;
|
|
43
|
+
private readonly compression: RedisCompression;
|
|
44
|
+
private readonly compressionThreshold: number;
|
|
45
|
+
|
|
46
|
+
constructor(options: RedisCacheAdapterOptions = {}) {
|
|
47
|
+
const {
|
|
48
|
+
url,
|
|
49
|
+
host = 'localhost',
|
|
50
|
+
port = 6379,
|
|
51
|
+
prefix = 'prismakit',
|
|
52
|
+
compression = 'none',
|
|
53
|
+
compressionThresholdBytes = 1024,
|
|
54
|
+
} = options;
|
|
55
|
+
|
|
56
|
+
this.prefix = prefix;
|
|
57
|
+
this.compression = compression;
|
|
58
|
+
this.compressionThreshold = compressionThresholdBytes;
|
|
59
|
+
this.client = url
|
|
60
|
+
? new Redis(url, { lazyConnect: true })
|
|
61
|
+
: new Redis({ host, port, lazyConnect: true });
|
|
62
|
+
|
|
63
|
+
this.client.on('ready', () => {
|
|
64
|
+
this.ready = true;
|
|
65
|
+
});
|
|
66
|
+
this.client.on('error', (err) => {
|
|
67
|
+
this.ready = false;
|
|
68
|
+
console.warn('[RedisCacheAdapter] connection error', err.message);
|
|
69
|
+
});
|
|
70
|
+
this.client.on('close', () => {
|
|
71
|
+
this.ready = false;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
void this.connect();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async connect(): Promise<void> {
|
|
78
|
+
if (this.client.status === 'ready' || this.client.status === 'connecting') {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
await this.client.connect();
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.warn(
|
|
85
|
+
'[RedisCacheAdapter] failed to connect',
|
|
86
|
+
(err as Error).message,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async disconnect(): Promise<void> {
|
|
92
|
+
await this.client?.quit();
|
|
93
|
+
this.ready = false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
isReady(): boolean {
|
|
97
|
+
return this.ready;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getPrefix(): string {
|
|
101
|
+
return this.prefix;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private encode(value: unknown): string {
|
|
105
|
+
const json = redisJsonStringify(value);
|
|
106
|
+
if (
|
|
107
|
+
this.compression === 'gzip' &&
|
|
108
|
+
Buffer.byteLength(json, 'utf8') >= this.compressionThreshold
|
|
109
|
+
) {
|
|
110
|
+
const compressed = gzipSync(Buffer.from(json, 'utf8')).toString('base64');
|
|
111
|
+
return COMPRESSED_PREFIX + compressed;
|
|
112
|
+
}
|
|
113
|
+
return json;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private decode<T>(raw: string): T {
|
|
117
|
+
if (raw.startsWith(COMPRESSED_PREFIX)) {
|
|
118
|
+
const buf = Buffer.from(raw.slice(COMPRESSED_PREFIX.length), 'base64');
|
|
119
|
+
const json = gunzipSync(buf).toString('utf8');
|
|
120
|
+
return redisJsonParse<T>(json);
|
|
121
|
+
}
|
|
122
|
+
return redisJsonParse<T>(raw);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --- low-level ops (may throw) ---
|
|
126
|
+
|
|
127
|
+
async get<T>(key: string): Promise<T | null> {
|
|
128
|
+
const raw = await this.client.get(key);
|
|
129
|
+
if (raw === null) return null;
|
|
130
|
+
return this.decode<T>(raw);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async set(key: string, value: unknown, ttlSeconds: number): Promise<void> {
|
|
134
|
+
await this.client.set(key, this.encode(value), 'EX', ttlSeconds);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async del(...keys: string[]): Promise<void> {
|
|
138
|
+
if (keys.length === 0) return;
|
|
139
|
+
await this.client.del(...keys);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async sadd(key: string, ...members: string[]): Promise<void> {
|
|
143
|
+
if (members.length === 0) return;
|
|
144
|
+
await this.client.sadd(key, ...members);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async smembers(key: string): Promise<string[]> {
|
|
148
|
+
return this.client.smembers(key);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async setNx(key: string, ttlSeconds: number): Promise<boolean> {
|
|
152
|
+
const result = await this.client.set(key, '1', 'EX', ttlSeconds, 'NX');
|
|
153
|
+
return result === 'OK';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Atomically SET a cache key + SADD into an index SET + EXPIRE the index
|
|
158
|
+
* via a single Redis pipeline.
|
|
159
|
+
*/
|
|
160
|
+
async setWithIndex(
|
|
161
|
+
key: string,
|
|
162
|
+
value: unknown,
|
|
163
|
+
ttlSeconds: number,
|
|
164
|
+
indexKey: string,
|
|
165
|
+
): Promise<void> {
|
|
166
|
+
const pipeline = this.client.pipeline();
|
|
167
|
+
pipeline.set(key, this.encode(value), 'EX', ttlSeconds);
|
|
168
|
+
pipeline.sadd(indexKey, key);
|
|
169
|
+
pipeline.expire(indexKey, ttlSeconds + INDEX_TTL_BUFFER);
|
|
170
|
+
await pipeline.exec();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Atomic invalidation via Lua (SMEMBERS + DEL members + DEL index).
|
|
175
|
+
*/
|
|
176
|
+
async invalidateByIndex(indexKey: string): Promise<void> {
|
|
177
|
+
await this.client.eval(INVALIDATE_BY_INDEX_LUA, 1, indexKey);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async saddAndExpire(
|
|
181
|
+
key: string,
|
|
182
|
+
members: string[],
|
|
183
|
+
ttlSeconds: number,
|
|
184
|
+
): Promise<void> {
|
|
185
|
+
if (members.length === 0) return;
|
|
186
|
+
const pipeline = this.client.pipeline();
|
|
187
|
+
pipeline.sadd(key, ...members);
|
|
188
|
+
pipeline.expire(key, ttlSeconds + INDEX_TTL_BUFFER);
|
|
189
|
+
await pipeline.exec();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// --- safe wrappers (never throw — warn + return fallback) ---
|
|
193
|
+
|
|
194
|
+
async safeGet<T>(key: string): Promise<T | null> {
|
|
195
|
+
try {
|
|
196
|
+
return await this.get<T>(key);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
console.warn(
|
|
199
|
+
`[RedisCacheAdapter] safeGet failed for key=${key}`,
|
|
200
|
+
(err as Error).message,
|
|
201
|
+
);
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async safeSet(
|
|
207
|
+
key: string,
|
|
208
|
+
value: unknown,
|
|
209
|
+
ttlSeconds: number,
|
|
210
|
+
): Promise<void> {
|
|
211
|
+
try {
|
|
212
|
+
await this.set(key, value, ttlSeconds);
|
|
213
|
+
} catch (err) {
|
|
214
|
+
console.warn(
|
|
215
|
+
`[RedisCacheAdapter] safeSet failed for key=${key}`,
|
|
216
|
+
(err as Error).message,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async safeDel(...keys: string[]): Promise<void> {
|
|
222
|
+
try {
|
|
223
|
+
await this.del(...keys);
|
|
224
|
+
} catch (err) {
|
|
225
|
+
console.warn(
|
|
226
|
+
'[RedisCacheAdapter] safeDel failed',
|
|
227
|
+
(err as Error).message,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async safeSetNx(key: string, ttlSeconds: number): Promise<boolean> {
|
|
233
|
+
try {
|
|
234
|
+
return await this.setNx(key, ttlSeconds);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
console.warn(
|
|
237
|
+
`[RedisCacheAdapter] safeSetNx failed for key=${key}`,
|
|
238
|
+
(err as Error).message,
|
|
239
|
+
);
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async safeSetWithIndex(
|
|
245
|
+
key: string,
|
|
246
|
+
value: unknown,
|
|
247
|
+
ttlSeconds: number,
|
|
248
|
+
indexKey: string,
|
|
249
|
+
): Promise<void> {
|
|
250
|
+
try {
|
|
251
|
+
await this.setWithIndex(key, value, ttlSeconds, indexKey);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
console.warn(
|
|
254
|
+
`[RedisCacheAdapter] safeSetWithIndex failed for key=${key}`,
|
|
255
|
+
(err as Error).message,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async safeInvalidateByIndex(indexKey: string): Promise<void> {
|
|
261
|
+
try {
|
|
262
|
+
await this.invalidateByIndex(indexKey);
|
|
263
|
+
} catch (err) {
|
|
264
|
+
console.warn(
|
|
265
|
+
`[RedisCacheAdapter] safeInvalidateByIndex failed for idx=${indexKey}`,
|
|
266
|
+
(err as Error).message,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async safeSaddAndExpire(
|
|
272
|
+
key: string,
|
|
273
|
+
members: string[],
|
|
274
|
+
ttlSeconds: number,
|
|
275
|
+
): Promise<void> {
|
|
276
|
+
try {
|
|
277
|
+
await this.saddAndExpire(key, members, ttlSeconds);
|
|
278
|
+
} catch (err) {
|
|
279
|
+
console.warn(
|
|
280
|
+
`[RedisCacheAdapter] safeSaddAndExpire failed for key=${key}`,
|
|
281
|
+
(err as Error).message,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async safeSmembers(key: string): Promise<string[]> {
|
|
287
|
+
try {
|
|
288
|
+
return await this.smembers(key);
|
|
289
|
+
} catch (err) {
|
|
290
|
+
console.warn(
|
|
291
|
+
`[RedisCacheAdapter] safeSmembers failed for key=${key}`,
|
|
292
|
+
(err as Error).message,
|
|
293
|
+
);
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const BIGINT_TAG = '__bigint';
|
|
2
|
+
|
|
3
|
+
/** JSON.stringify replacer — Prisma `BigInt` fields are not JSON-serializable by default. */
|
|
4
|
+
export function redisJsonReplacer(_key: string, value: unknown): unknown {
|
|
5
|
+
if (typeof value === 'bigint') {
|
|
6
|
+
return { [BIGINT_TAG]: value.toString() };
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** JSON.parse reviver — restore tagged BigInt payloads from {@link redisJsonReplacer}. */
|
|
12
|
+
export function redisJsonReviver(_key: string, value: unknown): unknown {
|
|
13
|
+
if (
|
|
14
|
+
value !== null &&
|
|
15
|
+
typeof value === 'object' &&
|
|
16
|
+
!Array.isArray(value) &&
|
|
17
|
+
Object.keys(value).length === 1 &&
|
|
18
|
+
BIGINT_TAG in value
|
|
19
|
+
) {
|
|
20
|
+
const raw = (value as Record<string, unknown>)[BIGINT_TAG];
|
|
21
|
+
if (typeof raw === 'string') {
|
|
22
|
+
return BigInt(raw);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function redisJsonStringify(value: unknown): string {
|
|
29
|
+
return JSON.stringify(value, redisJsonReplacer);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function redisJsonParse<T>(raw: string): T {
|
|
33
|
+
return JSON.parse(raw, redisJsonReviver) as T;
|
|
34
|
+
}
|