diff --git a/docs/Dockerfile b/docs/Dockerfile
index 831db214ce..35a27b3678 100644
--- a/docs/Dockerfile
+++ b/docs/Dockerfile
@@ -2,7 +2,7 @@
FROM squidfunk/mkdocs-material@sha256:eb04b60c566a8862be6b553157c16a92fbbfc45d71b7e4e8593526aecca63f52
# Install Node.js
-RUN apk add --no-cache nodejs=20.15.1-r0 npm
+RUN apk add --no-cache nodejs=22.13.1-r0 npm
COPY requirements.txt /tmp/
RUN pip install --require-hashes -r /tmp/requirements.txt
diff --git a/docs/features/idempotency.md b/docs/features/idempotency.md
index be4addef79..811a409672 100644
--- a/docs/features/idempotency.md
+++ b/docs/features/idempotency.md
@@ -12,6 +12,7 @@ The idempotency utility provides a simple solution to convert your Lambda functi
* Select a subset of the event as the idempotency key using JMESPath expressions
* Set a time window in which records with the same payload should be considered duplicates
* Expires in-progress executions if the Lambda function times out halfway through
+* Support for Amazon DynamoDB, Valkey, Redis OSS, or any Redis-compatible cache as the persistence layer
## Terminology
@@ -49,40 +50,29 @@ classDiagram
## Getting started
-### Installation
+!!! tip
+ Throughout the documentation we use Amazon DynamoDB as the default persistence layer. If you prefer to use a cache based persistence layer, you can learn more in the [cache database](#cache-database) and [`CachePersistenceLayer`](#cachepersistencelayer) sections.
-Install the library in your project
+### Amazon DynamoDB
```shell
npm i @aws-lambda-powertools/idempotency @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
```
-While we support Amazon DynamoDB as a persistence layer out of the box, you need to bring your own AWS SDK for JavaScript v3 DynamoDB client.
+#### IAM Permissions
-???+ note
- This utility supports **[AWS SDK for JavaScript v3](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/){target="_blank"} only**. If you are using the `nodejs18.x` runtime or newer, the AWS SDK for JavaScript v3 is already installed and you can install only the utility.
-
-### IAM Permissions
-
-Your Lambda function IAM Role must have `dynamodb:GetItem`, `dynamodb:PutItem`, `dynamodb:UpdateItem` and `dynamodb:DeleteItem` IAM permissions before using this feature. If you're using one of our examples: [AWS Serverless Application Model (SAM)](#required-resources) or [Terraform](#required-resources) the required permissions are already included.
-
-### Required resources
+Your Lambda function IAM Role must have `dynamodb:GetItem`, `dynamodb:PutItem`, `dynamodb:UpdateItem` and `dynamodb:DeleteItem` IAM permissions before using this feature. If you're using one of our examples below the required permissions are already included.
-Before getting started, you need to create a persistent storage layer where the idempotency utility can store its state - your lambda functions will need read and write access to it.
+#### Table configuration
-As of now, Amazon DynamoDB is the only supported persistent storage layer, so you'll need to create a table first.
-
-**Default table configuration**
-
-If you're not [changing the default configuration for the DynamoDB persistence layer](#dynamodbpersistencelayer), this is the expected default configuration:
+Unless you're looking to use an [existing table or customize each attribute](#dynamodbpersistencelayer), you only need the following:
| Configuration | Default value | Notes |
| ------------------ | :------------ | -------------------------------------------------------------------------------------- |
| Partition key | `id` | The id of each idempotency record which a combination of `functionName#hashOfPayload`. |
| TTL attribute name | `expiration` | This can only be configured after your table is created if you're using AWS Console. |
-???+ tip "Tip: You can share a single state table for all functions"
- You can reuse the same DynamoDB table to store idempotency state. We add the Lambda function name in addition to the idempotency key as a hash key.
+You **can** use a single DynamoDB table for all functions using this utility. We use the function name in addition to the idempotency key as a hash key.
=== "AWS Cloud Development Kit (CDK) example"
@@ -102,17 +92,51 @@ If you're not [changing the default configuration for the DynamoDB persistence l
--8<-- "examples/snippets/idempotency/templates/tableTerraform.tf"
```
-???+ warning "Warning: Large responses with DynamoDB persistence layer"
- When using this utility with DynamoDB, your function's responses must be [smaller than 400KB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html#limits-items){target="_blank"}.
+##### Limitations
+
+* **DynamoDB restricts [item sizes to 400KB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html#limits-items)**. This means that your idempotent function's response must be smaller than 400KB, otherwise your function will fail. Consider using the [cache persistence layer](#cache-database) if you need to store larger responses.
+* **Expect 2 WCUs per non-idempotent call**. During the first invocation, we use `PutItem` for locking and `UpdateItem` for completion. Consider reviewing [DynamoDB pricing documentation](https://aws.amazon.com/dynamodb/pricing/) to estimate cost.
+* **Expect 1 RCU per idempotent calls**. On subsequent invocations, we use `PutItem` to optimistically attempt to lock the record using `ConditionExpression` and `ReturnValuesOnConditionCheckFailure` to return the record if it exists. This is a single read operation.
+
+### Cache database
+
+Depending on the persistence layer you want to use, install the library and the corresponding peer dependencies.
+
+=== "Valkey"
+ ```shell
+ npm i @aws-lambda-powertools/idempotency @valkey/valkey-glide
+ ```
+
+=== "Redis OSS"
+ ```shell
+ npm i @aws-lambda-powertools/idempotency @redis/client
+ ```
+
+We recommend starting with a managed cache service, such as [Amazon ElastiCache for Valkey and for Redis OSS](https://aws.amazon.com/elasticache/redis/){target="_blank"} or [Amazon MemoryDB](https://aws.amazon.com/memorydb/){target="_blank"}.
- Larger items cannot be written to DynamoDB and will cause exceptions.
+In both services, you'll need to configure [VPC access](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html){target="_blank"} to your AWS Lambda and permissions for writing and reading from the cache.
-???+ info "Info: DynamoDB"
- Each function invocation will make only 1 request to DynamoDB by using DynamoDB's [conditional expressions](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ConditionExpressions.html){target="_blank"} to ensure that we don't overwrite existing records,
- and [ReturnValuesOnConditionCheckFailure](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ReturnValuesOnConditionCheckFailure){target="_blank"} to return the record if it exists.
- See [AWS Blog post on handling conditional write errors](https://aws.amazon.com/blogs/database/handle-conditional-write-errors-in-high-concurrency-scenarios-with-amazon-dynamodb/) for more details.
- For retried invocations, you will see 1WCU and 1RCU.
- Review the [DynamoDB pricing documentation](https://aws.amazon.com/dynamodb/pricing/){target="_blank"} to estimate the cost.
+#### Cache configuration
+
+=== "AWS Cloud Development Kit (CDK) example"
+
+ ```typescript title="template.ts" hl_lines="43"
+ --8<-- "examples/snippets/idempotency/templates/cacheCdk.ts"
+ ```
+
+ 1. Replace the VPC ID to match your VPC settings.
+ 2. Replace the Security Group ID to match your VPC settings.
+ 3. You can use the same template for Redis OSS, just replace the engine to `redis`.
+
+=== "AWS Serverless Application Model (SAM) example"
+
+ ```yaml hl_lines="6"
+ --8<-- "examples/snippets/idempotency/templates/cacheSam.yml"
+ ```
+
+ 1. You can use the same template for Redis OSS, just replace the engine to `redis`.
+ 2. Replace the Security Group ID and Subnet ID to match your VPC settings.
+ 3. Replace the Security Group ID and Subnet ID to match your VPC settings.
### MakeIdempotent function wrapper
@@ -523,7 +547,29 @@ sequenceDiagram
Optional idempotency key
-## Advanced
+#### Race condition with Cache
+
+
+```mermaid
+graph TD;
+ A(Existing orphan record in cache)-->A1;
+ A1[Two Lambda invoke at same time]-->B1[Lambda handler1];
+ B1-->B2[Fetch from Cache];
+ B2-->B3[Handler1 got orphan record];
+ B3-->B4[Handler1 acquired lock];
+ B4-->B5[Handler1 overwrite orphan record]
+ B5-->B6[Handler1 continue to execution];
+ A1-->C1[Lambda handler2];
+ C1-->C2[Fetch from Cache];
+ C2-->C3[Handler2 got orphan record];
+ C3-->C4[Handler2 failed to acquire lock];
+ C4-->C5[Handler2 wait and fetch from Cache];
+ C5-->C6[Handler2 return without executing];
+ B6-->D(Lambda handler executed only once);
+ C6-->D;
+```
+Race condition with Cache
+
### Persistence layers
@@ -551,6 +597,44 @@ When using DynamoDB as a persistence layer, you can alter the attribute names by
| **sortKeyAttr** | | | Sort key of the table (if table is configured with a sort key). |
| **staticPkValue** | | `idempotency#{LAMBDA_FUNCTION_NAME}` | Static value to use as the partition key. Only used when **sort_key_attr** is set. |
+#### CachePersistenceLayer
+
+The `CachePersistenceLayer` enables you to use Valkey, Redis OSS, or any Redis-compatible cache as the persistence layer for idempotency state. You need to provide your own cache client.
+
+We recommend using [`@valkey/valkey-glide`](https://www.npmjs.com/package/@valkey/valkey-glide){target="_blank"} for Valkey or [`@redis/client`](https://www.npmjs.com/package/@redis/client){target="_blank"} for Redis. However, any Redis OSS-compatible client should work.
+
+???+ info
+ Make sure your cache client is configured and connected before using it with `CachePersistenceLayer`.
+
+=== "Using Valkey Client"
+ ```typescript hl_lines="4 7-16 19"
+ --8<-- "examples/snippets/idempotency/cachePersistenceLayerValkey.ts:5:"
+ ```
+
+=== "Using Redis Client"
+ ```typescript hl_lines="4 7-10 13"
+ --8<-- "examples/snippets/idempotency/cachePersistenceLayerRedis.ts:5:"
+ ```
+
+When using Cache as a persistence layer, you can alter the attribute names by passing these parameters when initializing the persistence layer:
+
+| Parameter | Required | Default | Description |
+| ------------------------ | ------------------ | ------------------------------------ | -------------------------------------------------------------------------------------------------------- |
+| **client** | :heavy_check_mark: | | A connected Redis-compatible client instance |
+| **expiryAttr** | | `expiration` | Unix timestamp of when record expires |
+| **inProgressExpiryAttr** | | `in_progress_expiration` | Unix timestamp of when record expires while in progress (in case of the invocation times out) |
+| **statusAttr** | | `status` | Stores status of the lambda execution during and after invocation |
+| **dataAttr** | | `data` | Stores results of successfully executed Lambda handlers |
+| **validationKeyAttr** | | `validation` | Hashed representation of the parts of the event used for validation |
+
+=== "Customizing CachePersistenceLayer"
+
+ ```typescript hl_lines="20-24"
+ --8<-- "examples/snippets/idempotency/customizeCachePersistenceLayer.ts:5:"
+ ```
+
+## Advanced
+
### Customizing the default behavior
Idempotent decorator can be further configured with **`IdempotencyConfig`** as seen in the previous examples. These are the available options for further configuration
@@ -861,6 +945,22 @@ When testing your Lambda function locally, you can use a local DynamoDB instance
--8<-- "examples/snippets/idempotency/workingWithLocalDynamoDB.ts"
```
+### Testing with local cache
+
+Likewise, when using a cache database, you can use a local Valkey or Redis-OSS instance as a local server and replace the endpoint and port in the environment variables.
+
+=== "valkeyHandler.test.ts"
+
+ ```typescript hl_lines="5-8"
+ --8<-- "examples/snippets/idempotency/workingWithLocalCacheValkey.test.ts"
+ ```
+
+=== "valkeyHandler.ts"
+
+ ```typescript
+ --8<-- "examples/snippets/idempotency/cachePersistenceLayerValkey.ts:5:"
+ ```
+
## Extra resources
If you're interested in a deep dive on how Amazon uses idempotency when building our APIs, check out
diff --git a/examples/snippets/idempotency/cachePersistenceLayerRedis.ts b/examples/snippets/idempotency/cachePersistenceLayerRedis.ts
new file mode 100644
index 0000000000..18785fb826
--- /dev/null
+++ b/examples/snippets/idempotency/cachePersistenceLayerRedis.ts
@@ -0,0 +1,32 @@
+declare function processPayment(): Promise<{
+ paymentId: string;
+}>;
+
+import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache';
+import { makeHandlerIdempotent } from '@aws-lambda-powertools/idempotency/middleware';
+import middy from '@middy/core';
+import { createClient } from '@redis/client';
+import type { Context } from 'aws-lambda';
+
+const client = await createClient({
+ url: `rediss://${process.env.CACHE_ENDPOINT}:${process.env.CACHE_PORT}`,
+ username: 'default',
+}).connect();
+
+const persistenceStore = new CachePersistenceLayer({
+ client,
+});
+
+export const handler = middy(async (_event: unknown, _context: Context) => {
+ const payment = await processPayment();
+
+ return {
+ paymentId: payment?.paymentId,
+ message: 'success',
+ statusCode: 200,
+ };
+}).use(
+ makeHandlerIdempotent({
+ persistenceStore,
+ })
+);
diff --git a/examples/snippets/idempotency/cachePersistenceLayerValkey.ts b/examples/snippets/idempotency/cachePersistenceLayerValkey.ts
new file mode 100644
index 0000000000..172b8b277a
--- /dev/null
+++ b/examples/snippets/idempotency/cachePersistenceLayerValkey.ts
@@ -0,0 +1,38 @@
+declare function processPayment(): Promise<{
+ paymentId: string;
+}>;
+
+import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache';
+import { makeHandlerIdempotent } from '@aws-lambda-powertools/idempotency/middleware';
+import middy from '@middy/core';
+import { GlideClient } from '@valkey/valkey-glide';
+import type { Context } from 'aws-lambda';
+
+const client = await GlideClient.createClient({
+ addresses: [
+ {
+ host: String(process.env.CACHE_ENDPOINT),
+ port: Number(process.env.CACHE_PORT),
+ },
+ ],
+ useTLS: true,
+ requestTimeout: 2000,
+});
+
+const persistenceStore = new CachePersistenceLayer({
+ client,
+});
+
+export const handler = middy(async (_event: unknown, _context: Context) => {
+ const payment = await processPayment();
+
+ return {
+ paymentId: payment?.paymentId,
+ message: 'success',
+ statusCode: 200,
+ };
+}).use(
+ makeHandlerIdempotent({
+ persistenceStore,
+ })
+);
diff --git a/examples/snippets/idempotency/customizeCachePersistenceLayer.ts b/examples/snippets/idempotency/customizeCachePersistenceLayer.ts
new file mode 100644
index 0000000000..5b4cf08b58
--- /dev/null
+++ b/examples/snippets/idempotency/customizeCachePersistenceLayer.ts
@@ -0,0 +1,43 @@
+declare function processPayment(): Promise<{
+ paymentId: string;
+}>;
+
+import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache';
+import { makeHandlerIdempotent } from '@aws-lambda-powertools/idempotency/middleware';
+import middy from '@middy/core';
+import { GlideClient } from '@valkey/valkey-glide';
+import type { Context } from 'aws-lambda';
+
+const client = await GlideClient.createClient({
+ addresses: [
+ {
+ host: String(process.env.CACHE_ENDPOINT),
+ port: Number(process.env.CACHE_PORT),
+ },
+ ],
+ useTLS: true,
+ requestTimeout: 5000,
+});
+
+const persistenceStore = new CachePersistenceLayer({
+ client,
+ expiryAttr: 'expiresAt',
+ inProgressExpiryAttr: 'inProgressExpiresAt',
+ statusAttr: 'currentStatus',
+ dataAttr: 'resultData',
+ validationKeyAttr: 'validationKey',
+});
+
+export const handler = middy(async (_event: unknown, _context: Context) => {
+ const payment = await processPayment();
+
+ return {
+ paymentId: payment?.paymentId,
+ message: 'success',
+ statusCode: 200,
+ };
+}).use(
+ makeHandlerIdempotent({
+ persistenceStore,
+ })
+);
diff --git a/examples/snippets/idempotency/templates/cacheCdk.ts b/examples/snippets/idempotency/templates/cacheCdk.ts
new file mode 100644
index 0000000000..f1a6cbdf59
--- /dev/null
+++ b/examples/snippets/idempotency/templates/cacheCdk.ts
@@ -0,0 +1,99 @@
+import { Duration, RemovalPolicy, Stack, type StackProps } from 'aws-cdk-lib';
+import { Port, SecurityGroup, Vpc } from 'aws-cdk-lib/aws-ec2';
+import { CfnServerlessCache } from 'aws-cdk-lib/aws-elasticache';
+import {
+ Architecture,
+ Code,
+ LayerVersion,
+ Runtime,
+} from 'aws-cdk-lib/aws-lambda';
+import { NodejsFunction, OutputFormat } from 'aws-cdk-lib/aws-lambda-nodejs';
+import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs';
+import type { Construct } from 'constructs';
+
+export class ValkeyStack extends Stack {
+ constructor(scope: Construct, id: string, props?: StackProps) {
+ super(scope, id, props);
+
+ const vpc = Vpc.fromLookup(this, 'MyVpc', {
+ vpcId: 'vpc-{your_vpc_id}', // (1)!
+ });
+
+ const fnSecurityGroup = new SecurityGroup(this, 'ValkeyFnSecurityGroup', {
+ vpc,
+ allowAllOutbound: true,
+ description: 'Security group for Valkey function',
+ });
+
+ const idempotencyCacheSG = SecurityGroup.fromSecurityGroupId(
+ this,
+ 'IdempotencyCacheSG',
+ 'security-{your_sg_id}' // (2)!
+ );
+ idempotencyCacheSG.addIngressRule(
+ fnSecurityGroup,
+ Port.tcp(6379),
+ 'Allow Lambda to connect to serverless cache'
+ );
+
+ const serverlessCache = new CfnServerlessCache(
+ this,
+ 'MyCfnServerlessCache',
+ {
+ engine: 'valkey', // (3)!
+ majorEngineVersion: '8',
+ serverlessCacheName: 'idempotency-cache',
+ subnetIds: [
+ vpc.privateSubnets[0].subnetId,
+ vpc.privateSubnets[1].subnetId,
+ ],
+ securityGroupIds: [idempotencyCacheSG.securityGroupId],
+ }
+ );
+
+ const valkeyLayer = new LayerVersion(this, 'ValkeyLayer', {
+ removalPolicy: RemovalPolicy.DESTROY,
+ compatibleArchitectures: [Architecture.ARM_64],
+ compatibleRuntimes: [Runtime.NODEJS_22_X],
+ code: Code.fromAsset('./lib/layers/valkey-glide'),
+ });
+
+ const fnName = 'ValkeyFn';
+ const logGroup = new LogGroup(this, 'MyLogGroup', {
+ logGroupName: `/aws/lambda/${fnName}`,
+ removalPolicy: RemovalPolicy.DESTROY,
+ retention: RetentionDays.ONE_DAY,
+ });
+ const fn = new NodejsFunction(this, 'MyFunction', {
+ functionName: fnName,
+ logGroup,
+ runtime: Runtime.NODEJS_22_X,
+ architecture: Architecture.ARM_64,
+ memorySize: 512,
+ timeout: Duration.seconds(30),
+ entry: './src/idempotency.ts',
+ handler: 'handler',
+ layers: [valkeyLayer],
+ bundling: {
+ minify: true,
+ mainFields: ['module', 'main'],
+ sourceMap: true,
+ format: OutputFormat.ESM,
+ externalModules: ['@valkey/valkey-glide'],
+ metafile: true,
+ banner:
+ "import { createRequire } from 'module';const require = createRequire(import.meta.url);",
+ },
+ vpc,
+ securityGroups: [fnSecurityGroup],
+ });
+ fn.addEnvironment(
+ 'CACHE_ENDPOINT',
+ serverlessCache.getAtt('Endpoint.Address').toString()
+ );
+ fn.addEnvironment(
+ 'CACHE_PORT',
+ serverlessCache.getAtt('Endpoint.Port').toString()
+ );
+ }
+}
diff --git a/examples/snippets/idempotency/templates/cacheSam.yml b/examples/snippets/idempotency/templates/cacheSam.yml
new file mode 100644
index 0000000000..4541651907
--- /dev/null
+++ b/examples/snippets/idempotency/templates/cacheSam.yml
@@ -0,0 +1,30 @@
+AWSTemplateFormatVersion: "2010-09-09"
+Transform: AWS::Serverless-2016-10-31
+
+Resources:
+ CacheIdempotency:
+ Type: AWS::ElastiCache::ServerlessCache
+ Properties:
+ Engine: valkey # (1)!
+ ServerlessCacheName: idempotency-cache
+ SecurityGroupIds: # (2)!
+ - security-{your_sg_id}
+ SubnetIds:
+ - subnet-{your_subnet_id_1}
+ - subnet-{your_subnet_id_2}
+
+ HelloWorldFunction:
+ Type: AWS::Serverless::Function
+ Properties:
+ Runtime: nodejs22.x
+ Handler: index.js
+ VpcConfig: # (3)!
+ SecurityGroupIds:
+ - security-{your_sg_id}
+ SubnetIds:
+ - subnet-{your_subnet_id_1}
+ - subnet-{your_subnet_id_2}
+ Environment:
+ Variables:
+ CACHE_ENDPOINT: !GetAtt CacheIdempotency.Endpoint.Address
+ CACHE_PORT: !GetAtt CacheIdempotency.Endpoint.Port
\ No newline at end of file
diff --git a/examples/snippets/idempotency/templates/tableSam.yaml b/examples/snippets/idempotency/templates/tableSam.yaml
index 9de5e226de..38f1a4a06d 100644
--- a/examples/snippets/idempotency/templates/tableSam.yaml
+++ b/examples/snippets/idempotency/templates/tableSam.yaml
@@ -18,7 +18,7 @@ Resources:
Type: AWS::Serverless::Function
Properties:
Runtime: nodejs22.x
- Handler: app.py
+ Handler: index.js
Policies:
- Statement:
- Sid: AllowDynamodbReadWrite
diff --git a/examples/snippets/idempotency/workingWithLocalCacheValkey.test.ts b/examples/snippets/idempotency/workingWithLocalCacheValkey.test.ts
new file mode 100644
index 0000000000..949886269d
--- /dev/null
+++ b/examples/snippets/idempotency/workingWithLocalCacheValkey.test.ts
@@ -0,0 +1,36 @@
+import type { Context } from 'aws-lambda';
+import { describe, expect, it, vi } from 'vitest';
+import { handler } from './cachePersistenceLayerValkey.js';
+
+vi.hoisted(() => {
+ process.env.CACHE_ENDPOINT = 'localhost';
+ process.env.CACHE_PORT = '6379';
+});
+
+const context = {
+ functionName: 'foo-bar-function',
+ memoryLimitInMB: '128',
+ invokedFunctionArn:
+ 'arn:aws:lambda:eu-west-1:123456789012:function:foo-bar-function',
+ awsRequestId: 'c6af9ac6-7b61-11e6-9a41-93e812345678',
+ getRemainingTimeInMillis: () => 1234,
+} as Context;
+
+describe('Idempotent handler', () => {
+ it('returns the same response', async () => {
+ // Act
+ const response = await handler(
+ {
+ foo: 'bar',
+ },
+ context
+ );
+
+ // Assess
+ expect(response).toEqual({
+ paymentId: expect.any(String),
+ message: 'success',
+ statusCode: 200,
+ });
+ });
+});
diff --git a/examples/snippets/package.json b/examples/snippets/package.json
index 0ea1ab9585..7a925ca0a9 100644
--- a/examples/snippets/package.json
+++ b/examples/snippets/package.json
@@ -39,6 +39,8 @@
"@aws-sdk/client-ssm": "^3.810.0",
"@aws-sdk/util-dynamodb": "^3.810.0",
"@middy/core": "^4.7.0",
+ "@redis/client": "^5.0.1",
+ "@valkey/valkey-glide": "^1.3.4",
"aws-sdk": "^2.1692.0",
"aws-sdk-client-mock": "^4.1.0",
"zod": "^3.24.4"
diff --git a/package-lock.json b/package-lock.json
index d6571c4d6f..653ef5fad1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -98,6 +98,8 @@
"@aws-sdk/client-ssm": "^3.810.0",
"@aws-sdk/util-dynamodb": "^3.810.0",
"@middy/core": "^4.7.0",
+ "@redis/client": "^5.0.1",
+ "@valkey/valkey-glide": "^1.3.4",
"aws-sdk": "^2.1692.0",
"aws-sdk-client-mock": "^4.1.0",
"zod": "^3.24.4"
@@ -12096,13 +12098,96 @@
"node": ">=14"
}
},
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+ "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+ "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+ "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1",
+ "@protobufjs/inquire": "^1.1.0"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/@protobufjs/inquire": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+ "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "peer": true
+ },
"node_modules/@redis/client": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-5.0.1.tgz",
"integrity": "sha512-k0EJvlMGEyBqUD3orKe0UMZ66fPtfwqPIr+ZSd853sXj2EyhNtPXSx+J6sENXJNgAlEBhvD+57Dwt0qTisKB0A==",
+ "devOptional": true,
"license": "MIT",
- "optional": true,
- "peer": true,
"dependencies": {
"cluster-key-slot": "1.1.2"
},
@@ -13698,6 +13783,184 @@
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="
},
+ "node_modules/@valkey/valkey-glide": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@valkey/valkey-glide/-/valkey-glide-1.3.4.tgz",
+ "integrity": "sha512-EXjpGEeTjs2uhJm8ZNkHEK8d3qCQmppmxsv+553S6L9fArZTBvKMmfh7P2H7nxletHx6sxs+fc2UFFw9M5k5SQ==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "optionalDependencies": {
+ "@valkey/valkey-glide-darwin-arm64": "1.3.4",
+ "@valkey/valkey-glide-darwin-x64": "1.3.4",
+ "@valkey/valkey-glide-linux-arm64": "1.3.4",
+ "@valkey/valkey-glide-linux-musl-arm64": "1.3.4",
+ "@valkey/valkey-glide-linux-musl-x64": "1.3.4",
+ "@valkey/valkey-glide-linux-x64": "1.3.4"
+ }
+ },
+ "node_modules/@valkey/valkey-glide-darwin-arm64": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@valkey/valkey-glide-darwin-arm64/-/valkey-glide-darwin-arm64-1.3.4.tgz",
+ "integrity": "sha512-Ry8PEWdMtENju/thkqMEDCcf9wJSz5JavpwTJ8+NEaTVeYUSNK7PJ91ZrgxTWyDr51YAdA8e5MjyK2+p2X1HiQ==",
+ "bundleDependencies": [
+ "glide-rs"
+ ],
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "dependencies": {
+ "glide-rs": "file:rust-client",
+ "long": "5",
+ "protobufjs": "7"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@valkey/valkey-glide-darwin-arm64/node_modules/glide-rs": {
+ "version": "0.1.0",
+ "inBundle": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@valkey/valkey-glide-darwin-x64": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@valkey/valkey-glide-darwin-x64/-/valkey-glide-darwin-x64-1.3.4.tgz",
+ "integrity": "sha512-Jl/x1WovhwiP7lWUcA99tw68Usb3GCHZ/ISD/1F23cVErXJ9qB4ZNCFI8Y1QrmwEPSeojKsxSpqylrsInuv1ng==",
+ "bundleDependencies": [
+ "glide-rs"
+ ],
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "dependencies": {
+ "glide-rs": "file:rust-client",
+ "long": "5",
+ "protobufjs": "7"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@valkey/valkey-glide-linux-arm64": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@valkey/valkey-glide-linux-arm64/-/valkey-glide-linux-arm64-1.3.4.tgz",
+ "integrity": "sha512-ubTZDaEqlwS8NNuIggWt36I1BbtUNE17uHrCaBvUfc+aR1870r4xqvQNyvpQhT/VezfJisSBGKudFdLKeFrIjg==",
+ "bundleDependencies": [
+ "glide-rs"
+ ],
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "dependencies": {
+ "glide-rs": "file:rust-client",
+ "long": "5",
+ "protobufjs": "7"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@valkey/valkey-glide-linux-musl-arm64": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@valkey/valkey-glide-linux-musl-arm64/-/valkey-glide-linux-musl-arm64-1.3.4.tgz",
+ "integrity": "sha512-zNkmKXrT9ebe/XWtevzFleRrXycrzsLtmDjdQJGVUC96gUA3DcczK/gfu2rgvgH84C5s5jKbSM7oeo8f8eX4Vw==",
+ "bundleDependencies": [
+ "glide-rs"
+ ],
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "dependencies": {
+ "glide-rs": "file:rust-client",
+ "long": "5",
+ "protobufjs": "7"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@valkey/valkey-glide-linux-musl-x64": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@valkey/valkey-glide-linux-musl-x64/-/valkey-glide-linux-musl-x64-1.3.4.tgz",
+ "integrity": "sha512-QSaLTp81V0agKcGXsr3LUF+Vl4nTHrf5eBNV7/2GwbA4Pwed2x8TzORAjv+aS368hWR/9zftbVA1Xjn8TAND3w==",
+ "bundleDependencies": [
+ "glide-rs"
+ ],
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "dependencies": {
+ "glide-rs": "file:rust-client",
+ "long": "5",
+ "protobufjs": "7"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@valkey/valkey-glide-linux-x64": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@valkey/valkey-glide-linux-x64/-/valkey-glide-linux-x64-1.3.4.tgz",
+ "integrity": "sha512-j9acGMsr7T1oY377xBsIkY4rjtDqb78/OSH3CFcdntcJZU6u6fvcqGXG5Co/iRDvsm+q9uuYd/H5ITnGx60kng==",
+ "bundleDependencies": [
+ "glide-rs"
+ ],
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "dependencies": {
+ "glide-rs": "file:rust-client",
+ "long": "5",
+ "protobufjs": "7"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/@vitest/coverage-v8": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.3.tgz",
@@ -15664,9 +15927,8 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
+ "devOptional": true,
"license": "Apache-2.0",
- "optional": true,
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -16304,6 +16566,16 @@
"node": ">=4"
}
},
+ "node_modules/detect-libc": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
+ "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/devlop": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
@@ -19200,6 +19472,14 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
+ "node_modules/long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "peer": true
+ },
"node_modules/loupe": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz",
@@ -22197,6 +22477,32 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
+ "node_modules/protobufjs": {
+ "version": "7.5.2",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.2.tgz",
+ "integrity": "sha512-f2ls6rpO6G153Cy+o2XQ+Y0sARLOZ17+OGVLHrc3VUKcLHYKEKWbkSujdBWQXM7gKn5NTfp0XnRPZn1MIu8n9w==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.4",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.0",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.0",
+ "@types/node": ">=13.7.0",
+ "long": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/protocols": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz",
diff --git a/packages/idempotency/src/persistence/CachePersistenceLayer.ts b/packages/idempotency/src/persistence/CachePersistenceLayer.ts
index 7eca1ca465..0b48eae3a3 100644
--- a/packages/idempotency/src/persistence/CachePersistenceLayer.ts
+++ b/packages/idempotency/src/persistence/CachePersistenceLayer.ts
@@ -39,10 +39,11 @@ import { IdempotencyRecord } from './IdempotencyRecord.js';
*
* const client = await GlideClient.createClient({
* addresses: [{
- * host: process.env.CACHE_ENDPOINT,
+ * host: String(process.env.CACHE_ENDPOINT),
* port: Number(process.env.CACHE_PORT),
* }],
* useTLS: true,
+ * requestTimeout: 2000
* });
*
* const persistence = new CachePersistenceLayer({