Skip to content

Commit 9fc043c

Browse files
committed
chore: rename to CachePersistenceLayer
1 parent ce5773c commit 9fc043c

File tree

7 files changed

+178
-142
lines changed

7 files changed

+178
-142
lines changed

packages/idempotency/package.json

+12-12
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,13 @@
5252
"import": "./lib/esm/types/DynamoDBPersistence.js",
5353
"require": "./lib/cjs/types/DynamoDBPersistence.js"
5454
},
55-
"./redis": {
56-
"import": "./lib/esm/persistence/RedisPersistenceLayer.js",
57-
"require": "./lib/cjs/persistence/RedisPersistenceLayer.js"
55+
"./cache": {
56+
"import": "./lib/esm/persistence/CachePersistenceLayer.js",
57+
"require": "./lib/cjs/persistence/CachePersistenceLayer.js"
5858
},
59-
"./redis/types": {
60-
"import": "./lib/esm/types/RedisPersistence.js",
61-
"require": "./lib/cjs/types/RedisPersistence.js"
59+
"./cache/types": {
60+
"import": "./lib/esm/types/CachePersistence.js",
61+
"require": "./lib/cjs/types/CachePersistence.js"
6262
},
6363
"./middleware": {
6464
"import": "./lib/esm/middleware/makeHandlerIdempotent.js",
@@ -83,13 +83,13 @@
8383
"lib/cjs/types/DynamoDBPersistence.d.ts",
8484
"lib/esm/types/DynamoDBPersistence.d.ts"
8585
],
86-
"redis": [
87-
"lib/cjs/persistence/RedisPersistenceLayer.d.ts",
88-
"lib/esm/persistence/RedisPersistenceLayer.d.ts"
86+
"cache": [
87+
"lib/cjs/persistence/CachePersistenceLayer.d.ts",
88+
"lib/esm/persistence/CachePersistenceLayer.d.ts"
8989
],
90-
"redis/types": [
91-
"lib/cjs/types/RedisPersistence.d.ts",
92-
"lib/esm/types/RedisPersistence.d.ts"
90+
"cache/types": [
91+
"lib/cjs/types/CachePersistence.d.ts",
92+
"lib/esm/types/CachePersistence.d.ts"
9393
],
9494
"middleware": [
9595
"lib/cjs/middleware/makeHandlerIdempotent.d.ts",

packages/idempotency/src/persistence/RedisPersistenceLayer.ts renamed to packages/idempotency/src/persistence/CachePersistenceLayer.ts

+88-57
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { JSONObject } from '@aws-lambda-powertools/commons/types';
21
import {
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';
1311
import 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';
1716
import { BasePersistenceLayer } from './BasePersistenceLayer.js';
1817
import { 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 };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { CachePersistenceLayer } from './../persistence/CachePersistenceLayer.js';
2+
import type { BasePersistenceAttributes } from './BasePersistenceLayer.js';
3+
4+
type CacheValue = string | Uint8Array<ArrayBufferLike>;
5+
6+
/**
7+
* Interface for clients compatible with Valkey and Redis-OSS operations.
8+
*
9+
* This interface defines the minimum set of operations that must be implemented
10+
* by a client to be used with the cache persistence layer.
11+
*
12+
* It supports basic key-value operations like get, set, and delete.
13+
*/
14+
interface CacheClient {
15+
/**
16+
* Retrieves the value associated with the given key.
17+
*
18+
* @param name - The key to get the value for
19+
*/
20+
get(name: string): Promise<CacheValue | null>;
21+
22+
/**
23+
* Sets the value for the specified key with optional parameters.
24+
*
25+
* @param name - The key to set
26+
* @param value - The value to set
27+
* @param options - Optional parameters for setting the value
28+
* @param options.EX - Set the specified expire time, in seconds (a positive integer)
29+
* @param options.NX - Only set the key if it does not already exist
30+
*/
31+
set(
32+
name: CacheValue,
33+
value: unknown,
34+
options?: {
35+
EX?: number;
36+
NX?: boolean;
37+
}
38+
): Promise<CacheValue | null>;
39+
40+
/**
41+
* Deletes the specified keys from the cache.
42+
*
43+
* @param keys - The keys to delete
44+
*/
45+
del(keys: string[]): Promise<number>;
46+
}
47+
48+
/**
49+
* Options for the {@link CachePersistenceLayer | `CachePersistenceLayer`} class constructor.
50+
*
51+
* @see {@link BasePersistenceAttributes} for full list of properties.
52+
*
53+
* @interface
54+
* @property client - The client must be properly initialized and connected
55+
*/
56+
interface CachePersistenceOptions extends BasePersistenceAttributes {
57+
client: CacheClient;
58+
}
59+
60+
export type { CacheClient, CachePersistenceOptions };

0 commit comments

Comments
 (0)