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({