|
10 | 10 | import { IQueueParams } from '../../lib/index.js'; |
11 | 11 | import { RedisKeysInvalidKeyError } from './errors/index.js'; |
12 | 12 |
|
13 | | -// Key segments separator |
14 | | -const keySegmentSeparator = ':'; |
| 13 | +/** |
| 14 | + * Redis key configuration constants |
| 15 | + */ |
| 16 | +const REDIS_KEY_CONFIG = { |
| 17 | + /** Key segments separator */ |
| 18 | + SEGMENT_SEPARATOR: ':', |
15 | 19 |
|
16 | | -// Keys version |
17 | | -const keyVersion = `800.26`; |
| 20 | + /** Keys version */ |
| 21 | + VERSION: '800.26', |
18 | 22 |
|
19 | | -// Keys prefix |
20 | | -const keyPrefix = `redis-smq-${keyVersion}`; |
| 23 | + /** Global namespace identifier */ |
| 24 | + GLOBAL_NAMESPACE: 'global', |
| 25 | +} as const; |
21 | 26 |
|
22 | | -// Namespaces |
23 | | -const globalNamespace = 'global'; |
| 27 | +/** |
| 28 | + * Redis key prefix with version |
| 29 | + */ |
| 30 | +const KEY_PREFIX = `redis-smq-${REDIS_KEY_CONFIG.VERSION}`; |
24 | 31 |
|
| 32 | +/** |
| 33 | + * Enum for Redis key types |
| 34 | + */ |
25 | 35 | enum ERedisKey { |
26 | | - KEY_QUEUE_PENDING = 1, |
27 | | - KEY_QUEUE_PRIORITY_PENDING, |
28 | | - KEY_QUEUE_DL, |
29 | | - KEY_QUEUE_PROCESSING, |
30 | | - KEY_QUEUE_ACKNOWLEDGED, |
31 | | - KEY_QUEUE_SCHEDULED, |
32 | | - KEY_QUEUE_DELAYED, |
33 | | - KEY_QUEUE_REQUEUED, |
34 | | - KEY_QUEUE_CONSUMERS, |
35 | | - KEY_QUEUE_PROCESSING_QUEUES, |
36 | | - KEY_QUEUE_WORKERS_LOCK, |
37 | | - KEY_QUEUE_RATE_LIMIT_COUNTER, |
38 | | - KEY_QUEUE_PROPERTIES, |
39 | | - KEY_QUEUE_MESSAGES, |
40 | | - KEY_QUEUE_MESSAGE_IDS, |
41 | | - KEY_QUEUE_CONSUMER_GROUPS, |
42 | | - KEY_QUEUES, |
43 | | - KEY_CONSUMER_QUEUES, |
44 | | - KEY_CONSUMER_HEARTBEAT, |
45 | | - KEY_NS_QUEUES, |
46 | | - KEY_NAMESPACES, |
47 | | - KEY_EXCHANGE_BINDINGS, |
48 | | - KEY_FANOUT_EXCHANGES, |
49 | | - KEY_MESSAGE, |
| 36 | + // Queue related keys |
| 37 | + QUEUE_PENDING = 1, |
| 38 | + QUEUE_PRIORITY_PENDING, |
| 39 | + QUEUE_DL, |
| 40 | + QUEUE_PROCESSING, |
| 41 | + QUEUE_ACKNOWLEDGED, |
| 42 | + QUEUE_SCHEDULED, |
| 43 | + QUEUE_DELAYED, |
| 44 | + QUEUE_REQUEUED, |
| 45 | + QUEUE_CONSUMERS, |
| 46 | + QUEUE_PROCESSING_QUEUES, |
| 47 | + QUEUE_WORKERS_LOCK, |
| 48 | + QUEUE_RATE_LIMIT_COUNTER, |
| 49 | + QUEUE_PROPERTIES, |
| 50 | + QUEUE_MESSAGES, |
| 51 | + QUEUE_MESSAGE_IDS, |
| 52 | + QUEUE_CONSUMER_GROUPS, |
| 53 | + |
| 54 | + // Global keys |
| 55 | + QUEUES, |
| 56 | + CONSUMER_QUEUES, |
| 57 | + CONSUMER_HEARTBEAT, |
| 58 | + NS_QUEUES, |
| 59 | + NAMESPACES, |
| 60 | + EXCHANGE_BINDINGS, |
| 61 | + FANOUT_EXCHANGES, |
| 62 | + MESSAGE, |
50 | 63 | } |
51 | 64 |
|
52 | | -function makeNamespacedKeys<T extends Record<string, ERedisKey>>( |
| 65 | +/** |
| 66 | + * Type for key mapping objects |
| 67 | + */ |
| 68 | +type TRedisKeyMap = Record<string, ERedisKey>; |
| 69 | + |
| 70 | +/** |
| 71 | + * Creates namespaced Redis keys from a key mapping |
| 72 | + * |
| 73 | + * @param keys - Object mapping key names to ERedisKey values |
| 74 | + * @param namespace - Namespace for the keys |
| 75 | + * @param rest - Additional key segments |
| 76 | + * @returns Record with the same keys but values as formatted Redis key strings |
| 77 | + */ |
| 78 | +function makeNamespacedKeys<T extends TRedisKeyMap>( |
53 | 79 | keys: T, |
54 | 80 | namespace: string, |
55 | 81 | ...rest: (string | number)[] |
56 | 82 | ): Record<Extract<keyof T, string>, string> { |
57 | 83 | const result: Record<string, string> = {}; |
58 | | - for (const k in keys) { |
59 | | - result[k] = [keyPrefix, namespace, keys[k], ...rest].join( |
60 | | - keySegmentSeparator, |
| 84 | + |
| 85 | + for (const keyName in keys) { |
| 86 | + result[keyName] = [KEY_PREFIX, namespace, keys[keyName], ...rest].join( |
| 87 | + REDIS_KEY_CONFIG.SEGMENT_SEPARATOR, |
61 | 88 | ); |
62 | 89 | } |
| 90 | + |
63 | 91 | return result; |
64 | 92 | } |
65 | 93 |
|
| 94 | +/** |
| 95 | + * Redis keys utility functions |
| 96 | + */ |
66 | 97 | export const redisKeys = { |
| 98 | + /** |
| 99 | + * Get keys for a specific namespace |
| 100 | + * |
| 101 | + * @param ns - Namespace |
| 102 | + * @returns Namespace-specific keys |
| 103 | + */ |
67 | 104 | getNamespaceKeys(ns: string) { |
68 | 105 | const keys = { |
69 | | - keyNamespaceQueues: ERedisKey.KEY_NS_QUEUES, |
| 106 | + keyNamespaceQueues: ERedisKey.NS_QUEUES, |
70 | 107 | }; |
71 | 108 | return { |
72 | 109 | ...makeNamespacedKeys(keys, ns), |
73 | 110 | }; |
74 | 111 | }, |
75 | 112 |
|
| 113 | + /** |
| 114 | + * Get keys for a specific queue |
| 115 | + * |
| 116 | + * @param queueParams - Queue parameters |
| 117 | + * @param consumerGroupId - Optional consumer group ID |
| 118 | + * @returns Queue-specific keys |
| 119 | + */ |
76 | 120 | getQueueKeys(queueParams: IQueueParams, consumerGroupId: string | null) { |
77 | 121 | const queueKeys = { |
78 | | - keyQueueDL: ERedisKey.KEY_QUEUE_DL, |
79 | | - keyQueueProcessingQueues: ERedisKey.KEY_QUEUE_PROCESSING_QUEUES, |
80 | | - keyQueueAcknowledged: ERedisKey.KEY_QUEUE_ACKNOWLEDGED, |
81 | | - keyQueueScheduled: ERedisKey.KEY_QUEUE_SCHEDULED, |
82 | | - keyQueueRequeued: ERedisKey.KEY_QUEUE_REQUEUED, |
83 | | - keyQueueDelayed: ERedisKey.KEY_QUEUE_DELAYED, |
84 | | - keyQueueConsumers: ERedisKey.KEY_QUEUE_CONSUMERS, |
85 | | - keyQueueRateLimitCounter: ERedisKey.KEY_QUEUE_RATE_LIMIT_COUNTER, |
86 | | - keyQueueProperties: ERedisKey.KEY_QUEUE_PROPERTIES, |
87 | | - keyQueueMessages: ERedisKey.KEY_QUEUE_MESSAGES, |
88 | | - keyQueueMessageIds: ERedisKey.KEY_QUEUE_MESSAGE_IDS, |
89 | | - keyQueueConsumerGroups: ERedisKey.KEY_QUEUE_CONSUMER_GROUPS, |
90 | | - keyQueueWorkersLock: ERedisKey.KEY_QUEUE_WORKERS_LOCK, |
| 122 | + keyQueueDL: ERedisKey.QUEUE_DL, |
| 123 | + keyQueueProcessingQueues: ERedisKey.QUEUE_PROCESSING_QUEUES, |
| 124 | + keyQueueAcknowledged: ERedisKey.QUEUE_ACKNOWLEDGED, |
| 125 | + keyQueueScheduled: ERedisKey.QUEUE_SCHEDULED, |
| 126 | + keyQueueRequeued: ERedisKey.QUEUE_REQUEUED, |
| 127 | + keyQueueDelayed: ERedisKey.QUEUE_DELAYED, |
| 128 | + keyQueueConsumers: ERedisKey.QUEUE_CONSUMERS, |
| 129 | + keyQueueRateLimitCounter: ERedisKey.QUEUE_RATE_LIMIT_COUNTER, |
| 130 | + keyQueueProperties: ERedisKey.QUEUE_PROPERTIES, |
| 131 | + keyQueueMessages: ERedisKey.QUEUE_MESSAGES, |
| 132 | + keyQueueMessageIds: ERedisKey.QUEUE_MESSAGE_IDS, |
| 133 | + keyQueueConsumerGroups: ERedisKey.QUEUE_CONSUMER_GROUPS, |
| 134 | + keyQueueWorkersLock: ERedisKey.QUEUE_WORKERS_LOCK, |
91 | 135 | }; |
| 136 | + |
92 | 137 | const pendingKeys = { |
93 | | - keyQueuePending: ERedisKey.KEY_QUEUE_PENDING, |
94 | | - keyQueuePriorityPending: ERedisKey.KEY_QUEUE_PRIORITY_PENDING, |
| 138 | + keyQueuePending: ERedisKey.QUEUE_PENDING, |
| 139 | + keyQueuePriorityPending: ERedisKey.QUEUE_PRIORITY_PENDING, |
95 | 140 | }; |
| 141 | + |
96 | 142 | const payload = [queueParams.name]; |
| 143 | + const pendingPayload = [ |
| 144 | + ...payload, |
| 145 | + ...(consumerGroupId ? [consumerGroupId] : []), |
| 146 | + ]; |
| 147 | + |
97 | 148 | return { |
98 | 149 | ...makeNamespacedKeys(queueKeys, queueParams.ns, ...payload), |
99 | | - ...makeNamespacedKeys( |
100 | | - pendingKeys, |
101 | | - queueParams.ns, |
102 | | - ...payload, |
103 | | - ...(consumerGroupId ? [consumerGroupId] : []), |
104 | | - ), |
| 150 | + ...makeNamespacedKeys(pendingKeys, queueParams.ns, ...pendingPayload), |
105 | 151 | }; |
106 | 152 | }, |
107 | 153 |
|
| 154 | + /** |
| 155 | + * Get keys for a specific message |
| 156 | + * |
| 157 | + * @param messageId - Message ID |
| 158 | + * @returns Message-specific keys |
| 159 | + */ |
108 | 160 | getMessageKeys(messageId: string) { |
109 | | - const exchangeKeys = { |
110 | | - keyMessage: ERedisKey.KEY_MESSAGE, |
| 161 | + const messageKeys = { |
| 162 | + keyMessage: ERedisKey.MESSAGE, |
111 | 163 | }; |
112 | 164 | return { |
113 | | - ...makeNamespacedKeys(exchangeKeys, globalNamespace, messageId), |
| 165 | + ...makeNamespacedKeys( |
| 166 | + messageKeys, |
| 167 | + REDIS_KEY_CONFIG.GLOBAL_NAMESPACE, |
| 168 | + messageId, |
| 169 | + ), |
114 | 170 | }; |
115 | 171 | }, |
116 | 172 |
|
| 173 | + /** |
| 174 | + * Get keys for a fanout exchange |
| 175 | + * |
| 176 | + * @param bindingKey - Exchange binding key |
| 177 | + * @returns Exchange-specific keys |
| 178 | + */ |
117 | 179 | getFanOutExchangeKeys(bindingKey: string) { |
118 | 180 | const exchangeKeys = { |
119 | | - keyExchangeBindings: ERedisKey.KEY_EXCHANGE_BINDINGS, |
| 181 | + keyExchangeBindings: ERedisKey.EXCHANGE_BINDINGS, |
120 | 182 | }; |
121 | 183 | return { |
122 | | - ...makeNamespacedKeys(exchangeKeys, globalNamespace, bindingKey), |
| 184 | + ...makeNamespacedKeys( |
| 185 | + exchangeKeys, |
| 186 | + REDIS_KEY_CONFIG.GLOBAL_NAMESPACE, |
| 187 | + bindingKey, |
| 188 | + ), |
123 | 189 | }; |
124 | 190 | }, |
125 | 191 |
|
| 192 | + /** |
| 193 | + * Get keys for a consumer instance |
| 194 | + * |
| 195 | + * @param instanceId - Consumer instance ID |
| 196 | + * @returns Consumer-specific keys |
| 197 | + */ |
126 | 198 | getConsumerKeys(instanceId: string) { |
127 | 199 | const consumerKeys = { |
128 | | - keyConsumerQueues: ERedisKey.KEY_CONSUMER_QUEUES, |
129 | | - keyConsumerHeartbeat: ERedisKey.KEY_CONSUMER_HEARTBEAT, |
| 200 | + keyConsumerQueues: ERedisKey.CONSUMER_QUEUES, |
| 201 | + keyConsumerHeartbeat: ERedisKey.CONSUMER_HEARTBEAT, |
130 | 202 | }; |
131 | 203 | return { |
132 | | - ...makeNamespacedKeys(consumerKeys, globalNamespace, instanceId), |
| 204 | + ...makeNamespacedKeys( |
| 205 | + consumerKeys, |
| 206 | + REDIS_KEY_CONFIG.GLOBAL_NAMESPACE, |
| 207 | + instanceId, |
| 208 | + ), |
133 | 209 | }; |
134 | 210 | }, |
135 | 211 |
|
| 212 | + /** |
| 213 | + * Get keys for a queue consumer |
| 214 | + * |
| 215 | + * @param queueParams - Queue parameters |
| 216 | + * @param instanceId - Consumer instance ID |
| 217 | + * @returns Queue consumer-specific keys |
| 218 | + */ |
136 | 219 | getQueueConsumerKeys(queueParams: IQueueParams, instanceId: string) { |
137 | 220 | const keys = { |
138 | | - keyQueueProcessing: ERedisKey.KEY_QUEUE_PROCESSING, |
| 221 | + keyQueueProcessing: ERedisKey.QUEUE_PROCESSING, |
139 | 222 | }; |
140 | 223 | return { |
141 | 224 | ...makeNamespacedKeys(keys, queueParams.ns, queueParams.name, instanceId), |
142 | 225 | }; |
143 | 226 | }, |
144 | 227 |
|
| 228 | + /** |
| 229 | + * Get main global keys |
| 230 | + * |
| 231 | + * @returns Global keys |
| 232 | + */ |
145 | 233 | getMainKeys() { |
146 | 234 | const mainKeys = { |
147 | | - keyQueues: ERedisKey.KEY_QUEUES, |
148 | | - keyNamespaces: ERedisKey.KEY_NAMESPACES, |
149 | | - keyFanOutExchanges: ERedisKey.KEY_FANOUT_EXCHANGES, |
| 235 | + keyQueues: ERedisKey.QUEUES, |
| 236 | + keyNamespaces: ERedisKey.NAMESPACES, |
| 237 | + keyFanOutExchanges: ERedisKey.FANOUT_EXCHANGES, |
150 | 238 | }; |
151 | | - return makeNamespacedKeys(mainKeys, globalNamespace); |
| 239 | + return makeNamespacedKeys(mainKeys, REDIS_KEY_CONFIG.GLOBAL_NAMESPACE); |
152 | 240 | }, |
153 | 241 |
|
| 242 | + /** |
| 243 | + * Validate a namespace string |
| 244 | + * |
| 245 | + * @param ns - Namespace to validate |
| 246 | + * @returns Validated namespace or error |
| 247 | + */ |
154 | 248 | validateNamespace(ns: string): string | RedisKeysInvalidKeyError { |
155 | 249 | const validated = this.validateRedisKey(ns); |
156 | | - if (validated === globalNamespace) { |
| 250 | + |
| 251 | + if (validated instanceof RedisKeysInvalidKeyError) { |
| 252 | + return validated; |
| 253 | + } |
| 254 | + |
| 255 | + if (validated === REDIS_KEY_CONFIG.GLOBAL_NAMESPACE) { |
157 | 256 | return new RedisKeysInvalidKeyError(); |
158 | 257 | } |
| 258 | + |
159 | 259 | return validated; |
160 | 260 | }, |
161 | 261 |
|
| 262 | + /** |
| 263 | + * Validate a Redis key string |
| 264 | + * |
| 265 | + * @param key - Key to validate |
| 266 | + * @returns Validated key or error |
| 267 | + */ |
162 | 268 | validateRedisKey( |
163 | 269 | key: string | null | undefined, |
164 | 270 | ): string | RedisKeysInvalidKeyError { |
165 | 271 | if (!key || !key.length) { |
166 | 272 | return new RedisKeysInvalidKeyError(); |
167 | 273 | } |
| 274 | + |
168 | 275 | const lowerCase = key.toLowerCase(); |
| 276 | + // Regex matches valid key patterns, then we check if anything remains |
169 | 277 | const filtered = lowerCase.replace( |
170 | | - /(?:[a-z][a-z0-9]?)+(?:[-_.]?[a-z0-9])*/, |
| 278 | + /(?:[a-z][a-z0-9]?)+(?:[-_.]?[a-z0-9])*/g, |
171 | 279 | '', |
172 | 280 | ); |
| 281 | + |
173 | 282 | if (filtered.length) { |
174 | 283 | return new RedisKeysInvalidKeyError(); |
175 | 284 | } |
| 285 | + |
176 | 286 | return lowerCase; |
177 | 287 | }, |
178 | 288 |
|
| 289 | + /** |
| 290 | + * Get the key segment separator |
| 291 | + * |
| 292 | + * @returns Key segment separator |
| 293 | + */ |
179 | 294 | getKeySegmentSeparator() { |
180 | | - return keySegmentSeparator; |
| 295 | + return REDIS_KEY_CONFIG.SEGMENT_SEPARATOR; |
181 | 296 | }, |
182 | 297 | }; |
0 commit comments