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