1- import type { JSONObject } from '@aws-lambda-powertools/commons/types' ;
21import {
32 IdempotencyRecordStatus ,
43 PERSISTENCE_ATTRIBUTE_KEY_MAPPINGS ,
@@ -9,54 +8,81 @@ import {
98 IdempotencyPersistenceConsistencyError ,
109 IdempotencyUnknownError ,
1110} from '../errors.js' ;
12- import type { IdempotencyRecordStatusValue } from '../types/IdempotencyRecord.js' ;
1311import type {
14- RedisCompatibleClient ,
15- RedisPersistenceOptions ,
16- } from '../types/RedisPersistence.js' ;
12+ CacheClient ,
13+ CachePersistenceOptions ,
14+ } from '../types/CachePersistence.js' ;
15+ import type { IdempotencyRecordStatusValue } from '../types/IdempotencyRecord.js' ;
1716import { BasePersistenceLayer } from './BasePersistenceLayer.js' ;
1817import { IdempotencyRecord } from './IdempotencyRecord.js' ;
1918
2019/**
21- * Redis persistence layer for idempotency records.
20+ * Valey- and Redis OOS-compatible persistence layer for idempotency records.
2221 *
23- * This class uses Redis to write and read idempotency records. It supports any Redis client that
24- * implements the RedisCompatibleClient interface.
22+ * This class uses a cache client to write and read idempotency records. It supports any client that
23+ * implements the { @link CacheClient | `CacheClient`} interface.
2524 *
2625 * There are various options to configure the persistence layer, such as attribute names for storing
27- * status, expiry, data, and validation keys in Redis .
26+ * status, expiry, data, and validation keys in the cache .
2827 *
29- * You must provide your own connected Redis client instance by passing it through the `client` option.
28+ * You must provide your own connected client instance by passing it through the `client` option.
3029 *
3130 * See the {@link https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/idempotency/ Idempotency documentation}
32- * for more details on the Redis configuration and usage patterns.
31+ * for more details on the configuration and usage patterns.
32+ *
33+ * **Using Valkey Glide Client**
34+ *
35+ * @example
36+ * ```ts
37+ * import { GlideClient } from '@valkey/valkey-glide';
38+ * import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache';
39+ *
40+ * const client = await GlideClient.createClient({
41+ * addresses: [{
42+ * host: process.env.CACHE_ENDPOINT,
43+ * port: Number(process.env.CACHE_PORT),
44+ * }],
45+ * useTLS: true,
46+ * });
47+ *
48+ * const persistence = new CachePersistenceLayer({
49+ * client,
50+ * });
51+ *
52+ * // ... your function handler here
53+ * ```
54+ *
55+ * **Using Redis Client**
3356 *
3457 * @example
3558 * ```ts
3659 * import { createClient } from '@redis/client';
37- * import { RedisPersistenceLayer } from '@aws-lambda-powertools/idempotency/redis';
38- * import { RedisCompatibleClient } from '@aws-lambda-powertools/idempotency/redis/types';
60+ * import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache';
3961 *
40- * const redisClient = createClient({ url: 'redis://localhost:6379' });
41- * await redisClient.connect();
62+ * const client = await createClient({
63+ * url: `rediss://${process.env.CACHE_ENDPOINT}:${process.env.CACHE_PORT}`,
64+ * username: 'default',
65+ * }).connect();
4266 *
43- * const persistence = new RedisPersistenceLayer ({
44- * client: redisClient as RedisCompatibleClient ,
67+ * const persistence = new CachePersistenceLayer ({
68+ * client,
4569 * });
70+ *
71+ * // ... your function handler here
4672 * ```
4773 *
4874 * @category Persistence Layer
4975 */
50- class RedisPersistenceLayer extends BasePersistenceLayer {
51- readonly #client: RedisCompatibleClient ;
76+ class CachePersistenceLayer extends BasePersistenceLayer {
77+ readonly #client: CacheClient ;
5278 readonly #dataAttr: string ;
5379 readonly #expiryAttr: string ;
5480 readonly #inProgressExpiryAttr: string ;
5581 readonly #statusAttr: string ;
5682 readonly #validationKeyAttr: string ;
5783 readonly #orphanLockTimeout: number ;
5884
59- public constructor ( options : RedisPersistenceOptions ) {
85+ public constructor ( options : CachePersistenceOptions ) {
6086 super ( ) ;
6187
6288 this . #statusAttr =
@@ -76,9 +102,10 @@ class RedisPersistenceLayer extends BasePersistenceLayer {
76102 }
77103
78104 /**
79- * Deletes the idempotency record associated with a given record from Redis.
105+ * Deletes the idempotency record associated with a given record from the persistence store.
106+ *
80107 * This function is designed to be called after a Lambda handler invocation has completed processing.
81- * It ensures that the idempotency key associated with the record is removed from Redis to
108+ * It ensures that the idempotency key associated with the record is removed from the cache to
82109 * prevent future conflicts and to maintain the idempotency integrity.
83110 *
84111 * Note: it is essential that the idempotency key is not empty, as that would indicate the Lambda
@@ -110,25 +137,24 @@ class RedisPersistenceLayer extends BasePersistenceLayer {
110137 'Item does not exist in persistence store'
111138 ) ;
112139 }
113- let item : JSONObject ;
114140 try {
115- item = JSON . parse ( response ) ;
141+ const item = JSON . parse ( response as string ) ;
142+ return new IdempotencyRecord ( {
143+ idempotencyKey : idempotencyKey ,
144+ status : item [ this . #statusAttr] as IdempotencyRecordStatusValue ,
145+ expiryTimestamp : item [ this . #expiryAttr] as number | undefined ,
146+ inProgressExpiryTimestamp : item [ this . #inProgressExpiryAttr] as
147+ | number
148+ | undefined ,
149+ responseData : item [ this . #dataAttr] ,
150+ payloadHash : item [ this . #validationKeyAttr] as string | undefined ,
151+ } ) ;
116152 } catch ( error ) {
117153 throw new IdempotencyPersistenceConsistencyError (
118154 'Idempotency persistency consistency error, needs to be removed' ,
119155 error as Error
120156 ) ;
121157 }
122- return new IdempotencyRecord ( {
123- idempotencyKey : idempotencyKey ,
124- status : item [ this . #statusAttr] as IdempotencyRecordStatusValue ,
125- expiryTimestamp : item [ this . #expiryAttr] as number | undefined ,
126- inProgressExpiryTimestamp : item [ this . #inProgressExpiryAttr] as
127- | number
128- | undefined ,
129- responseData : item [ this . #dataAttr] ,
130- payloadHash : item [ this . #validationKeyAttr] as string | undefined ,
131- } ) ;
132158 }
133159
134160 protected async _updateRecord ( record : IdempotencyRecord ) : Promise < void > {
@@ -148,7 +174,8 @@ class RedisPersistenceLayer extends BasePersistenceLayer {
148174
149175 /**
150176 * Put a record in the persistence store with a status of "INPROGRESS".
151- * The method guards against concurrent execution by using Redis' conditional write operations.
177+ *
178+ * The method guards against concurrent execution by using conditional write operations.
152179 */
153180 async #putInProgressRecord( record : IdempotencyRecord ) : Promise < void > {
154181 const item : Record < string , unknown > = {
@@ -180,9 +207,8 @@ class RedisPersistenceLayer extends BasePersistenceLayer {
180207 * The idempotency key does not exist:
181208 * - first time that this invocation key is used
182209 * - previous invocation with the same key was deleted due to TTL
183- * - SET see https://redis .io/commands/set/
210+ * - SET see { @link https://valkey .io/commands/set/ | Valkey SET command}
184211 */
185-
186212 const response = await this . #client. set (
187213 record . idempotencyKey ,
188214 encodedItem ,
@@ -193,7 +219,7 @@ class RedisPersistenceLayer extends BasePersistenceLayer {
193219 ) ;
194220
195221 /**
196- * If response is not `null`, the redis SET operation was successful and the idempotency key was not
222+ * If response is not `null`, the SET operation was successful and the idempotency key was not
197223 * previously set. This indicates that we can safely proceed to the handler execution phase.
198224 * Most invocations should successfully proceed past this point.
199225 */
@@ -202,19 +228,21 @@ class RedisPersistenceLayer extends BasePersistenceLayer {
202228 }
203229
204230 /**
205- * If response is `null`, it indicates an existing record in Redis for the given idempotency key.
231+ * If response is `null`, it indicates an existing record in the cache for the given idempotency key.
232+ *
206233 * This could be due to:
207234 * - An active idempotency record from a previous invocation that has not yet expired.
208235 * - An orphan record where a previous invocation has timed out.
209- * - An expired idempotency record that has not been deleted by Redis .
236+ * - An expired idempotency record that has not been deleted yet .
210237 *
211238 * In any case, we proceed to retrieve the record for further inspection.
212239 */
213240 const existingRecord = await this . _getRecord ( record . idempotencyKey ) ;
214241
215- /** If the status of the idempotency record is `COMPLETED` and the record has not expired
216- * then a valid completed record exists. We raise an error to prevent duplicate processing
217- * of a request that has already been completed successfully.
242+ /**
243+ * If the status of the idempotency record is `COMPLETED` and the record has not expired
244+ * then a valid completed record exists. We raise an error to prevent duplicate processing
245+ * of a request that has already been completed successfully.
218246 */
219247 if (
220248 existingRecord . getStatus ( ) === IdempotencyRecordStatus . COMPLETED &&
@@ -226,10 +254,11 @@ class RedisPersistenceLayer extends BasePersistenceLayer {
226254 ) ;
227255 }
228256
229- /** If the idempotency record has a status of 'INPROGRESS' and has a valid `inProgressExpiryTimestamp`
230- * (meaning the timestamp is greater than the current timestamp in milliseconds), then we have encountered
231- * a valid in-progress record. This indicates that another process is currently handling the request, and
232- * to maintain idempotency, we raise an error to prevent concurrent processing of the same request.
257+ /**
258+ * If the idempotency record has a status of 'INPROGRESS' and has a valid `inProgressExpiryTimestamp`
259+ * (meaning the timestamp is greater than the current timestamp in milliseconds), then we have encountered
260+ * a valid in-progress record. This indicates that another process is currently handling the request, and
261+ * to maintain idempotency, we raise an error to prevent concurrent processing of the same request.
233262 */
234263 if (
235264 existingRecord . getStatus ( ) === IdempotencyRecordStatus . INPROGRESS &&
@@ -242,20 +271,22 @@ class RedisPersistenceLayer extends BasePersistenceLayer {
242271 ) ;
243272 }
244273
245- /** Reaching this point indicates that the idempotency record found is an orphan record. An orphan record is
246- * one that is neither completed nor in-progress within its expected time frame. It may result from a
247- * previous invocation that has timed out or an expired record that has yet to be cleaned up by Redis.
248- * We raise an error to handle this exceptional scenario appropriately.
274+ /**
275+ * Reaching this point indicates that the idempotency record found is an orphan record. An orphan record is
276+ * one that is neither completed nor in-progress within its expected time frame. It may result from a
277+ * previous invocation that has timed out or an expired record that has yet to be cleaned up from the cache.
278+ * We raise an error to handle this exceptional scenario appropriately.
249279 */
250280 throw new IdempotencyPersistenceConsistencyError (
251281 'Orphaned record detected'
252282 ) ;
253283 } catch ( error ) {
254284 if ( error instanceof IdempotencyPersistenceConsistencyError ) {
255- /** Handle an orphan record by attempting to acquire a lock, which by default lasts for 10 seconds.
256- * The purpose of acquiring the lock is to prevent race conditions with other processes that might
257- * also be trying to handle the same orphan record. Once the lock is acquired, we set a new value
258- * for the idempotency record in Redis with the appropriate time-to-live (TTL).
285+ /**
286+ * Handle an orphan record by attempting to acquire a lock, which by default lasts for 10 seconds.
287+ * The purpose of acquiring the lock is to prevent race conditions with other processes that might
288+ * also be trying to handle the same orphan record. Once the lock is acquired, we set a new value
289+ * for the idempotency record in the cache with the appropriate time-to-live (TTL).
259290 */
260291 await this . #acquireLock( record . idempotencyKey ) ;
261292
@@ -280,7 +311,7 @@ class RedisPersistenceLayer extends BasePersistenceLayer {
280311
281312 /**
282313 * Attempt to acquire a lock for a specified resource name, with a default timeout.
283- * This method attempts to set a lock using Redis to prevent concurrent access to a resource
314+ * This method attempts to set a lock to prevent concurrent access to a resource
284315 * identified by 'idempotencyKey'. It uses the 'NX' flag to ensure that the lock is only
285316 * set if it does not already exist, thereby enforcing mutual exclusion.
286317 *
@@ -306,4 +337,4 @@ class RedisPersistenceLayer extends BasePersistenceLayer {
306337 }
307338}
308339
309- export { RedisPersistenceLayer } ;
340+ export { CachePersistenceLayer } ;
0 commit comments