@forklaunch/infrastructure-redis 1.3.14 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/eject/infrastructure/redis.ts +105 -186
- package/lib/index.d.mts +14 -149
- package/lib/index.d.ts +14 -149
- package/lib/index.js +61 -178
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +61 -180
- package/lib/index.mjs.map +1 -1
- package/package.json +3 -3
package/lib/index.mjs
CHANGED
|
@@ -3,9 +3,6 @@ import { safeParse, safeStringify } from "@forklaunch/common";
|
|
|
3
3
|
import {
|
|
4
4
|
evaluateTelemetryOptions
|
|
5
5
|
} from "@forklaunch/core/http";
|
|
6
|
-
import {
|
|
7
|
-
getCurrentTenantId
|
|
8
|
-
} from "@forklaunch/core/persistence";
|
|
9
6
|
import { createClient } from "redis";
|
|
10
7
|
var ENCRYPTED_PREFIXES = ["v1:", "v2:"];
|
|
11
8
|
function isEncrypted(value) {
|
|
@@ -19,7 +16,7 @@ var RedisTtlCache = class {
|
|
|
19
16
|
* @param {OpenTelemetryCollector<MetricsDefinition>} openTelemetryCollector - Collector for OpenTelemetry metrics
|
|
20
17
|
* @param {RedisClientOptions} options - Configuration options for the Redis client
|
|
21
18
|
* @param {TelemetryOptions} telemetryOptions - Configuration options for telemetry
|
|
22
|
-
* @param {RedisCacheEncryptionOptions} encryption - Encryption configuration
|
|
19
|
+
* @param {RedisCacheEncryptionOptions} encryption - Encryption configuration
|
|
23
20
|
*/
|
|
24
21
|
constructor(ttlMilliseconds, openTelemetryCollector, options, telemetryOptions, encryption) {
|
|
25
22
|
this.ttlMilliseconds = ttlMilliseconds;
|
|
@@ -27,46 +24,38 @@ var RedisTtlCache = class {
|
|
|
27
24
|
this.telemetryOptions = evaluateTelemetryOptions(telemetryOptions);
|
|
28
25
|
this.client = createClient(options);
|
|
29
26
|
this.encryptor = encryption.encryptor;
|
|
30
|
-
this.encryptionDisabled = encryption.disabled ?? false;
|
|
31
27
|
if (this.telemetryOptions.enabled.logging) {
|
|
32
28
|
this.client.on("error", (err) => this.openTelemetryCollector.error(err));
|
|
33
29
|
this.client.connect().catch(this.openTelemetryCollector.error);
|
|
34
30
|
}
|
|
35
31
|
}
|
|
32
|
+
ttlMilliseconds;
|
|
33
|
+
openTelemetryCollector;
|
|
36
34
|
client;
|
|
37
35
|
telemetryOptions;
|
|
38
36
|
encryptor;
|
|
39
|
-
encryptionDisabled;
|
|
40
37
|
// ---------------------------------------------------------------------------
|
|
41
|
-
// Encryption helpers
|
|
38
|
+
// Encryption helpers — only active when compliance context is provided
|
|
42
39
|
// ---------------------------------------------------------------------------
|
|
43
|
-
encryptValue(serialized) {
|
|
44
|
-
if (!
|
|
45
|
-
return this.encryptor.encrypt(serialized,
|
|
40
|
+
encryptValue(serialized, compliance) {
|
|
41
|
+
if (!compliance || !this.encryptor) return serialized;
|
|
42
|
+
return this.encryptor.encrypt(serialized, compliance.tenantId) ?? serialized;
|
|
46
43
|
}
|
|
47
|
-
decryptValue(value) {
|
|
48
|
-
if (!
|
|
44
|
+
decryptValue(value, compliance) {
|
|
45
|
+
if (!compliance || !this.encryptor) return value;
|
|
49
46
|
if (!isEncrypted(value)) return value;
|
|
50
47
|
try {
|
|
51
|
-
return this.encryptor.decrypt(value,
|
|
48
|
+
return this.encryptor.decrypt(value, compliance.tenantId) ?? value;
|
|
52
49
|
} catch {
|
|
53
50
|
return value;
|
|
54
51
|
}
|
|
55
52
|
}
|
|
56
|
-
|
|
57
|
-
* Parses a raw Redis reply into the expected type.
|
|
58
|
-
* Handles null values, arrays, buffers, and JSON strings.
|
|
59
|
-
*
|
|
60
|
-
* @template T - The expected type of the parsed value
|
|
61
|
-
* @param {RedisCommandRawReply} value - The raw value from Redis to parse
|
|
62
|
-
* @returns {T} The parsed value cast to type T
|
|
63
|
-
*/
|
|
64
|
-
parseValue(value) {
|
|
53
|
+
parseValue(value, compliance) {
|
|
65
54
|
if (value == null) {
|
|
66
55
|
return null;
|
|
67
56
|
}
|
|
68
57
|
if (Array.isArray(value)) {
|
|
69
|
-
return value.map((v) => this.parseValue(v));
|
|
58
|
+
return value.map((v) => this.parseValue(v, compliance));
|
|
70
59
|
}
|
|
71
60
|
if (Buffer.isBuffer(value)) {
|
|
72
61
|
return value.toJSON();
|
|
@@ -74,90 +63,54 @@ var RedisTtlCache = class {
|
|
|
74
63
|
switch (typeof value) {
|
|
75
64
|
case "object":
|
|
76
65
|
case "string":
|
|
77
|
-
return safeParse(this.decryptValue(String(value)));
|
|
66
|
+
return safeParse(this.decryptValue(String(value), compliance));
|
|
78
67
|
case "number":
|
|
79
68
|
return value;
|
|
80
69
|
}
|
|
81
70
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
* @param {TtlCacheRecord<T>} param0 - The cache record containing key, value and optional TTL
|
|
87
|
-
* @param {string} param0.key - The key to store the value under
|
|
88
|
-
* @param {T} param0.value - The value to cache
|
|
89
|
-
* @param {number} [param0.ttlMilliseconds] - Optional TTL in milliseconds, defaults to constructor value
|
|
90
|
-
* @returns {Promise<void>} A promise that resolves when the value is cached
|
|
91
|
-
*/
|
|
92
|
-
async putRecord({
|
|
93
|
-
key,
|
|
94
|
-
value,
|
|
95
|
-
ttlMilliseconds = this.ttlMilliseconds
|
|
96
|
-
}) {
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// TtlCache implementation
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
async putRecord({ key, value, ttlMilliseconds = this.ttlMilliseconds }, compliance) {
|
|
97
75
|
if (this.telemetryOptions.enabled.logging) {
|
|
98
76
|
this.openTelemetryCollector.info(`Putting record into cache: ${key}`);
|
|
99
77
|
}
|
|
100
|
-
await this.client.set(
|
|
101
|
-
|
|
102
|
-
|
|
78
|
+
await this.client.set(
|
|
79
|
+
key,
|
|
80
|
+
this.encryptValue(safeStringify(value), compliance),
|
|
81
|
+
{ PX: ttlMilliseconds }
|
|
82
|
+
);
|
|
103
83
|
}
|
|
104
|
-
|
|
105
|
-
* Puts multiple records into the Redis cache in a single transaction.
|
|
106
|
-
*
|
|
107
|
-
* @template T - The type of values being cached
|
|
108
|
-
* @param {TtlCacheRecord<T>[]} cacheRecords - Array of cache records to store
|
|
109
|
-
* @returns {Promise<void>} A promise that resolves when all values are cached
|
|
110
|
-
*/
|
|
111
|
-
async putBatchRecords(cacheRecords) {
|
|
84
|
+
async putBatchRecords(cacheRecords, compliance) {
|
|
112
85
|
const multiCommand = this.client.multi();
|
|
113
86
|
for (const { key, value, ttlMilliseconds } of cacheRecords) {
|
|
114
|
-
multiCommand.set(
|
|
115
|
-
|
|
116
|
-
|
|
87
|
+
multiCommand.set(
|
|
88
|
+
key,
|
|
89
|
+
this.encryptValue(safeStringify(value), compliance),
|
|
90
|
+
{ PX: ttlMilliseconds || this.ttlMilliseconds }
|
|
91
|
+
);
|
|
117
92
|
}
|
|
118
93
|
await multiCommand.exec();
|
|
119
94
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
* @param {T} value - The value to add to the list
|
|
126
|
-
* @returns {Promise<void>} A promise that resolves when the value is enqueued
|
|
127
|
-
*/
|
|
128
|
-
async enqueueRecord(queueName, value) {
|
|
129
|
-
await this.client.lPush(queueName, this.encryptValue(safeStringify(value)));
|
|
95
|
+
async enqueueRecord(queueName, value, compliance) {
|
|
96
|
+
await this.client.lPush(
|
|
97
|
+
queueName,
|
|
98
|
+
this.encryptValue(safeStringify(value), compliance)
|
|
99
|
+
);
|
|
130
100
|
}
|
|
131
|
-
|
|
132
|
-
* Adds multiple values to the left end of a Redis list in a single transaction.
|
|
133
|
-
*
|
|
134
|
-
* @template T - The type of values being enqueued
|
|
135
|
-
* @param {string} queueName - The name of the Redis list
|
|
136
|
-
* @param {T[]} values - Array of values to add to the list
|
|
137
|
-
* @returns {Promise<void>} A promise that resolves when all values are enqueued
|
|
138
|
-
*/
|
|
139
|
-
async enqueueBatchRecords(queueName, values) {
|
|
101
|
+
async enqueueBatchRecords(queueName, values, compliance) {
|
|
140
102
|
const multiCommand = this.client.multi();
|
|
141
103
|
for (const value of values) {
|
|
142
|
-
multiCommand.lPush(
|
|
104
|
+
multiCommand.lPush(
|
|
105
|
+
queueName,
|
|
106
|
+
this.encryptValue(safeStringify(value), compliance)
|
|
107
|
+
);
|
|
143
108
|
}
|
|
144
109
|
await multiCommand.exec();
|
|
145
110
|
}
|
|
146
|
-
/**
|
|
147
|
-
* Deletes a record from the Redis cache.
|
|
148
|
-
*
|
|
149
|
-
* @param {string} cacheRecordKey - The key of the record to delete
|
|
150
|
-
* @returns {Promise<void>} A promise that resolves when the record is deleted
|
|
151
|
-
*/
|
|
152
111
|
async deleteRecord(cacheRecordKey) {
|
|
153
112
|
await this.client.del(cacheRecordKey);
|
|
154
113
|
}
|
|
155
|
-
/**
|
|
156
|
-
* Deletes multiple records from the Redis cache in a single transaction.
|
|
157
|
-
*
|
|
158
|
-
* @param {string[]} cacheRecordKeys - Array of keys to delete
|
|
159
|
-
* @returns {Promise<void>} A promise that resolves when all records are deleted
|
|
160
|
-
*/
|
|
161
114
|
async deleteBatchRecords(cacheRecordKeys) {
|
|
162
115
|
const multiCommand = this.client.multi();
|
|
163
116
|
for (const key of cacheRecordKeys) {
|
|
@@ -165,66 +118,41 @@ var RedisTtlCache = class {
|
|
|
165
118
|
}
|
|
166
119
|
await multiCommand.exec();
|
|
167
120
|
}
|
|
168
|
-
|
|
169
|
-
* Removes and returns the rightmost element from a Redis list.
|
|
170
|
-
*
|
|
171
|
-
* @template T - The type of value being dequeued
|
|
172
|
-
* @param {string} queueName - The name of the Redis list
|
|
173
|
-
* @returns {Promise<T>} A promise that resolves with the dequeued value
|
|
174
|
-
* @throws {Error} If the queue is empty
|
|
175
|
-
*/
|
|
176
|
-
async dequeueRecord(queueName) {
|
|
121
|
+
async dequeueRecord(queueName, compliance) {
|
|
177
122
|
const value = await this.client.rPop(queueName);
|
|
178
123
|
if (value === null) {
|
|
179
124
|
throw new Error(`Queue is empty: ${queueName}`);
|
|
180
125
|
}
|
|
181
|
-
return safeParse(this.decryptValue(value));
|
|
126
|
+
return safeParse(this.decryptValue(value, compliance));
|
|
182
127
|
}
|
|
183
|
-
|
|
184
|
-
* Removes and returns multiple elements from the right end of a Redis list.
|
|
185
|
-
*
|
|
186
|
-
* @template T - The type of values being dequeued
|
|
187
|
-
* @param {string} queueName - The name of the Redis list
|
|
188
|
-
* @param {number} pageSize - Maximum number of elements to dequeue
|
|
189
|
-
* @returns {Promise<T[]>} A promise that resolves with an array of dequeued values
|
|
190
|
-
*/
|
|
191
|
-
async dequeueBatchRecords(queueName, pageSize) {
|
|
128
|
+
async dequeueBatchRecords(queueName, pageSize, compliance) {
|
|
192
129
|
const multiCommand = this.client.multi();
|
|
193
130
|
for (let i = 0; i < pageSize; i++) {
|
|
194
131
|
multiCommand.rPop(queueName);
|
|
195
132
|
}
|
|
196
133
|
const values = await multiCommand.exec();
|
|
197
134
|
return values.map(
|
|
198
|
-
(value) => this.parseValue(value)
|
|
135
|
+
(value) => this.parseValue(value, compliance)
|
|
199
136
|
).filter(Boolean);
|
|
200
137
|
}
|
|
201
|
-
|
|
202
|
-
* Reads a record from the Redis cache.
|
|
203
|
-
*
|
|
204
|
-
* @template T - The type of value being read
|
|
205
|
-
* @param {string} cacheRecordKey - The key of the record to read
|
|
206
|
-
* @returns {Promise<TtlCacheRecord<T>>} A promise that resolves with the cache record
|
|
207
|
-
* @throws {Error} If the record is not found
|
|
208
|
-
*/
|
|
209
|
-
async readRecord(cacheRecordKey) {
|
|
138
|
+
async readRecord(cacheRecordKey, compliance) {
|
|
210
139
|
const [value, ttl] = await this.client.multi().get(cacheRecordKey).ttl(cacheRecordKey).exec();
|
|
211
140
|
if (value === null) {
|
|
212
141
|
throw new Error(`Record not found for key: ${cacheRecordKey}`);
|
|
213
142
|
}
|
|
214
143
|
return {
|
|
215
144
|
key: cacheRecordKey,
|
|
216
|
-
value: this.parseValue(
|
|
217
|
-
|
|
145
|
+
value: this.parseValue(
|
|
146
|
+
value,
|
|
147
|
+
compliance
|
|
148
|
+
),
|
|
149
|
+
ttlMilliseconds: this.parseValue(
|
|
150
|
+
ttl,
|
|
151
|
+
compliance
|
|
152
|
+
) * 1e3
|
|
218
153
|
};
|
|
219
154
|
}
|
|
220
|
-
|
|
221
|
-
* Reads multiple records from the Redis cache.
|
|
222
|
-
*
|
|
223
|
-
* @template T - The type of values being read
|
|
224
|
-
* @param {string[] | string} cacheRecordKeysOrPrefix - Array of keys to read, or a prefix pattern
|
|
225
|
-
* @returns {Promise<TtlCacheRecord<T>[]>} A promise that resolves with an array of cache records
|
|
226
|
-
*/
|
|
227
|
-
async readBatchRecords(cacheRecordKeysOrPrefix) {
|
|
155
|
+
async readBatchRecords(cacheRecordKeysOrPrefix, compliance) {
|
|
228
156
|
const keys = Array.isArray(cacheRecordKeysOrPrefix) ? cacheRecordKeysOrPrefix : await this.client.keys(cacheRecordKeysOrPrefix + "*");
|
|
229
157
|
const multiCommand = this.client.multi();
|
|
230
158
|
for (const key of keys) {
|
|
@@ -235,10 +163,12 @@ var RedisTtlCache = class {
|
|
|
235
163
|
return values.reduce((acc, value, index) => {
|
|
236
164
|
if (index % 2 === 0) {
|
|
237
165
|
const maybeValue = this.parseValue(
|
|
238
|
-
value
|
|
166
|
+
value,
|
|
167
|
+
compliance
|
|
239
168
|
);
|
|
240
169
|
const ttl = this.parseValue(
|
|
241
|
-
values[index + 1]
|
|
170
|
+
values[index + 1],
|
|
171
|
+
compliance
|
|
242
172
|
);
|
|
243
173
|
if (maybeValue && ttl) {
|
|
244
174
|
acc.push({
|
|
@@ -251,32 +181,13 @@ var RedisTtlCache = class {
|
|
|
251
181
|
return acc;
|
|
252
182
|
}, []);
|
|
253
183
|
}
|
|
254
|
-
/**
|
|
255
|
-
* Lists all keys in the Redis cache that match a pattern prefix.
|
|
256
|
-
*
|
|
257
|
-
* @param {string} pattern_prefix - The prefix pattern to match keys against
|
|
258
|
-
* @returns {Promise<string[]>} A promise that resolves with an array of matching keys
|
|
259
|
-
*/
|
|
260
184
|
async listKeys(pattern_prefix) {
|
|
261
|
-
|
|
262
|
-
return keys;
|
|
185
|
+
return this.client.keys(pattern_prefix + "*");
|
|
263
186
|
}
|
|
264
|
-
/**
|
|
265
|
-
* Checks if a record exists in the Redis cache.
|
|
266
|
-
*
|
|
267
|
-
* @param {string} cacheRecordKey - The key to check
|
|
268
|
-
* @returns {Promise<boolean>} A promise that resolves with true if the record exists, false otherwise
|
|
269
|
-
*/
|
|
270
187
|
async peekRecord(cacheRecordKey) {
|
|
271
188
|
const result = await this.client.exists(cacheRecordKey);
|
|
272
189
|
return result === 1;
|
|
273
190
|
}
|
|
274
|
-
/**
|
|
275
|
-
* Checks if multiple records exist in the Redis cache.
|
|
276
|
-
*
|
|
277
|
-
* @param {string[] | string} cacheRecordKeysOrPrefix - Array of keys to check, or a prefix pattern
|
|
278
|
-
* @returns {Promise<boolean[]>} A promise that resolves with an array of existence booleans
|
|
279
|
-
*/
|
|
280
191
|
async peekBatchRecords(cacheRecordKeysOrPrefix) {
|
|
281
192
|
const keys = Array.isArray(cacheRecordKeysOrPrefix) ? cacheRecordKeysOrPrefix : await this.client.keys(cacheRecordKeysOrPrefix + "*");
|
|
282
193
|
const multiCommand = this.client.multi();
|
|
@@ -286,50 +197,20 @@ var RedisTtlCache = class {
|
|
|
286
197
|
const results = await multiCommand.exec();
|
|
287
198
|
return results.map((result) => result === 1);
|
|
288
199
|
}
|
|
289
|
-
|
|
290
|
-
* Peeks at a record in the Redis cache.
|
|
291
|
-
*
|
|
292
|
-
* @template T - The type of value being peeked at
|
|
293
|
-
* @param {string} queueName - The name of the Redis queue
|
|
294
|
-
* @returns {Promise<T>} A promise that resolves with the peeked value
|
|
295
|
-
*/
|
|
296
|
-
async peekQueueRecord(queueName) {
|
|
200
|
+
async peekQueueRecord(queueName, compliance) {
|
|
297
201
|
const value = await this.client.lRange(queueName, 0, 0);
|
|
298
|
-
return this.parseValue(value[0]);
|
|
202
|
+
return this.parseValue(value[0], compliance);
|
|
299
203
|
}
|
|
300
|
-
|
|
301
|
-
* Peeks at multiple records in the Redis cache.
|
|
302
|
-
*
|
|
303
|
-
* @template T - The type of values being peeked at
|
|
304
|
-
* @param {string} queueName - The name of the Redis queue
|
|
305
|
-
* @param {number} pageSize - The number of records to peek at
|
|
306
|
-
* @returns {Promise<T[]>} A promise that resolves with an array of peeked values
|
|
307
|
-
*/
|
|
308
|
-
async peekQueueRecords(queueName, pageSize) {
|
|
204
|
+
async peekQueueRecords(queueName, pageSize, compliance) {
|
|
309
205
|
const values = await this.client.lRange(queueName, 0, pageSize - 1);
|
|
310
|
-
return values.map((value) => this.parseValue(value)).filter(Boolean);
|
|
206
|
+
return values.map((value) => this.parseValue(value, compliance)).filter(Boolean);
|
|
311
207
|
}
|
|
312
|
-
/**
|
|
313
|
-
* Gracefully disconnects from the Redis server.
|
|
314
|
-
*
|
|
315
|
-
* @returns {Promise<void>} A promise that resolves when the connection is closed
|
|
316
|
-
*/
|
|
317
208
|
async disconnect() {
|
|
318
209
|
await this.client.quit();
|
|
319
210
|
}
|
|
320
|
-
/**
|
|
321
|
-
* Gets the default Time-To-Live value in milliseconds.
|
|
322
|
-
*
|
|
323
|
-
* @returns {number} The default TTL in milliseconds
|
|
324
|
-
*/
|
|
325
211
|
getTtlMilliseconds() {
|
|
326
212
|
return this.ttlMilliseconds;
|
|
327
213
|
}
|
|
328
|
-
/**
|
|
329
|
-
* Gets the underlying Redis client instance.
|
|
330
|
-
*
|
|
331
|
-
* @returns {typeof this.client} The Redis client instance
|
|
332
|
-
*/
|
|
333
214
|
getClient() {
|
|
334
215
|
return this.client;
|
|
335
216
|
}
|
package/lib/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../index.ts"],"sourcesContent":["import { safeParse, safeStringify } from '@forklaunch/common';\nimport { TtlCache, TtlCacheRecord } from '@forklaunch/core/cache';\nimport {\n evaluateTelemetryOptions,\n MetricsDefinition,\n OpenTelemetryCollector,\n TelemetryOptions\n} from '@forklaunch/core/http';\nimport {\n getCurrentTenantId,\n type FieldEncryptor\n} from '@forklaunch/core/persistence';\nimport { createClient, RedisClientOptions } from 'redis';\n\n/**\n * Type representing a raw reply from Redis commands.\n * Can be a string, number, Buffer, null, undefined, or array of raw replies.\n */\ntype RedisCommandRawReply =\n | string\n | number\n | Buffer\n | null\n | undefined\n | Array<RedisCommandRawReply>;\n\nconst ENCRYPTED_PREFIXES = ['v1:', 'v2:'] as const;\n\nfunction isEncrypted(value: string): boolean {\n return ENCRYPTED_PREFIXES.some((p) => value.startsWith(p));\n}\n\n/**\n * Options for configuring encryption on the Redis cache.\n * Required — every consumer must explicitly configure encryption.\n */\nexport interface RedisCacheEncryptionOptions {\n /** The FieldEncryptor instance to use for encrypting cache values. */\n encryptor: FieldEncryptor;\n /** Set to true to disable encryption. Defaults to false (encryption enabled). */\n disabled?: boolean;\n}\n\n/**\n * Class representing a Redis-based TTL (Time-To-Live) cache.\n * Implements the TtlCache interface to provide caching functionality with automatic expiration.\n *\n * Encryption is enabled by default when an encryptor is provided. Values are encrypted\n * before storage and decrypted on read using AES-256-GCM with per-tenant key derivation.\n */\nexport class RedisTtlCache implements TtlCache {\n private client;\n private telemetryOptions;\n private encryptor?: FieldEncryptor;\n private encryptionDisabled: boolean;\n\n /**\n * Creates an instance of RedisTtlCache.\n *\n * @param {number} ttlMilliseconds - The default Time-To-Live in milliseconds for cache entries\n * @param {OpenTelemetryCollector<MetricsDefinition>} openTelemetryCollector - Collector for OpenTelemetry metrics\n * @param {RedisClientOptions} options - Configuration options for the Redis client\n * @param {TelemetryOptions} telemetryOptions - Configuration options for telemetry\n * @param {RedisCacheEncryptionOptions} encryption - Encryption configuration (enabled by default)\n */\n constructor(\n private ttlMilliseconds: number,\n private openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>,\n options: RedisClientOptions,\n telemetryOptions: TelemetryOptions,\n encryption: RedisCacheEncryptionOptions\n ) {\n this.telemetryOptions = evaluateTelemetryOptions(telemetryOptions);\n this.client = createClient(options);\n this.encryptor = encryption.encryptor;\n this.encryptionDisabled = encryption.disabled ?? false;\n if (this.telemetryOptions.enabled.logging) {\n this.client.on('error', (err) => this.openTelemetryCollector.error(err));\n this.client.connect().catch(this.openTelemetryCollector.error);\n }\n }\n\n // ---------------------------------------------------------------------------\n // Encryption helpers\n // ---------------------------------------------------------------------------\n\n private encryptValue(serialized: string): string {\n if (!this.encryptor || this.encryptionDisabled) return serialized;\n return (\n this.encryptor.encrypt(serialized, getCurrentTenantId()) ?? serialized\n );\n }\n\n private decryptValue(value: string): string {\n if (!this.encryptor || this.encryptionDisabled) return value;\n if (!isEncrypted(value)) return value;\n try {\n return this.encryptor.decrypt(value, getCurrentTenantId()) ?? value;\n } catch {\n return value;\n }\n }\n\n /**\n * Parses a raw Redis reply into the expected type.\n * Handles null values, arrays, buffers, and JSON strings.\n *\n * @template T - The expected type of the parsed value\n * @param {RedisCommandRawReply} value - The raw value from Redis to parse\n * @returns {T} The parsed value cast to type T\n */\n private parseValue<T>(value: RedisCommandRawReply): T {\n if (value == null) {\n return null as T;\n }\n\n if (Array.isArray(value)) {\n return value.map((v) => this.parseValue<T>(v)) as T;\n }\n\n if (Buffer.isBuffer(value)) {\n return value.toJSON() as T;\n }\n\n switch (typeof value) {\n case 'object':\n case 'string':\n return safeParse(this.decryptValue(String(value))) as T;\n case 'number':\n return value as T;\n }\n }\n\n /**\n * Puts a record into the Redis cache.\n *\n * @template T - The type of value being cached\n * @param {TtlCacheRecord<T>} param0 - The cache record containing key, value and optional TTL\n * @param {string} param0.key - The key to store the value under\n * @param {T} param0.value - The value to cache\n * @param {number} [param0.ttlMilliseconds] - Optional TTL in milliseconds, defaults to constructor value\n * @returns {Promise<void>} A promise that resolves when the value is cached\n */\n async putRecord<T>({\n key,\n value,\n ttlMilliseconds = this.ttlMilliseconds\n }: TtlCacheRecord<T>): Promise<void> {\n if (this.telemetryOptions.enabled.logging) {\n this.openTelemetryCollector.info(`Putting record into cache: ${key}`);\n }\n await this.client.set(key, this.encryptValue(safeStringify(value)), {\n PX: ttlMilliseconds\n });\n }\n\n /**\n * Puts multiple records into the Redis cache in a single transaction.\n *\n * @template T - The type of values being cached\n * @param {TtlCacheRecord<T>[]} cacheRecords - Array of cache records to store\n * @returns {Promise<void>} A promise that resolves when all values are cached\n */\n async putBatchRecords<T>(cacheRecords: TtlCacheRecord<T>[]): Promise<void> {\n const multiCommand = this.client.multi();\n for (const { key, value, ttlMilliseconds } of cacheRecords) {\n multiCommand.set(key, this.encryptValue(safeStringify(value)), {\n PX: ttlMilliseconds || this.ttlMilliseconds\n });\n }\n await multiCommand.exec();\n }\n\n /**\n * Adds a value to the left end of a Redis list.\n *\n * @template T - The type of value being enqueued\n * @param {string} queueName - The name of the Redis list\n * @param {T} value - The value to add to the list\n * @returns {Promise<void>} A promise that resolves when the value is enqueued\n */\n async enqueueRecord<T>(queueName: string, value: T): Promise<void> {\n await this.client.lPush(queueName, this.encryptValue(safeStringify(value)));\n }\n\n /**\n * Adds multiple values to the left end of a Redis list in a single transaction.\n *\n * @template T - The type of values being enqueued\n * @param {string} queueName - The name of the Redis list\n * @param {T[]} values - Array of values to add to the list\n * @returns {Promise<void>} A promise that resolves when all values are enqueued\n */\n async enqueueBatchRecords<T>(queueName: string, values: T[]): Promise<void> {\n const multiCommand = this.client.multi();\n for (const value of values) {\n multiCommand.lPush(queueName, this.encryptValue(safeStringify(value)));\n }\n await multiCommand.exec();\n }\n\n /**\n * Deletes a record from the Redis cache.\n *\n * @param {string} cacheRecordKey - The key of the record to delete\n * @returns {Promise<void>} A promise that resolves when the record is deleted\n */\n async deleteRecord(cacheRecordKey: string): Promise<void> {\n await this.client.del(cacheRecordKey);\n }\n\n /**\n * Deletes multiple records from the Redis cache in a single transaction.\n *\n * @param {string[]} cacheRecordKeys - Array of keys to delete\n * @returns {Promise<void>} A promise that resolves when all records are deleted\n */\n async deleteBatchRecords(cacheRecordKeys: string[]): Promise<void> {\n const multiCommand = this.client.multi();\n for (const key of cacheRecordKeys) {\n multiCommand.del(key);\n }\n await multiCommand.exec();\n }\n\n /**\n * Removes and returns the rightmost element from a Redis list.\n *\n * @template T - The type of value being dequeued\n * @param {string} queueName - The name of the Redis list\n * @returns {Promise<T>} A promise that resolves with the dequeued value\n * @throws {Error} If the queue is empty\n */\n async dequeueRecord<T>(queueName: string): Promise<T> {\n const value = await this.client.rPop(queueName);\n if (value === null) {\n throw new Error(`Queue is empty: ${queueName}`);\n }\n return safeParse(this.decryptValue(value)) as T;\n }\n\n /**\n * Removes and returns multiple elements from the right end of a Redis list.\n *\n * @template T - The type of values being dequeued\n * @param {string} queueName - The name of the Redis list\n * @param {number} pageSize - Maximum number of elements to dequeue\n * @returns {Promise<T[]>} A promise that resolves with an array of dequeued values\n */\n async dequeueBatchRecords<T>(\n queueName: string,\n pageSize: number\n ): Promise<T[]> {\n const multiCommand = this.client.multi();\n for (let i = 0; i < pageSize; i++) {\n multiCommand.rPop(queueName);\n }\n const values = await multiCommand.exec();\n return values\n .map((value) =>\n this.parseValue<T>(value as unknown as RedisCommandRawReply)\n )\n .filter(Boolean);\n }\n\n /**\n * Reads a record from the Redis cache.\n *\n * @template T - The type of value being read\n * @param {string} cacheRecordKey - The key of the record to read\n * @returns {Promise<TtlCacheRecord<T>>} A promise that resolves with the cache record\n * @throws {Error} If the record is not found\n */\n async readRecord<T>(cacheRecordKey: string): Promise<TtlCacheRecord<T>> {\n const [value, ttl] = await this.client\n .multi()\n .get(cacheRecordKey)\n .ttl(cacheRecordKey)\n .exec();\n if (value === null) {\n throw new Error(`Record not found for key: ${cacheRecordKey}`);\n }\n\n return {\n key: cacheRecordKey,\n value: this.parseValue<T>(value as unknown as RedisCommandRawReply),\n ttlMilliseconds:\n this.parseValue<number>(ttl as unknown as RedisCommandRawReply) * 1000\n };\n }\n\n /**\n * Reads multiple records from the Redis cache.\n *\n * @template T - The type of values being read\n * @param {string[] | string} cacheRecordKeysOrPrefix - Array of keys to read, or a prefix pattern\n * @returns {Promise<TtlCacheRecord<T>[]>} A promise that resolves with an array of cache records\n */\n async readBatchRecords<T>(\n cacheRecordKeysOrPrefix: string[] | string\n ): Promise<TtlCacheRecord<T>[]> {\n const keys = Array.isArray(cacheRecordKeysOrPrefix)\n ? cacheRecordKeysOrPrefix\n : await this.client.keys(cacheRecordKeysOrPrefix + '*');\n const multiCommand = this.client.multi();\n for (const key of keys) {\n multiCommand.get(key);\n multiCommand.ttl(key);\n }\n const values = await multiCommand.exec();\n return values.reduce<TtlCacheRecord<T>[]>((acc, value, index) => {\n if (index % 2 === 0) {\n const maybeValue = this.parseValue<T>(\n value as unknown as RedisCommandRawReply\n );\n const ttl = this.parseValue<number>(\n values[index + 1] as unknown as RedisCommandRawReply\n );\n if (maybeValue && ttl) {\n acc.push({\n key: keys[index / 2],\n value: maybeValue,\n ttlMilliseconds: ttl * 1000\n });\n }\n }\n return acc;\n }, []);\n }\n\n /**\n * Lists all keys in the Redis cache that match a pattern prefix.\n *\n * @param {string} pattern_prefix - The prefix pattern to match keys against\n * @returns {Promise<string[]>} A promise that resolves with an array of matching keys\n */\n async listKeys(pattern_prefix: string): Promise<string[]> {\n const keys = await this.client.keys(pattern_prefix + '*');\n return keys;\n }\n\n /**\n * Checks if a record exists in the Redis cache.\n *\n * @param {string} cacheRecordKey - The key to check\n * @returns {Promise<boolean>} A promise that resolves with true if the record exists, false otherwise\n */\n async peekRecord(cacheRecordKey: string): Promise<boolean> {\n const result = await this.client.exists(cacheRecordKey);\n return result === 1;\n }\n\n /**\n * Checks if multiple records exist in the Redis cache.\n *\n * @param {string[] | string} cacheRecordKeysOrPrefix - Array of keys to check, or a prefix pattern\n * @returns {Promise<boolean[]>} A promise that resolves with an array of existence booleans\n */\n async peekBatchRecords(\n cacheRecordKeysOrPrefix: string[] | string\n ): Promise<boolean[]> {\n const keys = Array.isArray(cacheRecordKeysOrPrefix)\n ? cacheRecordKeysOrPrefix\n : await this.client.keys(cacheRecordKeysOrPrefix + '*');\n const multiCommand = this.client.multi();\n for (const key of keys) {\n multiCommand.exists(key);\n }\n const results = await multiCommand.exec();\n return results.map((result) => (result as unknown as number) === 1);\n }\n\n /**\n * Peeks at a record in the Redis cache.\n *\n * @template T - The type of value being peeked at\n * @param {string} queueName - The name of the Redis queue\n * @returns {Promise<T>} A promise that resolves with the peeked value\n */\n async peekQueueRecord<T>(queueName: string): Promise<T> {\n const value = await this.client.lRange(queueName, 0, 0);\n return this.parseValue<T>(value[0]);\n }\n\n /**\n * Peeks at multiple records in the Redis cache.\n *\n * @template T - The type of values being peeked at\n * @param {string} queueName - The name of the Redis queue\n * @param {number} pageSize - The number of records to peek at\n * @returns {Promise<T[]>} A promise that resolves with an array of peeked values\n */\n async peekQueueRecords<T>(queueName: string, pageSize: number): Promise<T[]> {\n const values = await this.client.lRange(queueName, 0, pageSize - 1);\n return values.map((value) => this.parseValue<T>(value)).filter(Boolean);\n }\n\n /**\n * Gracefully disconnects from the Redis server.\n *\n * @returns {Promise<void>} A promise that resolves when the connection is closed\n */\n async disconnect(): Promise<void> {\n await this.client.quit();\n }\n\n /**\n * Gets the default Time-To-Live value in milliseconds.\n *\n * @returns {number} The default TTL in milliseconds\n */\n getTtlMilliseconds(): number {\n return this.ttlMilliseconds;\n }\n\n /**\n * Gets the underlying Redis client instance.\n *\n * @returns {typeof this.client} The Redis client instance\n */\n getClient(): typeof this.client {\n return this.client;\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../index.ts"],"sourcesContent":["import { safeParse, safeStringify } from '@forklaunch/common';\nimport {\n type ComplianceContext,\n TtlCache,\n TtlCacheRecord\n} from '@forklaunch/core/cache';\nimport {\n evaluateTelemetryOptions,\n MetricsDefinition,\n OpenTelemetryCollector,\n TelemetryOptions\n} from '@forklaunch/core/http';\nimport { type FieldEncryptor } from '@forklaunch/core/persistence';\nimport { createClient, RedisClientOptions } from 'redis';\n\n/**\n * Type representing a raw reply from Redis commands.\n * Can be a string, number, Buffer, null, undefined, or array of raw replies.\n */\ntype RedisCommandRawReply =\n | string\n | number\n | Buffer\n | null\n | undefined\n | Array<RedisCommandRawReply>;\n\nconst ENCRYPTED_PREFIXES = ['v1:', 'v2:'] as const;\n\nfunction isEncrypted(value: string): boolean {\n return ENCRYPTED_PREFIXES.some((p) => value.startsWith(p));\n}\n\n/**\n * Options for configuring encryption on the Redis cache.\n * Required — every consumer must explicitly configure encryption.\n */\nexport interface RedisCacheEncryptionOptions {\n /** The FieldEncryptor instance to use for encrypting cache values. */\n encryptor: FieldEncryptor;\n}\n\n/**\n * Class representing a Redis-based TTL (Time-To-Live) cache.\n * Implements the TtlCache interface to provide caching functionality with automatic expiration.\n *\n * Encryption is activated per-operation when a `compliance` context is provided.\n * Without it, values are stored and read as plaintext.\n */\nexport class RedisTtlCache implements TtlCache {\n private client;\n private telemetryOptions;\n private encryptor?: FieldEncryptor;\n\n /**\n * Creates an instance of RedisTtlCache.\n *\n * @param {number} ttlMilliseconds - The default Time-To-Live in milliseconds for cache entries\n * @param {OpenTelemetryCollector<MetricsDefinition>} openTelemetryCollector - Collector for OpenTelemetry metrics\n * @param {RedisClientOptions} options - Configuration options for the Redis client\n * @param {TelemetryOptions} telemetryOptions - Configuration options for telemetry\n * @param {RedisCacheEncryptionOptions} encryption - Encryption configuration\n */\n constructor(\n private ttlMilliseconds: number,\n private openTelemetryCollector: OpenTelemetryCollector<MetricsDefinition>,\n options: RedisClientOptions,\n telemetryOptions: TelemetryOptions,\n encryption: RedisCacheEncryptionOptions\n ) {\n this.telemetryOptions = evaluateTelemetryOptions(telemetryOptions);\n this.client = createClient(options);\n this.encryptor = encryption.encryptor;\n if (this.telemetryOptions.enabled.logging) {\n this.client.on('error', (err) => this.openTelemetryCollector.error(err));\n this.client.connect().catch(this.openTelemetryCollector.error);\n }\n }\n\n // ---------------------------------------------------------------------------\n // Encryption helpers — only active when compliance context is provided\n // ---------------------------------------------------------------------------\n\n private encryptValue(\n serialized: string,\n compliance?: ComplianceContext\n ): string {\n if (!compliance || !this.encryptor) return serialized;\n return (\n this.encryptor.encrypt(serialized, compliance.tenantId) ?? serialized\n );\n }\n\n private decryptValue(value: string, compliance?: ComplianceContext): string {\n if (!compliance || !this.encryptor) return value;\n if (!isEncrypted(value)) return value;\n try {\n return this.encryptor.decrypt(value, compliance.tenantId) ?? value;\n } catch {\n return value;\n }\n }\n\n private parseValue<T>(\n value: RedisCommandRawReply,\n compliance?: ComplianceContext\n ): T {\n if (value == null) {\n return null as T;\n }\n\n if (Array.isArray(value)) {\n return value.map((v) => this.parseValue<T>(v, compliance)) as T;\n }\n\n if (Buffer.isBuffer(value)) {\n return value.toJSON() as T;\n }\n\n switch (typeof value) {\n case 'object':\n case 'string':\n return safeParse(this.decryptValue(String(value), compliance)) as T;\n case 'number':\n return value as T;\n }\n }\n\n // ---------------------------------------------------------------------------\n // TtlCache implementation\n // ---------------------------------------------------------------------------\n\n async putRecord<T>(\n { key, value, ttlMilliseconds = this.ttlMilliseconds }: TtlCacheRecord<T>,\n compliance?: ComplianceContext\n ): Promise<void> {\n if (this.telemetryOptions.enabled.logging) {\n this.openTelemetryCollector.info(`Putting record into cache: ${key}`);\n }\n await this.client.set(\n key,\n this.encryptValue(safeStringify(value), compliance),\n { PX: ttlMilliseconds }\n );\n }\n\n async putBatchRecords<T>(\n cacheRecords: TtlCacheRecord<T>[],\n compliance?: ComplianceContext\n ): Promise<void> {\n const multiCommand = this.client.multi();\n for (const { key, value, ttlMilliseconds } of cacheRecords) {\n multiCommand.set(\n key,\n this.encryptValue(safeStringify(value), compliance),\n { PX: ttlMilliseconds || this.ttlMilliseconds }\n );\n }\n await multiCommand.exec();\n }\n\n async enqueueRecord<T>(\n queueName: string,\n value: T,\n compliance?: ComplianceContext\n ): Promise<void> {\n await this.client.lPush(\n queueName,\n this.encryptValue(safeStringify(value), compliance)\n );\n }\n\n async enqueueBatchRecords<T>(\n queueName: string,\n values: T[],\n compliance?: ComplianceContext\n ): Promise<void> {\n const multiCommand = this.client.multi();\n for (const value of values) {\n multiCommand.lPush(\n queueName,\n this.encryptValue(safeStringify(value), compliance)\n );\n }\n await multiCommand.exec();\n }\n\n async deleteRecord(cacheRecordKey: string): Promise<void> {\n await this.client.del(cacheRecordKey);\n }\n\n async deleteBatchRecords(cacheRecordKeys: string[]): Promise<void> {\n const multiCommand = this.client.multi();\n for (const key of cacheRecordKeys) {\n multiCommand.del(key);\n }\n await multiCommand.exec();\n }\n\n async dequeueRecord<T>(\n queueName: string,\n compliance?: ComplianceContext\n ): Promise<T> {\n const value = await this.client.rPop(queueName);\n if (value === null) {\n throw new Error(`Queue is empty: ${queueName}`);\n }\n return safeParse(this.decryptValue(value, compliance)) as T;\n }\n\n async dequeueBatchRecords<T>(\n queueName: string,\n pageSize: number,\n compliance?: ComplianceContext\n ): Promise<T[]> {\n const multiCommand = this.client.multi();\n for (let i = 0; i < pageSize; i++) {\n multiCommand.rPop(queueName);\n }\n const values = await multiCommand.exec();\n return values\n .map((value) =>\n this.parseValue<T>(value as unknown as RedisCommandRawReply, compliance)\n )\n .filter(Boolean);\n }\n\n async readRecord<T>(\n cacheRecordKey: string,\n compliance?: ComplianceContext\n ): Promise<TtlCacheRecord<T>> {\n const [value, ttl] = await this.client\n .multi()\n .get(cacheRecordKey)\n .ttl(cacheRecordKey)\n .exec();\n if (value === null) {\n throw new Error(`Record not found for key: ${cacheRecordKey}`);\n }\n\n return {\n key: cacheRecordKey,\n value: this.parseValue<T>(\n value as unknown as RedisCommandRawReply,\n compliance\n ),\n ttlMilliseconds:\n this.parseValue<number>(\n ttl as unknown as RedisCommandRawReply,\n compliance\n ) * 1000\n };\n }\n\n async readBatchRecords<T>(\n cacheRecordKeysOrPrefix: string[] | string,\n compliance?: ComplianceContext\n ): Promise<TtlCacheRecord<T>[]> {\n const keys = Array.isArray(cacheRecordKeysOrPrefix)\n ? cacheRecordKeysOrPrefix\n : await this.client.keys(cacheRecordKeysOrPrefix + '*');\n const multiCommand = this.client.multi();\n for (const key of keys) {\n multiCommand.get(key);\n multiCommand.ttl(key);\n }\n const values = await multiCommand.exec();\n return values.reduce<TtlCacheRecord<T>[]>((acc, value, index) => {\n if (index % 2 === 0) {\n const maybeValue = this.parseValue<T>(\n value as unknown as RedisCommandRawReply,\n compliance\n );\n const ttl = this.parseValue<number>(\n values[index + 1] as unknown as RedisCommandRawReply,\n compliance\n );\n if (maybeValue && ttl) {\n acc.push({\n key: keys[index / 2],\n value: maybeValue,\n ttlMilliseconds: ttl * 1000\n });\n }\n }\n return acc;\n }, []);\n }\n\n async listKeys(pattern_prefix: string): Promise<string[]> {\n return this.client.keys(pattern_prefix + '*');\n }\n\n async peekRecord(cacheRecordKey: string): Promise<boolean> {\n const result = await this.client.exists(cacheRecordKey);\n return result === 1;\n }\n\n async peekBatchRecords(\n cacheRecordKeysOrPrefix: string[] | string\n ): Promise<boolean[]> {\n const keys = Array.isArray(cacheRecordKeysOrPrefix)\n ? cacheRecordKeysOrPrefix\n : await this.client.keys(cacheRecordKeysOrPrefix + '*');\n const multiCommand = this.client.multi();\n for (const key of keys) {\n multiCommand.exists(key);\n }\n const results = await multiCommand.exec();\n return results.map((result) => (result as unknown as number) === 1);\n }\n\n async peekQueueRecord<T>(\n queueName: string,\n compliance?: ComplianceContext\n ): Promise<T> {\n const value = await this.client.lRange(queueName, 0, 0);\n return this.parseValue<T>(value[0], compliance);\n }\n\n async peekQueueRecords<T>(\n queueName: string,\n pageSize: number,\n compliance?: ComplianceContext\n ): Promise<T[]> {\n const values = await this.client.lRange(queueName, 0, pageSize - 1);\n return values\n .map((value) => this.parseValue<T>(value, compliance))\n .filter(Boolean);\n }\n\n async disconnect(): Promise<void> {\n await this.client.quit();\n }\n\n getTtlMilliseconds(): number {\n return this.ttlMilliseconds;\n }\n\n getClient(): typeof this.client {\n return this.client;\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forklaunch/infrastructure-redis",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Redis infrastructure for ForkLaunch components.",
|
|
5
5
|
"homepage": "https://github.com/forklaunch/forklaunch-js#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"redis": "^5.11.0",
|
|
32
|
-
"@forklaunch/common": "1.2.
|
|
33
|
-
"@forklaunch/core": "1.
|
|
32
|
+
"@forklaunch/common": "1.2.14",
|
|
33
|
+
"@forklaunch/core": "1.4.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@eslint/js": "^10.0.1",
|