@forklaunch/infrastructure-redis 1.3.15 → 1.4.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/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 (enabled by default)
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 (!this.encryptor || this.encryptionDisabled) return serialized;
45
- return this.encryptor.encrypt(serialized, getCurrentTenantId()) ?? 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 (!this.encryptor || this.encryptionDisabled) return value;
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, getCurrentTenantId()) ?? 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
- * Puts a record into the Redis cache.
84
- *
85
- * @template T - The type of value being cached
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(key, this.encryptValue(safeStringify(value)), {
101
- PX: ttlMilliseconds
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(key, this.encryptValue(safeStringify(value)), {
115
- PX: ttlMilliseconds || this.ttlMilliseconds
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
- * Adds a value to the left end of a Redis list.
122
- *
123
- * @template T - The type of value being enqueued
124
- * @param {string} queueName - The name of the Redis list
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(queueName, this.encryptValue(safeStringify(value)));
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(value),
217
- ttlMilliseconds: this.parseValue(ttl) * 1e3
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
- const keys = await this.client.keys(pattern_prefix + "*");
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.15",
3
+ "version": "1.4.1",
4
4
  "description": "Redis infrastructure for ForkLaunch components.",
5
5
  "homepage": "https://github.com/forklaunch/forklaunch-js#readme",
6
6
  "bugs": {
@@ -29,18 +29,18 @@
29
29
  ],
30
30
  "dependencies": {
31
31
  "redis": "^5.11.0",
32
- "@forklaunch/common": "1.2.14",
33
- "@forklaunch/core": "1.3.17"
32
+ "@forklaunch/common": "1.2.15",
33
+ "@forklaunch/core": "1.4.1"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@eslint/js": "^10.0.1",
37
37
  "@types/jest": "^30.0.0",
38
- "@typescript/native-preview": "7.0.0-dev.20260331.1",
38
+ "@typescript/native-preview": "7.0.0-dev.20260407.1",
39
39
  "globals": "^17.4.0",
40
40
  "jest": "^30.3.0",
41
41
  "prettier": "^3.8.1",
42
42
  "testcontainers": "^11.13.0",
43
- "ts-jest": "^29.4.6",
43
+ "ts-jest": "^29.4.9",
44
44
  "ts-node": "^10.9.2",
45
45
  "tsup": "^8.5.1",
46
46
  "typedoc": "^0.28.18",