@prismakit/redis 3.2.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/index.cjs +13 -98
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -29
- package/dist/index.d.ts +3 -29
- package/dist/index.js +22 -97
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/redis-cache-adapter.spec.ts +20 -0
- package/src/index.ts +0 -9
- package/src/redis-cache-adapter.ts +7 -1
- package/src/redis-json.ts +11 -132
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ Redis `CacheAdapter` for `@prismakit/core` repositories (ioredis).
|
|
|
4
4
|
|
|
5
5
|
Fail-open: cache errors do not break reads/writes.
|
|
6
6
|
|
|
7
|
+
**Status:** pre-stable (4.0).
|
|
8
|
+
|
|
7
9
|
[Documentation](https://github.com/fikiap23/prismakit/blob/master/docs/README.md) · [Cache guide](https://github.com/fikiap23/prismakit/blob/master/docs/guide/cache.md) · [GitHub](https://github.com/fikiap23/prismakit)
|
|
8
10
|
|
|
9
11
|
## Install
|
|
@@ -15,11 +17,13 @@ pnpm add @prismakit/redis ioredis
|
|
|
15
17
|
## Usage
|
|
16
18
|
|
|
17
19
|
```typescript
|
|
20
|
+
import { Prisma } from '@prisma/client';
|
|
18
21
|
import { RedisCacheAdapter } from '@prismakit/redis';
|
|
19
22
|
|
|
20
23
|
const cache = new RedisCacheAdapter({
|
|
21
24
|
url: process.env.REDIS_URL, // or host + port
|
|
22
25
|
prefix: 'myapp',
|
|
26
|
+
decimalFactory: (s) => new Prisma.Decimal(s),
|
|
23
27
|
});
|
|
24
28
|
```
|
|
25
29
|
|
|
@@ -31,6 +35,7 @@ const cache = new RedisCacheAdapter({
|
|
|
31
35
|
| `host` | `localhost` | Used when `url` is omitted |
|
|
32
36
|
| `port` | `6379` | Used when `url` is omitted |
|
|
33
37
|
| `prefix` | `prismakit` | Key prefix for all cache keys |
|
|
38
|
+
| `decimalFactory` | keep string | `(s) => new Prisma.Decimal(s)` so Decimal matches native Prisma |
|
|
34
39
|
|
|
35
40
|
### Wire it up
|
|
36
41
|
|
|
@@ -49,6 +54,12 @@ PrismaKitModule.forRoot({
|
|
|
49
54
|
});
|
|
50
55
|
```
|
|
51
56
|
|
|
57
|
+
### JSON codec
|
|
58
|
+
|
|
59
|
+
Tagged JSON for `Date`, `BigInt`, `Bytes`, and Prisma `Decimal`. Pass `decimalFactory` so cache hits and AutoComposer clones return native Prisma scalars (`Date` / `Decimal`), not strings from `toJSON`.
|
|
60
|
+
|
|
61
|
+
Cache debug (`CACHE_DEBUG=true`, `cacheDebugStorage`, …) lives on `@prismakit/core`.
|
|
62
|
+
|
|
52
63
|
## Cache behavior (reminder)
|
|
53
64
|
|
|
54
65
|
- Reads cache only when `setCache: true`
|
package/dist/index.cjs
CHANGED
|
@@ -31,102 +31,21 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
RedisCacheAdapter: () => RedisCacheAdapter,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
redisJsonParse: () => redisJsonParse,
|
|
40
|
-
redisJsonReplacer: () => redisJsonReplacer,
|
|
41
|
-
redisJsonReviver: () => redisJsonReviver,
|
|
42
|
-
redisJsonStringify: () => redisJsonStringify
|
|
34
|
+
cloneWithCodec: () => import_core.cloneWithCodec,
|
|
35
|
+
createRedisJsonReviver: () => import_core.createTaggedJsonReviver,
|
|
36
|
+
redisJsonParse: () => import_core.taggedJsonParse,
|
|
37
|
+
redisJsonReplacer: () => import_core.taggedJsonReplacer,
|
|
38
|
+
redisJsonStringify: () => import_core.taggedJsonStringify
|
|
43
39
|
});
|
|
44
40
|
module.exports = __toCommonJS(index_exports);
|
|
45
41
|
|
|
46
42
|
// src/redis-cache-adapter.ts
|
|
47
43
|
var import_ioredis = __toESM(require("ioredis"), 1);
|
|
48
44
|
var import_zlib = require("zlib");
|
|
45
|
+
var import_core2 = require("@prismakit/core");
|
|
49
46
|
|
|
50
47
|
// src/redis-json.ts
|
|
51
|
-
var
|
|
52
|
-
var DATE_TAG = "__date";
|
|
53
|
-
var BYTES_TAG = "__bytes";
|
|
54
|
-
var DECIMAL_TAG = "__decimal";
|
|
55
|
-
function isPlainObject(value) {
|
|
56
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date) && !Buffer.isBuffer(value);
|
|
57
|
-
}
|
|
58
|
-
function isDecimalLike(value) {
|
|
59
|
-
return isPlainObject(value) && typeof value.constructor?.name === "string" && (value.constructor.name === "Decimal" || typeof value.toFixed === "function" && typeof value.d === "object");
|
|
60
|
-
}
|
|
61
|
-
function redisJsonReplacer(_key, value) {
|
|
62
|
-
if (typeof value === "bigint") {
|
|
63
|
-
return { [BIGINT_TAG]: value.toString() };
|
|
64
|
-
}
|
|
65
|
-
if (value instanceof Date) {
|
|
66
|
-
return { [DATE_TAG]: value.toISOString() };
|
|
67
|
-
}
|
|
68
|
-
if (Buffer.isBuffer(value)) {
|
|
69
|
-
return { [BYTES_TAG]: value.toString("base64") };
|
|
70
|
-
}
|
|
71
|
-
if (value && typeof value === "object" && value.type === "Buffer" && Array.isArray(value.data)) {
|
|
72
|
-
return {
|
|
73
|
-
[BYTES_TAG]: Buffer.from(
|
|
74
|
-
value.data
|
|
75
|
-
).toString("base64")
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
if (isDecimalLike(value)) {
|
|
79
|
-
return { [DECIMAL_TAG]: value.toFixed() };
|
|
80
|
-
}
|
|
81
|
-
return value;
|
|
82
|
-
}
|
|
83
|
-
function createRedisJsonReviver(options = {}) {
|
|
84
|
-
const decimalFactory = options.decimalFactory ?? ((s) => s);
|
|
85
|
-
return (_key, value) => {
|
|
86
|
-
if (!isPlainObject(value)) return value;
|
|
87
|
-
const keys = Object.keys(value);
|
|
88
|
-
if (keys.length !== 1) return value;
|
|
89
|
-
if (BIGINT_TAG in value && typeof value[BIGINT_TAG] === "string") {
|
|
90
|
-
return BigInt(value[BIGINT_TAG]);
|
|
91
|
-
}
|
|
92
|
-
if (DATE_TAG in value && typeof value[DATE_TAG] === "string") {
|
|
93
|
-
return new Date(value[DATE_TAG]);
|
|
94
|
-
}
|
|
95
|
-
if (BYTES_TAG in value && typeof value[BYTES_TAG] === "string") {
|
|
96
|
-
return Buffer.from(value[BYTES_TAG], "base64");
|
|
97
|
-
}
|
|
98
|
-
if (DECIMAL_TAG in value && typeof value[DECIMAL_TAG] === "string") {
|
|
99
|
-
return decimalFactory(value[DECIMAL_TAG]);
|
|
100
|
-
}
|
|
101
|
-
return value;
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
function redisJsonReviver(_key, value) {
|
|
105
|
-
return createRedisJsonReviver()(_key, value);
|
|
106
|
-
}
|
|
107
|
-
function redisJsonStringify(value) {
|
|
108
|
-
return JSON.stringify(value, function(key, val) {
|
|
109
|
-
const holder = this;
|
|
110
|
-
const raw = key === "" ? value : holder[key];
|
|
111
|
-
if (raw instanceof Date) {
|
|
112
|
-
return redisJsonReplacer(key, raw);
|
|
113
|
-
}
|
|
114
|
-
return redisJsonReplacer(key, val);
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
function redisJsonParse(raw, options) {
|
|
118
|
-
return JSON.parse(raw, createRedisJsonReviver(options));
|
|
119
|
-
}
|
|
120
|
-
function cloneWithCodec(value, options) {
|
|
121
|
-
if (value === null || value === void 0) return value;
|
|
122
|
-
if (typeof structuredClone === "function") {
|
|
123
|
-
try {
|
|
124
|
-
return structuredClone(value);
|
|
125
|
-
} catch {
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return redisJsonParse(redisJsonStringify(value), options);
|
|
129
|
-
}
|
|
48
|
+
var import_core = require("@prismakit/core");
|
|
130
49
|
|
|
131
50
|
// src/redis-cache-adapter.ts
|
|
132
51
|
var INDEX_TTL_BUFFER = 60;
|
|
@@ -162,6 +81,9 @@ var RedisCacheAdapter = class {
|
|
|
162
81
|
this.compression = compression;
|
|
163
82
|
this.compressionThreshold = compressionThresholdBytes;
|
|
164
83
|
this.jsonOptions = { decimalFactory };
|
|
84
|
+
if (decimalFactory) {
|
|
85
|
+
(0, import_core2.setTaggedJsonOptions)({ decimalFactory });
|
|
86
|
+
}
|
|
165
87
|
this.onError = onError;
|
|
166
88
|
this.client = url ? new import_ioredis.default(url, { lazyConnect: true }) : new import_ioredis.default({ host, port, lazyConnect: true });
|
|
167
89
|
this.client.on("ready", () => {
|
|
@@ -207,7 +129,7 @@ var RedisCacheAdapter = class {
|
|
|
207
129
|
return this.prefix;
|
|
208
130
|
}
|
|
209
131
|
encode(value) {
|
|
210
|
-
const json =
|
|
132
|
+
const json = (0, import_core.taggedJsonStringify)(value);
|
|
211
133
|
if (this.compression === "gzip" && Buffer.byteLength(json, "utf8") >= this.compressionThreshold) {
|
|
212
134
|
const compressed = (0, import_zlib.gzipSync)(Buffer.from(json, "utf8")).toString("base64");
|
|
213
135
|
return COMPRESSED_PREFIX + compressed;
|
|
@@ -218,9 +140,9 @@ var RedisCacheAdapter = class {
|
|
|
218
140
|
if (raw.startsWith(COMPRESSED_PREFIX)) {
|
|
219
141
|
const buf = Buffer.from(raw.slice(COMPRESSED_PREFIX.length), "base64");
|
|
220
142
|
const json = (0, import_zlib.gunzipSync)(buf).toString("utf8");
|
|
221
|
-
return
|
|
143
|
+
return (0, import_core.taggedJsonParse)(json, this.jsonOptions);
|
|
222
144
|
}
|
|
223
|
-
return
|
|
145
|
+
return (0, import_core.taggedJsonParse)(raw, this.jsonOptions);
|
|
224
146
|
}
|
|
225
147
|
async get(key) {
|
|
226
148
|
const raw = await this.client.get(key);
|
|
@@ -322,20 +244,13 @@ var RedisCacheAdapter = class {
|
|
|
322
244
|
}
|
|
323
245
|
}
|
|
324
246
|
};
|
|
325
|
-
|
|
326
|
-
// src/index.ts
|
|
327
|
-
var import_core = require("@prismakit/core");
|
|
328
247
|
// Annotate the CommonJS export names for ESM import in node:
|
|
329
248
|
0 && (module.exports = {
|
|
330
249
|
RedisCacheAdapter,
|
|
331
|
-
cacheDebugStorage,
|
|
332
250
|
cloneWithCodec,
|
|
333
251
|
createRedisJsonReviver,
|
|
334
|
-
isCacheDebugEnabled,
|
|
335
|
-
recordCacheDebug,
|
|
336
252
|
redisJsonParse,
|
|
337
253
|
redisJsonReplacer,
|
|
338
|
-
redisJsonReviver,
|
|
339
254
|
redisJsonStringify
|
|
340
255
|
});
|
|
341
256
|
//# sourceMappingURL=index.cjs.map
|
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 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","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"],"mappings":"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","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 createRedisJsonReviver,\n cloneWithCodec,\n type DecimalFactory,\n type RedisJsonOptions,\n} from './redis-json';\n\nexport type { CacheAdapter } from '@prismakit/core';\n","import Redis from 'ioredis';\nimport { gzipSync, gunzipSync } from 'zlib';\nimport {\n setTaggedJsonOptions,\n type CacheAdapter,\n} 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 if (decimalFactory) {\n setTaggedJsonOptions({ decimalFactory });\n }\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","export {\n taggedJsonParse as redisJsonParse,\n taggedJsonStringify as redisJsonStringify,\n taggedJsonReplacer as redisJsonReplacer,\n createTaggedJsonReviver as createRedisJsonReviver,\n cloneWithCodec,\n setTaggedJsonOptions,\n getTaggedJsonOptions,\n type DecimalFactory,\n type TaggedJsonOptions as RedisJsonOptions,\n} from '@prismakit/core';\n"],"mappings":"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","names":["import_core","Redis"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,31 +1,5 @@
|
|
|
1
|
-
import { CacheAdapter } from '@prismakit/core';
|
|
2
|
-
export { CacheAdapter,
|
|
3
|
-
|
|
4
|
-
type DecimalFactory = (value: string) => unknown;
|
|
5
|
-
type RedisJsonOptions = {
|
|
6
|
-
/**
|
|
7
|
-
* Reconstruct Prisma `Decimal` (or equivalent) from the tagged string.
|
|
8
|
-
* Default keeps the precision-preserving string so values are not silently
|
|
9
|
-
* coerced to numbers.
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* decimalFactory: (s) => new Prisma.Decimal(s)
|
|
13
|
-
*/
|
|
14
|
-
decimalFactory?: DecimalFactory;
|
|
15
|
-
};
|
|
16
|
-
/** JSON.stringify replacer — Prisma BigInt / Date / Bytes / Decimal. */
|
|
17
|
-
declare function redisJsonReplacer(_key: string, value: unknown): unknown;
|
|
18
|
-
/** JSON.parse reviver — restore tagged payloads from {@link redisJsonReplacer}. */
|
|
19
|
-
declare function createRedisJsonReviver(options?: RedisJsonOptions): (_key: string, value: unknown) => unknown;
|
|
20
|
-
/** @deprecated Prefer {@link createRedisJsonReviver} when a decimalFactory is needed. */
|
|
21
|
-
declare function redisJsonReviver(_key: string, value: unknown): unknown;
|
|
22
|
-
declare function redisJsonStringify(value: unknown): string;
|
|
23
|
-
declare function redisJsonParse<T>(raw: string, options?: RedisJsonOptions): T;
|
|
24
|
-
/**
|
|
25
|
-
* Deep clone via structuredClone, falling back to the tagged JSON codec so
|
|
26
|
-
* Date / BigInt / Buffer / Decimal survive (unlike plain JSON.stringify).
|
|
27
|
-
*/
|
|
28
|
-
declare function cloneWithCodec<T>(value: T, options?: RedisJsonOptions): T;
|
|
1
|
+
import { CacheAdapter, DecimalFactory } from '@prismakit/core';
|
|
2
|
+
export { CacheAdapter, DecimalFactory, TaggedJsonOptions as RedisJsonOptions, cloneWithCodec, createTaggedJsonReviver as createRedisJsonReviver, taggedJsonParse as redisJsonParse, taggedJsonReplacer as redisJsonReplacer, taggedJsonStringify as redisJsonStringify } from '@prismakit/core';
|
|
29
3
|
|
|
30
4
|
type RedisCompression = 'none' | 'gzip';
|
|
31
5
|
type RedisCacheAdapterOptions = {
|
|
@@ -86,4 +60,4 @@ declare class RedisCacheAdapter implements CacheAdapter {
|
|
|
86
60
|
safeSmembers(key: string): Promise<string[]>;
|
|
87
61
|
}
|
|
88
62
|
|
|
89
|
-
export {
|
|
63
|
+
export { RedisCacheAdapter, type RedisCacheAdapterOptions, type RedisCompression };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,31 +1,5 @@
|
|
|
1
|
-
import { CacheAdapter } from '@prismakit/core';
|
|
2
|
-
export { CacheAdapter,
|
|
3
|
-
|
|
4
|
-
type DecimalFactory = (value: string) => unknown;
|
|
5
|
-
type RedisJsonOptions = {
|
|
6
|
-
/**
|
|
7
|
-
* Reconstruct Prisma `Decimal` (or equivalent) from the tagged string.
|
|
8
|
-
* Default keeps the precision-preserving string so values are not silently
|
|
9
|
-
* coerced to numbers.
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* decimalFactory: (s) => new Prisma.Decimal(s)
|
|
13
|
-
*/
|
|
14
|
-
decimalFactory?: DecimalFactory;
|
|
15
|
-
};
|
|
16
|
-
/** JSON.stringify replacer — Prisma BigInt / Date / Bytes / Decimal. */
|
|
17
|
-
declare function redisJsonReplacer(_key: string, value: unknown): unknown;
|
|
18
|
-
/** JSON.parse reviver — restore tagged payloads from {@link redisJsonReplacer}. */
|
|
19
|
-
declare function createRedisJsonReviver(options?: RedisJsonOptions): (_key: string, value: unknown) => unknown;
|
|
20
|
-
/** @deprecated Prefer {@link createRedisJsonReviver} when a decimalFactory is needed. */
|
|
21
|
-
declare function redisJsonReviver(_key: string, value: unknown): unknown;
|
|
22
|
-
declare function redisJsonStringify(value: unknown): string;
|
|
23
|
-
declare function redisJsonParse<T>(raw: string, options?: RedisJsonOptions): T;
|
|
24
|
-
/**
|
|
25
|
-
* Deep clone via structuredClone, falling back to the tagged JSON codec so
|
|
26
|
-
* Date / BigInt / Buffer / Decimal survive (unlike plain JSON.stringify).
|
|
27
|
-
*/
|
|
28
|
-
declare function cloneWithCodec<T>(value: T, options?: RedisJsonOptions): T;
|
|
1
|
+
import { CacheAdapter, DecimalFactory } from '@prismakit/core';
|
|
2
|
+
export { CacheAdapter, DecimalFactory, TaggedJsonOptions as RedisJsonOptions, cloneWithCodec, createTaggedJsonReviver as createRedisJsonReviver, taggedJsonParse as redisJsonParse, taggedJsonReplacer as redisJsonReplacer, taggedJsonStringify as redisJsonStringify } from '@prismakit/core';
|
|
29
3
|
|
|
30
4
|
type RedisCompression = 'none' | 'gzip';
|
|
31
5
|
type RedisCacheAdapterOptions = {
|
|
@@ -86,4 +60,4 @@ declare class RedisCacheAdapter implements CacheAdapter {
|
|
|
86
60
|
safeSmembers(key: string): Promise<string[]>;
|
|
87
61
|
}
|
|
88
62
|
|
|
89
|
-
export {
|
|
63
|
+
export { RedisCacheAdapter, type RedisCacheAdapterOptions, type RedisCompression };
|
package/dist/index.js
CHANGED
|
@@ -1,87 +1,20 @@
|
|
|
1
1
|
// src/redis-cache-adapter.ts
|
|
2
2
|
import Redis from "ioredis";
|
|
3
3
|
import { gzipSync, gunzipSync } from "zlib";
|
|
4
|
+
import {
|
|
5
|
+
setTaggedJsonOptions as setTaggedJsonOptions2
|
|
6
|
+
} from "@prismakit/core";
|
|
4
7
|
|
|
5
8
|
// src/redis-json.ts
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
function redisJsonReplacer(_key, value) {
|
|
17
|
-
if (typeof value === "bigint") {
|
|
18
|
-
return { [BIGINT_TAG]: value.toString() };
|
|
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
|
-
}
|
|
36
|
-
return value;
|
|
37
|
-
}
|
|
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]);
|
|
46
|
-
}
|
|
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);
|
|
61
|
-
}
|
|
62
|
-
function redisJsonStringify(value) {
|
|
63
|
-
return JSON.stringify(value, function(key, val) {
|
|
64
|
-
const holder = this;
|
|
65
|
-
const raw = key === "" ? value : holder[key];
|
|
66
|
-
if (raw instanceof Date) {
|
|
67
|
-
return redisJsonReplacer(key, raw);
|
|
68
|
-
}
|
|
69
|
-
return redisJsonReplacer(key, val);
|
|
70
|
-
});
|
|
71
|
-
}
|
|
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);
|
|
84
|
-
}
|
|
9
|
+
import {
|
|
10
|
+
taggedJsonParse,
|
|
11
|
+
taggedJsonStringify,
|
|
12
|
+
taggedJsonReplacer,
|
|
13
|
+
createTaggedJsonReviver,
|
|
14
|
+
cloneWithCodec,
|
|
15
|
+
setTaggedJsonOptions,
|
|
16
|
+
getTaggedJsonOptions
|
|
17
|
+
} from "@prismakit/core";
|
|
85
18
|
|
|
86
19
|
// src/redis-cache-adapter.ts
|
|
87
20
|
var INDEX_TTL_BUFFER = 60;
|
|
@@ -117,6 +50,9 @@ var RedisCacheAdapter = class {
|
|
|
117
50
|
this.compression = compression;
|
|
118
51
|
this.compressionThreshold = compressionThresholdBytes;
|
|
119
52
|
this.jsonOptions = { decimalFactory };
|
|
53
|
+
if (decimalFactory) {
|
|
54
|
+
setTaggedJsonOptions2({ decimalFactory });
|
|
55
|
+
}
|
|
120
56
|
this.onError = onError;
|
|
121
57
|
this.client = url ? new Redis(url, { lazyConnect: true }) : new Redis({ host, port, lazyConnect: true });
|
|
122
58
|
this.client.on("ready", () => {
|
|
@@ -162,7 +98,7 @@ var RedisCacheAdapter = class {
|
|
|
162
98
|
return this.prefix;
|
|
163
99
|
}
|
|
164
100
|
encode(value) {
|
|
165
|
-
const json =
|
|
101
|
+
const json = taggedJsonStringify(value);
|
|
166
102
|
if (this.compression === "gzip" && Buffer.byteLength(json, "utf8") >= this.compressionThreshold) {
|
|
167
103
|
const compressed = gzipSync(Buffer.from(json, "utf8")).toString("base64");
|
|
168
104
|
return COMPRESSED_PREFIX + compressed;
|
|
@@ -173,9 +109,9 @@ var RedisCacheAdapter = class {
|
|
|
173
109
|
if (raw.startsWith(COMPRESSED_PREFIX)) {
|
|
174
110
|
const buf = Buffer.from(raw.slice(COMPRESSED_PREFIX.length), "base64");
|
|
175
111
|
const json = gunzipSync(buf).toString("utf8");
|
|
176
|
-
return
|
|
112
|
+
return taggedJsonParse(json, this.jsonOptions);
|
|
177
113
|
}
|
|
178
|
-
return
|
|
114
|
+
return taggedJsonParse(raw, this.jsonOptions);
|
|
179
115
|
}
|
|
180
116
|
async get(key) {
|
|
181
117
|
const raw = await this.client.get(key);
|
|
@@ -277,23 +213,12 @@ var RedisCacheAdapter = class {
|
|
|
277
213
|
}
|
|
278
214
|
}
|
|
279
215
|
};
|
|
280
|
-
|
|
281
|
-
// src/index.ts
|
|
282
|
-
import {
|
|
283
|
-
cacheDebugStorage,
|
|
284
|
-
isCacheDebugEnabled,
|
|
285
|
-
recordCacheDebug
|
|
286
|
-
} from "@prismakit/core";
|
|
287
216
|
export {
|
|
288
217
|
RedisCacheAdapter,
|
|
289
|
-
cacheDebugStorage,
|
|
290
218
|
cloneWithCodec,
|
|
291
|
-
createRedisJsonReviver,
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
redisJsonReplacer,
|
|
296
|
-
redisJsonReviver,
|
|
297
|
-
redisJsonStringify
|
|
219
|
+
createTaggedJsonReviver as createRedisJsonReviver,
|
|
220
|
+
taggedJsonParse as redisJsonParse,
|
|
221
|
+
taggedJsonReplacer as redisJsonReplacer,
|
|
222
|
+
taggedJsonStringify as redisJsonStringify
|
|
298
223
|
};
|
|
299
224
|
//# sourceMappingURL=index.js.map
|
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 {\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":[]}
|
|
1
|
+
{"version":3,"sources":["../src/redis-cache-adapter.ts","../src/redis-json.ts"],"sourcesContent":["import Redis from 'ioredis';\nimport { gzipSync, gunzipSync } from 'zlib';\nimport {\n setTaggedJsonOptions,\n type CacheAdapter,\n} 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 if (decimalFactory) {\n setTaggedJsonOptions({ decimalFactory });\n }\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","export {\n taggedJsonParse as redisJsonParse,\n taggedJsonStringify as redisJsonStringify,\n taggedJsonReplacer as redisJsonReplacer,\n createTaggedJsonReviver as createRedisJsonReviver,\n cloneWithCodec,\n setTaggedJsonOptions,\n getTaggedJsonOptions,\n type DecimalFactory,\n type TaggedJsonOptions as RedisJsonOptions,\n} from '@prismakit/core';\n"],"mappings":"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","names":["setTaggedJsonOptions","setTaggedJsonOptions"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prismakit/redis",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.1",
|
|
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": "
|
|
27
|
+
"@prismakit/core": "4.0.1"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@prismakit/core": ">=
|
|
30
|
+
"@prismakit/core": ">=4.0.1 <5",
|
|
31
31
|
"ioredis": ">=5.0.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
@@ -69,6 +69,26 @@ describe('RedisCacheAdapter', () => {
|
|
|
69
69
|
expect(parsed.blob.toString()).toBe('hello');
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
+
it('round-trips Decimal despite Decimal#toJSON', async () => {
|
|
73
|
+
class Decimal {
|
|
74
|
+
constructor(private readonly value: string) {}
|
|
75
|
+
toFixed() {
|
|
76
|
+
return this.value;
|
|
77
|
+
}
|
|
78
|
+
toJSON() {
|
|
79
|
+
return this.value;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const { redisJsonParse, redisJsonStringify } = await import('../redis-json');
|
|
83
|
+
const raw = redisJsonStringify({ amount: new Decimal('8.20') });
|
|
84
|
+
expect(raw).toContain('__decimal');
|
|
85
|
+
const parsed = redisJsonParse<{ amount: Decimal }>(raw, {
|
|
86
|
+
decimalFactory: (s) => new Decimal(s),
|
|
87
|
+
});
|
|
88
|
+
expect(parsed.amount).toBeInstanceOf(Decimal);
|
|
89
|
+
expect(parsed.amount.toFixed()).toBe('8.20');
|
|
90
|
+
});
|
|
91
|
+
|
|
72
92
|
it('set/get uses BigInt-safe JSON', async () => {
|
|
73
93
|
const { RedisCacheAdapter } = await import('../redis-cache-adapter');
|
|
74
94
|
let stored: string | undefined;
|
package/src/index.ts
CHANGED
|
@@ -8,19 +8,10 @@ export {
|
|
|
8
8
|
redisJsonParse,
|
|
9
9
|
redisJsonStringify,
|
|
10
10
|
redisJsonReplacer,
|
|
11
|
-
redisJsonReviver,
|
|
12
11
|
createRedisJsonReviver,
|
|
13
12
|
cloneWithCodec,
|
|
14
13
|
type DecimalFactory,
|
|
15
14
|
type RedisJsonOptions,
|
|
16
15
|
} from './redis-json';
|
|
17
16
|
|
|
18
|
-
/** Re-export cache debug helpers from core — do not duplicate. */
|
|
19
|
-
export {
|
|
20
|
-
type CacheDebugStatus,
|
|
21
|
-
cacheDebugStorage,
|
|
22
|
-
isCacheDebugEnabled,
|
|
23
|
-
recordCacheDebug,
|
|
24
|
-
} from '@prismakit/core';
|
|
25
|
-
|
|
26
17
|
export type { CacheAdapter } from '@prismakit/core';
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import Redis from 'ioredis';
|
|
2
2
|
import { gzipSync, gunzipSync } from 'zlib';
|
|
3
|
-
import
|
|
3
|
+
import {
|
|
4
|
+
setTaggedJsonOptions,
|
|
5
|
+
type CacheAdapter,
|
|
6
|
+
} from '@prismakit/core';
|
|
4
7
|
import {
|
|
5
8
|
redisJsonParse,
|
|
6
9
|
redisJsonStringify,
|
|
@@ -73,6 +76,9 @@ export class RedisCacheAdapter implements CacheAdapter {
|
|
|
73
76
|
this.compression = compression;
|
|
74
77
|
this.compressionThreshold = compressionThresholdBytes;
|
|
75
78
|
this.jsonOptions = { decimalFactory };
|
|
79
|
+
if (decimalFactory) {
|
|
80
|
+
setTaggedJsonOptions({ decimalFactory });
|
|
81
|
+
}
|
|
76
82
|
this.onError = onError;
|
|
77
83
|
this.client = url
|
|
78
84
|
? new Redis(url, { lazyConnect: true })
|
package/src/redis-json.ts
CHANGED
|
@@ -1,132 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* coerced to numbers.
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* decimalFactory: (s) => new Prisma.Decimal(s)
|
|
16
|
-
*/
|
|
17
|
-
decimalFactory?: DecimalFactory;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
21
|
-
return (
|
|
22
|
-
value !== null &&
|
|
23
|
-
typeof value === 'object' &&
|
|
24
|
-
!Array.isArray(value) &&
|
|
25
|
-
!(value instanceof Date) &&
|
|
26
|
-
!Buffer.isBuffer(value)
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function isDecimalLike(value: unknown): value is { toFixed: () => string } {
|
|
31
|
-
return (
|
|
32
|
-
isPlainObject(value) &&
|
|
33
|
-
typeof (value as { constructor?: { name?: string } }).constructor?.name ===
|
|
34
|
-
'string' &&
|
|
35
|
-
((value as { constructor: { name: string } }).constructor.name ===
|
|
36
|
-
'Decimal' ||
|
|
37
|
-
(typeof (value as { toFixed?: unknown }).toFixed === 'function' &&
|
|
38
|
-
typeof (value as { d?: unknown }).d === 'object'))
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** JSON.stringify replacer — Prisma BigInt / Date / Bytes / Decimal. */
|
|
43
|
-
export function redisJsonReplacer(_key: string, value: unknown): unknown {
|
|
44
|
-
if (typeof value === 'bigint') {
|
|
45
|
-
return { [BIGINT_TAG]: value.toString() };
|
|
46
|
-
}
|
|
47
|
-
if (value instanceof Date) {
|
|
48
|
-
return { [DATE_TAG]: value.toISOString() };
|
|
49
|
-
}
|
|
50
|
-
if (Buffer.isBuffer(value)) {
|
|
51
|
-
return { [BYTES_TAG]: value.toString('base64') };
|
|
52
|
-
}
|
|
53
|
-
if (
|
|
54
|
-
value &&
|
|
55
|
-
typeof value === 'object' &&
|
|
56
|
-
(value as { type?: string }).type === 'Buffer' &&
|
|
57
|
-
Array.isArray((value as { data?: unknown }).data)
|
|
58
|
-
) {
|
|
59
|
-
return {
|
|
60
|
-
[BYTES_TAG]: Buffer.from(
|
|
61
|
-
(value as { data: number[] }).data,
|
|
62
|
-
).toString('base64'),
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
if (isDecimalLike(value)) {
|
|
66
|
-
return { [DECIMAL_TAG]: value.toFixed() };
|
|
67
|
-
}
|
|
68
|
-
return value;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** JSON.parse reviver — restore tagged payloads from {@link redisJsonReplacer}. */
|
|
72
|
-
export function createRedisJsonReviver(options: RedisJsonOptions = {}) {
|
|
73
|
-
const decimalFactory = options.decimalFactory ?? ((s: string) => s);
|
|
74
|
-
return (_key: string, value: unknown): unknown => {
|
|
75
|
-
if (!isPlainObject(value)) return value;
|
|
76
|
-
const keys = Object.keys(value);
|
|
77
|
-
if (keys.length !== 1) return value;
|
|
78
|
-
|
|
79
|
-
if (BIGINT_TAG in value && typeof value[BIGINT_TAG] === 'string') {
|
|
80
|
-
return BigInt(value[BIGINT_TAG] as string);
|
|
81
|
-
}
|
|
82
|
-
if (DATE_TAG in value && typeof value[DATE_TAG] === 'string') {
|
|
83
|
-
return new Date(value[DATE_TAG] as string);
|
|
84
|
-
}
|
|
85
|
-
if (BYTES_TAG in value && typeof value[BYTES_TAG] === 'string') {
|
|
86
|
-
return Buffer.from(value[BYTES_TAG] as string, 'base64');
|
|
87
|
-
}
|
|
88
|
-
if (DECIMAL_TAG in value && typeof value[DECIMAL_TAG] === 'string') {
|
|
89
|
-
return decimalFactory(value[DECIMAL_TAG] as string);
|
|
90
|
-
}
|
|
91
|
-
return value;
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** @deprecated Prefer {@link createRedisJsonReviver} when a decimalFactory is needed. */
|
|
96
|
-
export function redisJsonReviver(_key: string, value: unknown): unknown {
|
|
97
|
-
return createRedisJsonReviver()(_key, value);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function redisJsonStringify(value: unknown): string {
|
|
101
|
-
return JSON.stringify(value, function (key, val) {
|
|
102
|
-
const holder = this as Record<string, unknown>;
|
|
103
|
-
const raw = key === '' ? value : holder[key];
|
|
104
|
-
if (raw instanceof Date) {
|
|
105
|
-
return redisJsonReplacer(key, raw);
|
|
106
|
-
}
|
|
107
|
-
return redisJsonReplacer(key, val);
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function redisJsonParse<T>(
|
|
112
|
-
raw: string,
|
|
113
|
-
options?: RedisJsonOptions,
|
|
114
|
-
): T {
|
|
115
|
-
return JSON.parse(raw, createRedisJsonReviver(options)) as T;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Deep clone via structuredClone, falling back to the tagged JSON codec so
|
|
120
|
-
* Date / BigInt / Buffer / Decimal survive (unlike plain JSON.stringify).
|
|
121
|
-
*/
|
|
122
|
-
export function cloneWithCodec<T>(value: T, options?: RedisJsonOptions): T {
|
|
123
|
-
if (value === null || value === undefined) return value;
|
|
124
|
-
if (typeof structuredClone === 'function') {
|
|
125
|
-
try {
|
|
126
|
-
return structuredClone(value);
|
|
127
|
-
} catch {
|
|
128
|
-
// Fall through for non-cloneable values (e.g. Decimal)
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return redisJsonParse(redisJsonStringify(value), options);
|
|
132
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
taggedJsonParse as redisJsonParse,
|
|
3
|
+
taggedJsonStringify as redisJsonStringify,
|
|
4
|
+
taggedJsonReplacer as redisJsonReplacer,
|
|
5
|
+
createTaggedJsonReviver as createRedisJsonReviver,
|
|
6
|
+
cloneWithCodec,
|
|
7
|
+
setTaggedJsonOptions,
|
|
8
|
+
getTaggedJsonOptions,
|
|
9
|
+
type DecimalFactory,
|
|
10
|
+
type TaggedJsonOptions as RedisJsonOptions,
|
|
11
|
+
} from '@prismakit/core';
|