@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.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
|
|
21
|
-
*
|
|
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
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
65
|
-
return this.encryptor.encrypt(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 (!
|
|
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,
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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(
|
|
121
|
-
|
|
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(
|
|
135
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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(
|
|
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(
|
|
237
|
-
|
|
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
|
-
|
|
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":[]}
|