@prismakit/redis 4.0.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 +4 -7
- package/dist/index.cjs +13 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -27
- package/dist/index.d.ts +3 -27
- package/dist/index.js +22 -83
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/redis-cache-adapter.spec.ts +20 -0
- package/src/redis-cache-adapter.ts +7 -1
- package/src/redis-json.ts +11 -127
package/README.md
CHANGED
|
@@ -17,11 +17,13 @@ pnpm add @prismakit/redis ioredis
|
|
|
17
17
|
## Usage
|
|
18
18
|
|
|
19
19
|
```typescript
|
|
20
|
+
import { Prisma } from '@prisma/client';
|
|
20
21
|
import { RedisCacheAdapter } from '@prismakit/redis';
|
|
21
22
|
|
|
22
23
|
const cache = new RedisCacheAdapter({
|
|
23
24
|
url: process.env.REDIS_URL, // or host + port
|
|
24
25
|
prefix: 'myapp',
|
|
26
|
+
decimalFactory: (s) => new Prisma.Decimal(s),
|
|
25
27
|
});
|
|
26
28
|
```
|
|
27
29
|
|
|
@@ -33,6 +35,7 @@ const cache = new RedisCacheAdapter({
|
|
|
33
35
|
| `host` | `localhost` | Used when `url` is omitted |
|
|
34
36
|
| `port` | `6379` | Used when `url` is omitted |
|
|
35
37
|
| `prefix` | `prismakit` | Key prefix for all cache keys |
|
|
38
|
+
| `decimalFactory` | keep string | `(s) => new Prisma.Decimal(s)` so Decimal matches native Prisma |
|
|
36
39
|
|
|
37
40
|
### Wire it up
|
|
38
41
|
|
|
@@ -53,13 +56,7 @@ PrismaKitModule.forRoot({
|
|
|
53
56
|
|
|
54
57
|
### JSON codec
|
|
55
58
|
|
|
56
|
-
Tagged JSON for `Date`, `BigInt`, `Bytes`, and Prisma `Decimal`.
|
|
57
|
-
|
|
58
|
-
```typescript
|
|
59
|
-
import { createRedisJsonReviver } from '@prismakit/redis';
|
|
60
|
-
|
|
61
|
-
const reviver = createRedisJsonReviver({ /* DecimalFactory? */ });
|
|
62
|
-
```
|
|
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`.
|
|
63
60
|
|
|
64
61
|
Cache debug (`CACHE_DEBUG=true`, `cacheDebugStorage`, …) lives on `@prismakit/core`.
|
|
65
62
|
|
package/dist/index.cjs
CHANGED
|
@@ -31,95 +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
|
-
cloneWithCodec: () => cloneWithCodec,
|
|
35
|
-
createRedisJsonReviver: () =>
|
|
36
|
-
redisJsonParse: () =>
|
|
37
|
-
redisJsonReplacer: () =>
|
|
38
|
-
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
|
|
39
39
|
});
|
|
40
40
|
module.exports = __toCommonJS(index_exports);
|
|
41
41
|
|
|
42
42
|
// src/redis-cache-adapter.ts
|
|
43
43
|
var import_ioredis = __toESM(require("ioredis"), 1);
|
|
44
44
|
var import_zlib = require("zlib");
|
|
45
|
+
var import_core2 = require("@prismakit/core");
|
|
45
46
|
|
|
46
47
|
// src/redis-json.ts
|
|
47
|
-
var
|
|
48
|
-
var DATE_TAG = "__date";
|
|
49
|
-
var BYTES_TAG = "__bytes";
|
|
50
|
-
var DECIMAL_TAG = "__decimal";
|
|
51
|
-
function isPlainObject(value) {
|
|
52
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date) && !Buffer.isBuffer(value);
|
|
53
|
-
}
|
|
54
|
-
function isDecimalLike(value) {
|
|
55
|
-
return isPlainObject(value) && typeof value.constructor?.name === "string" && (value.constructor.name === "Decimal" || typeof value.toFixed === "function" && typeof value.d === "object");
|
|
56
|
-
}
|
|
57
|
-
function redisJsonReplacer(_key, value) {
|
|
58
|
-
if (typeof value === "bigint") {
|
|
59
|
-
return { [BIGINT_TAG]: value.toString() };
|
|
60
|
-
}
|
|
61
|
-
if (value instanceof Date) {
|
|
62
|
-
return { [DATE_TAG]: value.toISOString() };
|
|
63
|
-
}
|
|
64
|
-
if (Buffer.isBuffer(value)) {
|
|
65
|
-
return { [BYTES_TAG]: value.toString("base64") };
|
|
66
|
-
}
|
|
67
|
-
if (value && typeof value === "object" && value.type === "Buffer" && Array.isArray(value.data)) {
|
|
68
|
-
return {
|
|
69
|
-
[BYTES_TAG]: Buffer.from(
|
|
70
|
-
value.data
|
|
71
|
-
).toString("base64")
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
if (isDecimalLike(value)) {
|
|
75
|
-
return { [DECIMAL_TAG]: value.toFixed() };
|
|
76
|
-
}
|
|
77
|
-
return value;
|
|
78
|
-
}
|
|
79
|
-
function createRedisJsonReviver(options = {}) {
|
|
80
|
-
const decimalFactory = options.decimalFactory ?? ((s) => s);
|
|
81
|
-
return (_key, value) => {
|
|
82
|
-
if (!isPlainObject(value)) return value;
|
|
83
|
-
const keys = Object.keys(value);
|
|
84
|
-
if (keys.length !== 1) return value;
|
|
85
|
-
if (BIGINT_TAG in value && typeof value[BIGINT_TAG] === "string") {
|
|
86
|
-
return BigInt(value[BIGINT_TAG]);
|
|
87
|
-
}
|
|
88
|
-
if (DATE_TAG in value && typeof value[DATE_TAG] === "string") {
|
|
89
|
-
return new Date(value[DATE_TAG]);
|
|
90
|
-
}
|
|
91
|
-
if (BYTES_TAG in value && typeof value[BYTES_TAG] === "string") {
|
|
92
|
-
return Buffer.from(value[BYTES_TAG], "base64");
|
|
93
|
-
}
|
|
94
|
-
if (DECIMAL_TAG in value && typeof value[DECIMAL_TAG] === "string") {
|
|
95
|
-
return decimalFactory(value[DECIMAL_TAG]);
|
|
96
|
-
}
|
|
97
|
-
return value;
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
function redisJsonStringify(value) {
|
|
101
|
-
return JSON.stringify(value, function(key, val) {
|
|
102
|
-
const holder = this;
|
|
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
|
-
function redisJsonParse(raw, options) {
|
|
111
|
-
return JSON.parse(raw, createRedisJsonReviver(options));
|
|
112
|
-
}
|
|
113
|
-
function cloneWithCodec(value, options) {
|
|
114
|
-
if (value === null || value === void 0) return value;
|
|
115
|
-
if (typeof structuredClone === "function") {
|
|
116
|
-
try {
|
|
117
|
-
return structuredClone(value);
|
|
118
|
-
} catch {
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return redisJsonParse(redisJsonStringify(value), options);
|
|
122
|
-
}
|
|
48
|
+
var import_core = require("@prismakit/core");
|
|
123
49
|
|
|
124
50
|
// src/redis-cache-adapter.ts
|
|
125
51
|
var INDEX_TTL_BUFFER = 60;
|
|
@@ -155,6 +81,9 @@ var RedisCacheAdapter = class {
|
|
|
155
81
|
this.compression = compression;
|
|
156
82
|
this.compressionThreshold = compressionThresholdBytes;
|
|
157
83
|
this.jsonOptions = { decimalFactory };
|
|
84
|
+
if (decimalFactory) {
|
|
85
|
+
(0, import_core2.setTaggedJsonOptions)({ decimalFactory });
|
|
86
|
+
}
|
|
158
87
|
this.onError = onError;
|
|
159
88
|
this.client = url ? new import_ioredis.default(url, { lazyConnect: true }) : new import_ioredis.default({ host, port, lazyConnect: true });
|
|
160
89
|
this.client.on("ready", () => {
|
|
@@ -200,7 +129,7 @@ var RedisCacheAdapter = class {
|
|
|
200
129
|
return this.prefix;
|
|
201
130
|
}
|
|
202
131
|
encode(value) {
|
|
203
|
-
const json =
|
|
132
|
+
const json = (0, import_core.taggedJsonStringify)(value);
|
|
204
133
|
if (this.compression === "gzip" && Buffer.byteLength(json, "utf8") >= this.compressionThreshold) {
|
|
205
134
|
const compressed = (0, import_zlib.gzipSync)(Buffer.from(json, "utf8")).toString("base64");
|
|
206
135
|
return COMPRESSED_PREFIX + compressed;
|
|
@@ -211,9 +140,9 @@ var RedisCacheAdapter = class {
|
|
|
211
140
|
if (raw.startsWith(COMPRESSED_PREFIX)) {
|
|
212
141
|
const buf = Buffer.from(raw.slice(COMPRESSED_PREFIX.length), "base64");
|
|
213
142
|
const json = (0, import_zlib.gunzipSync)(buf).toString("utf8");
|
|
214
|
-
return
|
|
143
|
+
return (0, import_core.taggedJsonParse)(json, this.jsonOptions);
|
|
215
144
|
}
|
|
216
|
-
return
|
|
145
|
+
return (0, import_core.taggedJsonParse)(raw, this.jsonOptions);
|
|
217
146
|
}
|
|
218
147
|
async get(key) {
|
|
219
148
|
const raw = await this.client.get(key);
|
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 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 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\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,qBAAkB;AAClB,kBAAqC;;;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;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;;;ADpHA,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,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,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,iBAAa,sBAAS,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,WAAO,wBAAW,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;","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,29 +1,5 @@
|
|
|
1
|
-
import { CacheAdapter } from '@prismakit/core';
|
|
2
|
-
export { CacheAdapter } from '@prismakit/core';
|
|
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
|
-
declare function redisJsonStringify(value: unknown): string;
|
|
21
|
-
declare function redisJsonParse<T>(raw: string, options?: RedisJsonOptions): T;
|
|
22
|
-
/**
|
|
23
|
-
* Deep clone via structuredClone, falling back to the tagged JSON codec so
|
|
24
|
-
* Date / BigInt / Buffer / Decimal survive (unlike plain JSON.stringify).
|
|
25
|
-
*/
|
|
26
|
-
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';
|
|
27
3
|
|
|
28
4
|
type RedisCompression = 'none' | 'gzip';
|
|
29
5
|
type RedisCacheAdapterOptions = {
|
|
@@ -84,4 +60,4 @@ declare class RedisCacheAdapter implements CacheAdapter {
|
|
|
84
60
|
safeSmembers(key: string): Promise<string[]>;
|
|
85
61
|
}
|
|
86
62
|
|
|
87
|
-
export {
|
|
63
|
+
export { RedisCacheAdapter, type RedisCacheAdapterOptions, type RedisCompression };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,29 +1,5 @@
|
|
|
1
|
-
import { CacheAdapter } from '@prismakit/core';
|
|
2
|
-
export { CacheAdapter } from '@prismakit/core';
|
|
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
|
-
declare function redisJsonStringify(value: unknown): string;
|
|
21
|
-
declare function redisJsonParse<T>(raw: string, options?: RedisJsonOptions): T;
|
|
22
|
-
/**
|
|
23
|
-
* Deep clone via structuredClone, falling back to the tagged JSON codec so
|
|
24
|
-
* Date / BigInt / Buffer / Decimal survive (unlike plain JSON.stringify).
|
|
25
|
-
*/
|
|
26
|
-
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';
|
|
27
3
|
|
|
28
4
|
type RedisCompression = 'none' | 'gzip';
|
|
29
5
|
type RedisCacheAdapterOptions = {
|
|
@@ -84,4 +60,4 @@ declare class RedisCacheAdapter implements CacheAdapter {
|
|
|
84
60
|
safeSmembers(key: string): Promise<string[]>;
|
|
85
61
|
}
|
|
86
62
|
|
|
87
|
-
export {
|
|
63
|
+
export { RedisCacheAdapter, type RedisCacheAdapterOptions, type RedisCompression };
|
package/dist/index.js
CHANGED
|
@@ -1,84 +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 redisJsonStringify(value) {
|
|
60
|
-
return JSON.stringify(value, function(key, val) {
|
|
61
|
-
const holder = this;
|
|
62
|
-
const raw = key === "" ? value : holder[key];
|
|
63
|
-
if (raw instanceof Date) {
|
|
64
|
-
return redisJsonReplacer(key, raw);
|
|
65
|
-
}
|
|
66
|
-
return redisJsonReplacer(key, val);
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
function redisJsonParse(raw, options) {
|
|
70
|
-
return JSON.parse(raw, createRedisJsonReviver(options));
|
|
71
|
-
}
|
|
72
|
-
function cloneWithCodec(value, options) {
|
|
73
|
-
if (value === null || value === void 0) return value;
|
|
74
|
-
if (typeof structuredClone === "function") {
|
|
75
|
-
try {
|
|
76
|
-
return structuredClone(value);
|
|
77
|
-
} catch {
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return redisJsonParse(redisJsonStringify(value), options);
|
|
81
|
-
}
|
|
9
|
+
import {
|
|
10
|
+
taggedJsonParse,
|
|
11
|
+
taggedJsonStringify,
|
|
12
|
+
taggedJsonReplacer,
|
|
13
|
+
createTaggedJsonReviver,
|
|
14
|
+
cloneWithCodec,
|
|
15
|
+
setTaggedJsonOptions,
|
|
16
|
+
getTaggedJsonOptions
|
|
17
|
+
} from "@prismakit/core";
|
|
82
18
|
|
|
83
19
|
// src/redis-cache-adapter.ts
|
|
84
20
|
var INDEX_TTL_BUFFER = 60;
|
|
@@ -114,6 +50,9 @@ var RedisCacheAdapter = class {
|
|
|
114
50
|
this.compression = compression;
|
|
115
51
|
this.compressionThreshold = compressionThresholdBytes;
|
|
116
52
|
this.jsonOptions = { decimalFactory };
|
|
53
|
+
if (decimalFactory) {
|
|
54
|
+
setTaggedJsonOptions2({ decimalFactory });
|
|
55
|
+
}
|
|
117
56
|
this.onError = onError;
|
|
118
57
|
this.client = url ? new Redis(url, { lazyConnect: true }) : new Redis({ host, port, lazyConnect: true });
|
|
119
58
|
this.client.on("ready", () => {
|
|
@@ -159,7 +98,7 @@ var RedisCacheAdapter = class {
|
|
|
159
98
|
return this.prefix;
|
|
160
99
|
}
|
|
161
100
|
encode(value) {
|
|
162
|
-
const json =
|
|
101
|
+
const json = taggedJsonStringify(value);
|
|
163
102
|
if (this.compression === "gzip" && Buffer.byteLength(json, "utf8") >= this.compressionThreshold) {
|
|
164
103
|
const compressed = gzipSync(Buffer.from(json, "utf8")).toString("base64");
|
|
165
104
|
return COMPRESSED_PREFIX + compressed;
|
|
@@ -170,9 +109,9 @@ var RedisCacheAdapter = class {
|
|
|
170
109
|
if (raw.startsWith(COMPRESSED_PREFIX)) {
|
|
171
110
|
const buf = Buffer.from(raw.slice(COMPRESSED_PREFIX.length), "base64");
|
|
172
111
|
const json = gunzipSync(buf).toString("utf8");
|
|
173
|
-
return
|
|
112
|
+
return taggedJsonParse(json, this.jsonOptions);
|
|
174
113
|
}
|
|
175
|
-
return
|
|
114
|
+
return taggedJsonParse(raw, this.jsonOptions);
|
|
176
115
|
}
|
|
177
116
|
async get(key) {
|
|
178
117
|
const raw = await this.client.get(key);
|
|
@@ -277,9 +216,9 @@ var RedisCacheAdapter = class {
|
|
|
277
216
|
export {
|
|
278
217
|
RedisCacheAdapter,
|
|
279
218
|
cloneWithCodec,
|
|
280
|
-
createRedisJsonReviver,
|
|
281
|
-
redisJsonParse,
|
|
282
|
-
redisJsonReplacer,
|
|
283
|
-
redisJsonStringify
|
|
219
|
+
createTaggedJsonReviver as createRedisJsonReviver,
|
|
220
|
+
taggedJsonParse as redisJsonParse,
|
|
221
|
+
taggedJsonReplacer as redisJsonReplacer,
|
|
222
|
+
taggedJsonStringify as redisJsonStringify
|
|
284
223
|
};
|
|
285
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"],"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\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":[]}
|
|
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": "4.0.
|
|
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": "4.0.
|
|
27
|
+
"@prismakit/core": "4.0.1"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@prismakit/core": ">=4.0.
|
|
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;
|
|
@@ -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,127 +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
|
-
export function redisJsonStringify(value: unknown): string {
|
|
96
|
-
return JSON.stringify(value, function (key, val) {
|
|
97
|
-
const holder = this as Record<string, unknown>;
|
|
98
|
-
const raw = key === '' ? value : holder[key];
|
|
99
|
-
if (raw instanceof Date) {
|
|
100
|
-
return redisJsonReplacer(key, raw);
|
|
101
|
-
}
|
|
102
|
-
return redisJsonReplacer(key, val);
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function redisJsonParse<T>(
|
|
107
|
-
raw: string,
|
|
108
|
-
options?: RedisJsonOptions,
|
|
109
|
-
): T {
|
|
110
|
-
return JSON.parse(raw, createRedisJsonReviver(options)) as T;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Deep clone via structuredClone, falling back to the tagged JSON codec so
|
|
115
|
-
* Date / BigInt / Buffer / Decimal survive (unlike plain JSON.stringify).
|
|
116
|
-
*/
|
|
117
|
-
export function cloneWithCodec<T>(value: T, options?: RedisJsonOptions): T {
|
|
118
|
-
if (value === null || value === undefined) return value;
|
|
119
|
-
if (typeof structuredClone === 'function') {
|
|
120
|
-
try {
|
|
121
|
-
return structuredClone(value);
|
|
122
|
-
} catch {
|
|
123
|
-
// Fall through for non-cloneable values (e.g. Decimal)
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return redisJsonParse(redisJsonStringify(value), options);
|
|
127
|
-
}
|
|
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';
|