diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c288b8895f..42a899ded4 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,7 +8,7 @@ // Append -bullseye or -buster to pin to an OS version. // Use -bullseye variants on local arm64/Apple Silicon. "args": { - "VARIANT": "22-bullseye" + "VARIANT": "24-bullseye" } }, "customizations": { diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 956f81860a..800cf21170 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -67,6 +67,7 @@ body: attributes: label: AWS Lambda function runtime options: + - 24.x - 22.x - 20.x validations: diff --git a/.github/workflows/bootstrap_region.yml b/.github/workflows/bootstrap_region.yml index 80db607dbe..3fb7e9b242 100644 --- a/.github/workflows/bootstrap_region.yml +++ b/.github/workflows/bootstrap_region.yml @@ -49,7 +49,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: "22" + node-version: 24 - name: Setup dependencies uses: aws-powertools/actions/.github/actions/cached-node-modules@29979bc5339bf54f76a11ac36ff67701986bb0f0 - id: credentials diff --git a/.github/workflows/make-release.yml b/.github/workflows/make-release.yml index fb622cc7d4..29d6549b99 100644 --- a/.github/workflows/make-release.yml +++ b/.github/workflows/make-release.yml @@ -50,7 +50,7 @@ jobs: - name: Setup NodeJS uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: "22" + node-version: 24 cache: "npm" registry-url: 'https://registry.npmjs.org' - name: Setup auth tokens diff --git a/.github/workflows/make-version.yml b/.github/workflows/make-version.yml index cf3349e0a8..d688529242 100644 --- a/.github/workflows/make-version.yml +++ b/.github/workflows/make-version.yml @@ -25,7 +25,7 @@ jobs: pull-requests: write runs-on: ubuntu-latest env: - NODE_VERSION: "22" + NODE_VERSION: "24" outputs: RELEASE_VERSION: ${{ steps.version-n-changelog.outputs.new-version }} steps: diff --git a/.github/workflows/publish-package.yml b/.github/workflows/publish-package.yml index f255290e9f..7739744eb6 100644 --- a/.github/workflows/publish-package.yml +++ b/.github/workflows/publish-package.yml @@ -41,7 +41,7 @@ jobs: - name: Setup NodeJS uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: "22" + node-version: 24 cache: "npm" registry-url: 'https://registry.npmjs.org' - name: Setup auth tokens diff --git a/.github/workflows/publish_layer.yml b/.github/workflows/publish_layer.yml index ff3042f788..29df13a9cf 100644 --- a/.github/workflows/publish_layer.yml +++ b/.github/workflows/publish_layer.yml @@ -44,7 +44,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: "22" + node-version: 24 - name: Setup dependencies uses: aws-powertools/actions/.github/actions/cached-node-modules@29979bc5339bf54f76a11ac36ff67701986bb0f0 - name: CDK build diff --git a/.github/workflows/quality_check.yml b/.github/workflows/quality_check.yml index 925da42cb6..ec4b9053bc 100644 --- a/.github/workflows/quality_check.yml +++ b/.github/workflows/quality_check.yml @@ -19,7 +19,7 @@ jobs: NODE_ENV: dev strategy: matrix: - version: [20, 22] + version: [20, 22, 24] workspace: [ "packages/batch", "packages/commons", @@ -68,7 +68,7 @@ jobs: - name: Setup NodeJS uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: 22 + node-version: 24 cache: "npm" - name: Setup dependencies uses: aws-powertools/actions/.github/actions/cached-node-modules@29979bc5339bf54f76a11ac36ff67701986bb0f0 @@ -86,7 +86,7 @@ jobs: - name: Setup NodeJS uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: 22 + node-version: 24 cache: "npm" - name: Setup dependencies uses: aws-powertools/actions/.github/actions/cached-node-modules@29979bc5339bf54f76a11ac36ff67701986bb0f0 @@ -104,7 +104,7 @@ jobs: - name: Setup NodeJS uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: 22 + node-version: 24 cache: "npm" - name: Setup dependencies uses: aws-powertools/actions/.github/actions/cached-node-modules@29979bc5339bf54f76a11ac36ff67701986bb0f0 @@ -120,7 +120,7 @@ jobs: - name: Setup NodeJS uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: 22 + node-version: 24 cache: "npm" - name: Setup dependencies uses: aws-powertools/actions/.github/actions/cached-node-modules@29979bc5339bf54f76a11ac36ff67701986bb0f0 diff --git a/.github/workflows/reusable-run-linting-check-and-unit-tests.yml b/.github/workflows/reusable-run-linting-check-and-unit-tests.yml index d3cbc79fee..3da0a1a89b 100644 --- a/.github/workflows/reusable-run-linting-check-and-unit-tests.yml +++ b/.github/workflows/reusable-run-linting-check-and-unit-tests.yml @@ -39,7 +39,7 @@ jobs: NODE_ENV: dev strategy: matrix: - version: [20, 22] + version: [20, 22, 24] workspace: [ "packages/batch", "packages/commons", @@ -92,7 +92,7 @@ jobs: name: Setup Node.js uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: 22 + node-version: 24 cache: "npm" - &setup_dependencies name: Setup dependencies diff --git a/.github/workflows/reusable_deploy_layer_stack.yml b/.github/workflows/reusable_deploy_layer_stack.yml index 7ee6a6a616..320b8c5127 100644 --- a/.github/workflows/reusable_deploy_layer_stack.yml +++ b/.github/workflows/reusable_deploy_layer_stack.yml @@ -83,7 +83,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: "22" + node-version: 24 - name: Setup dependencies uses: aws-powertools/actions/.github/actions/cached-node-modules@29979bc5339bf54f76a11ac36ff67701986bb0f0 - name: Download artifact diff --git a/.github/workflows/reusable_publish_docs.yml b/.github/workflows/reusable_publish_docs.yml index a19210c32e..9677b8c795 100644 --- a/.github/workflows/reusable_publish_docs.yml +++ b/.github/workflows/reusable_publish_docs.yml @@ -54,7 +54,7 @@ jobs: - name: Setup NodeJS uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: "22" + node-version: 24 cache: "npm" - name: Setup dependencies uses: aws-powertools/actions/.github/actions/cached-node-modules@29979bc5339bf54f76a11ac36ff67701986bb0f0 diff --git a/.github/workflows/run-e2e-tests.yml b/.github/workflows/run-e2e-tests.yml index c6eed9fc9b..a704d8ca56 100644 --- a/.github/workflows/run-e2e-tests.yml +++ b/.github/workflows/run-e2e-tests.yml @@ -35,7 +35,7 @@ jobs: packages/tracer, layers, ] - version: [20, 22] + version: [20, 22, 24] arch: [x86_64, arm64] fail-fast: false steps: @@ -53,11 +53,11 @@ jobs: - name: Setup Node.js uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: '22' + node-version: 24 - name: Setup dependencies uses: aws-powertools/actions/.github/actions/cached-node-modules@3b5b8e2e58b7af07994be982e83584a94e8c76c5 # v1.5.0 with: - node-version: '22' + node-version: 24 - name: Setup AWS credentials uses: aws-actions/configure-aws-credentials@00943011d9042930efac3dcd3a170e4273319bc8 # v5.1.0 with: diff --git a/.nvmrc b/.nvmrc index deed13c016..941ea4863d 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -lts/jod +lts/krypton \ No newline at end of file diff --git a/docs/contributing/setup.md b/docs/contributing/setup.md index f3671355c6..32b7092137 100644 --- a/docs/contributing/setup.md +++ b/docs/contributing/setup.md @@ -25,7 +25,7 @@ graph LR Unless you're using the pre-configured Cloud environment, you'll need the following installed: * [GitHub account](https://github.com/join){target="_blank" rel="nofollow"}. You'll need to be able to fork, clone, and contribute via pull request. -* [Node.js 22.x](https://nodejs.org/download/release/latest-v22.x/){target="_blank" rel="nofollow"}. The repository contains an `.nvmrc` file, so if you use tools like [nvm](https://github.com/nvm-sh/nvm#nvmrc), [fnm](https://github.com/Schniz/fnm) you can switch version quickly. +* [Node.js 24.x](https://nodejs.org/download/release/latest-v24.x/){target="_blank" rel="nofollow"}. The repository contains an `.nvmrc` file, so if you use tools like [nvm](https://github.com/nvm-sh/nvm#nvmrc), [fnm](https://github.com/Schniz/fnm) you can switch version quickly. * [npm 10.x](https://www.npmjs.com/). We use it to install dependencies and manage the workspaces. * [Docker](https://docs.docker.com/engine/install/){target="_blank" rel="nofollow"}. We use it to run documentation, and non-JavaScript tooling. * [Fork the repository](https://github.com/aws-powertools/powertools-lambda-typescript/fork). You'll work against your fork of this repository. diff --git a/docs/contributing/testing.md b/docs/contributing/testing.md index 771b0b2db0..ab833fd3e4 100644 --- a/docs/contributing/testing.md +++ b/docs/contributing/testing.md @@ -74,7 +74,7 @@ To run integration tests you'll need to set up an AWS account and obtain credent * `npm test:e2e -ws` to run all the integration tests for all the modules sequentially * `test:e2e:parallel` to run all the integration tests for all the modules in parallel * `npm test:e2e -w packages/metrics` to run all the integration tests for the `metrics` module -* `npm run test:e2e:nodejs22x -w packages/metrics` to run all the integration tests for the `metrics` module using the `nodejs22x` runtime +* `npm run test:e2e:nodejs24x -w packages/metrics` to run all the integration tests for the `metrics` module using the `nodejs24x` runtime The tests will deploy the necessary AWS resources using AWS CDK, and will run the Lambda functions using the AWS SDK. After that, the tests will verify the Lambda functions behave as expected by checking logs, metrics, traces, and other resources as needed. Finally, the tests will destroy all the AWS resources created at the beginning. diff --git a/docs/features/logger.md b/docs/features/logger.md index 833389cade..7e97f6ad4c 100644 --- a/docs/features/logger.md +++ b/docs/features/logger.md @@ -75,7 +75,7 @@ Check API docs to learn more about [Logger constructor options](https://docs.aws ShoppingCartApiFunction: Type: AWS::Serverless::Function Properties: - Runtime: nodejs22.x + Runtime: nodejs24.x Environment: Variables: POWERTOOLS_LOG_LEVEL: WARN diff --git a/docs/features/metrics.md b/docs/features/metrics.md index 6f1392e8d1..8bfed4986a 100644 --- a/docs/features/metrics.md +++ b/docs/features/metrics.md @@ -93,7 +93,7 @@ The `Metrics` utility is instantiated outside of the Lambda handler. In doing th HelloWorldFunction: Type: AWS::Serverless::Function Properties: - Runtime: nodejs22.x + Runtime: nodejs24.x Environment: Variables: POWERTOOLS_SERVICE_NAME: orders diff --git a/docs/features/tracer.md b/docs/features/tracer.md index 1ac01700f0..b61297ad68 100644 --- a/docs/features/tracer.md +++ b/docs/features/tracer.md @@ -63,7 +63,7 @@ To use it in an ESM project, you can instruct your bundler to use the `require` super(scope, id, props); const handler = new NodejsFunction(this, 'helloWorldFunction', { - runtime: Runtime.NODEJS_22_X, + runtime: Runtime.NODEJS_24_X, handler: 'handler', entry: 'src/index.ts', bundling: { @@ -90,7 +90,7 @@ To use it in an ESM project, you can instruct your bundler to use the `require` HelloWorldFunction: Type: AWS::Serverless::Function Properties: - Runtime: nodejs22.x + Runtime: nodejs24.x Handler: src/index.handler Metadata: BuildMethod: esbuild @@ -140,7 +140,7 @@ The `Tracer` utility is instantiated outside of the Lambda handler. In doing thi HelloWorldFunction: Type: AWS::Serverless::Function Properties: - Runtime: nodejs22.x + Runtime: nodejs24.x Tracing: Active Environment: Variables: diff --git a/docs/getting-started/lambda-layers.md b/docs/getting-started/lambda-layers.md index 0624964ea6..940b7ce0cd 100644 --- a/docs/getting-started/lambda-layers.md +++ b/docs/getting-started/lambda-layers.md @@ -110,7 +110,8 @@ Change `{aws::region}` to your AWS region, e.g. `eu-west-1`, and run the followi "Version": 24, "CompatibleRuntimes": [ "nodejs20.x", - "nodejs22.x" + "nodejs22.x", + "nodejs24.x" ], "LicenseInfo": "MIT-0", "CompatibleArchitectures": [ @@ -142,7 +143,7 @@ Change `{aws::region}` to your AWS region, e.g. `eu-west-1`, and run the followi ); new NodejsFunction(this, 'Function', { - runtime: Runtime.NODEJS_22_X, + runtime: Runtime.NODEJS_24_X, // Add the Layer to a Lambda function layers: [powertoolsLayer], code: Code.fromInline(`...`), @@ -191,7 +192,7 @@ Change `{aws::region}` to your AWS region, e.g. `eu-west-1`, and run the followi ); new NodejsFunction(this, 'Function', { - runtime: Runtime.NODEJS_22_X, + runtime: Runtime.NODEJS_24_X, // Add the Layer to a Lambda function layers: [powertoolsLayer], code: Code.fromInline(`...`), @@ -281,7 +282,7 @@ Change `{aws::region}` to your AWS region, e.g. `eu-west-1`, and run the followi function_name = "lambda_function_name" role = ... handler = "index.handler" - runtime = "nodejs22.x" + runtime = "nodejs24.x" layers = ["arn:aws:lambda:{aws::region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:40"] source_code_hash = filebase64sha256("lambda_function_payload.zip") } @@ -298,7 +299,7 @@ Change `{aws::region}` to your AWS region, e.g. `eu-west-1`, and run the followi resource "aws_lambda_function" "test_lambda" { ... - runtime = "nodejs22.x" + runtime = "nodejs24.x" layers = [data.aws_ssm_parameter.powertools_version.value] } @@ -323,7 +324,7 @@ Change `{aws::region}` to your AWS region, e.g. `eu-west-1`, and run the followi tracingConfig: { mode: 'Active' }, - runtime: aws.lambda.Runtime.NodeJS22dX, + runtime: aws.lambda.Runtime.NodeJS24dX, handler: 'index.handler', role: role.arn, architectures: ['x86_64'] diff --git a/docs/upgrade.md b/docs/upgrade.md index 5136d8c225..1c2ddd9f81 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -78,7 +78,7 @@ Here is an example using `esbuild` bundler. super(scope, id, props); const handler = new NodejsFunction(this, 'helloWorldFunction', { - runtime: Runtime.NODEJS_22_X, + runtime: Runtime.NODEJS_24_X, handler: 'handler', entry: 'src/index.ts', bundling: { @@ -105,7 +105,7 @@ Here is an example using `esbuild` bundler. HelloWorldFunction: Type: AWS::Serverless::Function Properties: - Runtime: nodejs22.x + Runtime: nodejs24.x Handler: src/index.handler Metadata: BuildMethod: esbuild diff --git a/examples/app/template.yaml b/examples/app/template.yaml index 5f634f372e..958a83f86f 100644 --- a/examples/app/template.yaml +++ b/examples/app/template.yaml @@ -10,7 +10,7 @@ Transform: AWS::Serverless-2016-10-31 # https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-specification-template-anatomy-globals.html Globals: Function: - Runtime: nodejs22.x + Runtime: nodejs24.x Tracing: Active Architectures: - arm64 diff --git a/examples/snippets/batch/templates/sam/dynamodb.yaml b/examples/snippets/batch/templates/sam/dynamodb.yaml index 16164f1ad5..1c42414b79 100644 --- a/examples/snippets/batch/templates/sam/dynamodb.yaml +++ b/examples/snippets/batch/templates/sam/dynamodb.yaml @@ -6,7 +6,7 @@ Globals: Function: Timeout: 5 MemorySize: 256 - Runtime: nodejs22.x + Runtime: nodejs24.x Tracing: Active Environment: Variables: diff --git a/examples/snippets/batch/templates/sam/kinesis.yaml b/examples/snippets/batch/templates/sam/kinesis.yaml index 55db822e90..1184996079 100644 --- a/examples/snippets/batch/templates/sam/kinesis.yaml +++ b/examples/snippets/batch/templates/sam/kinesis.yaml @@ -6,7 +6,7 @@ Globals: Function: Timeout: 5 MemorySize: 256 - Runtime: nodejs22.x + Runtime: nodejs24.x Tracing: Active Environment: Variables: diff --git a/examples/snippets/batch/templates/sam/sqs.yaml b/examples/snippets/batch/templates/sam/sqs.yaml index 484b7c937d..08a0d01ecd 100644 --- a/examples/snippets/batch/templates/sam/sqs.yaml +++ b/examples/snippets/batch/templates/sam/sqs.yaml @@ -6,7 +6,7 @@ Globals: Function: Timeout: 5 MemorySize: 256 - Runtime: nodejs22.x + Runtime: nodejs24.x Tracing: Active Environment: Variables: diff --git a/examples/snippets/event-handler/appsync-events/templates/gettingStarted.yaml b/examples/snippets/event-handler/appsync-events/templates/gettingStarted.yaml index ead3874653..d38161f025 100644 --- a/examples/snippets/event-handler/appsync-events/templates/gettingStarted.yaml +++ b/examples/snippets/event-handler/appsync-events/templates/gettingStarted.yaml @@ -5,7 +5,7 @@ Globals: Function: Timeout: 5 MemorySize: 256 - Runtime: nodejs22.x + Runtime: nodejs24.x Tracing: Active Environment: Variables: diff --git a/examples/snippets/event-handler/appsync-graphql/templates/gettingStartedSam.yaml b/examples/snippets/event-handler/appsync-graphql/templates/gettingStartedSam.yaml index 6d0c0ddee5..1d68a7d270 100644 --- a/examples/snippets/event-handler/appsync-graphql/templates/gettingStartedSam.yaml +++ b/examples/snippets/event-handler/appsync-graphql/templates/gettingStartedSam.yaml @@ -6,7 +6,7 @@ Globals: Function: Timeout: 5 MemorySize: 256 - Runtime: nodejs22.x + Runtime: nodejs24.x Environment: Variables: # Powertools for AWS Lambda (TypeScript) env vars: https://docs.aws.amazon.com/powertools/typescript/latest/environment-variables/ diff --git a/examples/snippets/event-handler/bedrock-agents/templates/gettingStartedCdk.ts b/examples/snippets/event-handler/bedrock-agents/templates/gettingStartedCdk.ts index 192a00472f..d37894c4ac 100644 --- a/examples/snippets/event-handler/bedrock-agents/templates/gettingStartedCdk.ts +++ b/examples/snippets/event-handler/bedrock-agents/templates/gettingStartedCdk.ts @@ -24,7 +24,7 @@ export class BedrockAgentsStack extends Stack { const fn = new NodejsFunction(this, 'AirlineAgentFunction', { functionName: fnName, logGroup, - runtime: Runtime.NODEJS_22_X, + runtime: Runtime.NODEJS_24_X, entry: './src/index.ts', handler: 'handler', bundling: { diff --git a/examples/snippets/event-handler/bedrock-agents/templates/gettingStartedSam.yaml b/examples/snippets/event-handler/bedrock-agents/templates/gettingStartedSam.yaml index aa531aab24..c4b4ea766f 100644 --- a/examples/snippets/event-handler/bedrock-agents/templates/gettingStartedSam.yaml +++ b/examples/snippets/event-handler/bedrock-agents/templates/gettingStartedSam.yaml @@ -5,7 +5,7 @@ Globals: Function: Timeout: 30 MemorySize: 256 - Runtime: nodejs22.x + Runtime: nodejs24.x Resources: HelloWorldFunction: diff --git a/examples/snippets/event-handler/rest/templates/api_gateway.yml b/examples/snippets/event-handler/rest/templates/api_gateway.yml index 056f84d413..c9c84443fe 100644 --- a/examples/snippets/event-handler/rest/templates/api_gateway.yml +++ b/examples/snippets/event-handler/rest/templates/api_gateway.yml @@ -19,7 +19,7 @@ Globals: Function: Timeout: 5 MemorySize: 256 - Runtime: nodejs22.x + Runtime: nodejs24.x Tracing: Active Environment: Variables: diff --git a/examples/snippets/event-handler/rest/templates/lambda_furl.yml b/examples/snippets/event-handler/rest/templates/lambda_furl.yml index 1770b5a278..89044f6599 100644 --- a/examples/snippets/event-handler/rest/templates/lambda_furl.yml +++ b/examples/snippets/event-handler/rest/templates/lambda_furl.yml @@ -6,7 +6,7 @@ Globals: Function: Timeout: 5 MemorySize: 256 - Runtime: nodejs22.x + Runtime: nodejs24.x Tracing: Active Environment: Variables: diff --git a/examples/snippets/idempotency/templates/cacheCdk.ts b/examples/snippets/idempotency/templates/cacheCdk.ts index f1a6cbdf59..364fcbb805 100644 --- a/examples/snippets/idempotency/templates/cacheCdk.ts +++ b/examples/snippets/idempotency/templates/cacheCdk.ts @@ -54,7 +54,7 @@ export class ValkeyStack extends Stack { const valkeyLayer = new LayerVersion(this, 'ValkeyLayer', { removalPolicy: RemovalPolicy.DESTROY, compatibleArchitectures: [Architecture.ARM_64], - compatibleRuntimes: [Runtime.NODEJS_22_X], + compatibleRuntimes: [Runtime.NODEJS_22_X, Runtime.NODEJS_24_X], code: Code.fromAsset('./lib/layers/valkey-glide'), }); @@ -67,7 +67,7 @@ export class ValkeyStack extends Stack { const fn = new NodejsFunction(this, 'MyFunction', { functionName: fnName, logGroup, - runtime: Runtime.NODEJS_22_X, + runtime: Runtime.NODEJS_24_X, architecture: Architecture.ARM_64, memorySize: 512, timeout: Duration.seconds(30), diff --git a/examples/snippets/idempotency/templates/cacheSam.yml b/examples/snippets/idempotency/templates/cacheSam.yml index 4541651907..beaedfeb94 100644 --- a/examples/snippets/idempotency/templates/cacheSam.yml +++ b/examples/snippets/idempotency/templates/cacheSam.yml @@ -16,7 +16,7 @@ Resources: HelloWorldFunction: Type: AWS::Serverless::Function Properties: - Runtime: nodejs22.x + Runtime: nodejs24.x Handler: index.js VpcConfig: # (3)! SecurityGroupIds: diff --git a/examples/snippets/idempotency/templates/tableSam.yaml b/examples/snippets/idempotency/templates/tableSam.yaml index 38f1a4a06d..098823afc4 100644 --- a/examples/snippets/idempotency/templates/tableSam.yaml +++ b/examples/snippets/idempotency/templates/tableSam.yaml @@ -17,7 +17,7 @@ Resources: HelloWorldFunction: Type: AWS::Serverless::Function Properties: - Runtime: nodejs22.x + Runtime: nodejs24.x Handler: index.js Policies: - Statement: diff --git a/examples/snippets/kafka/templates/advancedBatchSizeConfiguration.yaml b/examples/snippets/kafka/templates/advancedBatchSizeConfiguration.yaml index 02a399b7dd..75634ef96b 100644 --- a/examples/snippets/kafka/templates/advancedBatchSizeConfiguration.yaml +++ b/examples/snippets/kafka/templates/advancedBatchSizeConfiguration.yaml @@ -2,7 +2,7 @@ Resources: OrderProcessingFunction: Type: AWS::Serverless::Function Properties: - Runtime: nodejs22.x + Runtime: nodejs24.x Handler: index.js Events: KafkaEvent: diff --git a/examples/snippets/kafka/templates/gettingStartedWithMsk.yaml b/examples/snippets/kafka/templates/gettingStartedWithMsk.yaml index 3a22c9328a..602912de60 100644 --- a/examples/snippets/kafka/templates/gettingStartedWithMsk.yaml +++ b/examples/snippets/kafka/templates/gettingStartedWithMsk.yaml @@ -4,7 +4,7 @@ Resources: KafkaConsumerFunction: Type: AWS::Serverless::Function Properties: - Runtime: nodejs22.x + Runtime: nodejs24.x Handler: index.js Timeout: 30 Events: diff --git a/layers/package.json b/layers/package.json index c22a3a7939..821f9e6f6f 100644 --- a/layers/package.json +++ b/layers/package.json @@ -14,6 +14,7 @@ "test:unit:types": "echo 'Not Implemented'", "test:e2e:nodejs20x": "echo 'Not Implemented'", "test:e2e:nodejs22x": "echo 'Not Implemented'", + "test:e2e:nodejs24x": "echo 'Not Implemented'", "test:e2e": "vitest --run tests/e2e", "build": "echo 'Not applicable, run `npx cdk synth` instead to build the stack'", "cdk": "cdk", diff --git a/layers/src/layer-publisher-stack.ts b/layers/src/layer-publisher-stack.ts index 7f35789f98..776f6d9dac 100644 --- a/layers/src/layer-publisher-stack.ts +++ b/layers/src/layer-publisher-stack.ts @@ -41,13 +41,17 @@ export class LayerPublisherStack extends Stack { this.lambdaLayerVersion = new LayerVersion(this, 'LambdaPowertoolsLayer', { layerVersionName: props?.layerName, description: `Powertools for AWS Lambda (TypeScript) version ${powertoolsPackageVersion}`, - compatibleRuntimes: [Runtime.NODEJS_20_X, Runtime.NODEJS_22_X], + compatibleRuntimes: [ + Runtime.NODEJS_20_X, + Runtime.NODEJS_22_X, + Runtime.NODEJS_24_X, + ], license: 'MIT-0', compatibleArchitectures: [Architecture.ARM_64, Architecture.X86_64], code: Code.fromAsset(resolve(__dirname), { bundling: { // This is here only because is required by CDK, however it is not used since the bundling is done locally - image: Runtime.NODEJS_22_X.bundlingImage, + image: Runtime.NODEJS_24_X.bundlingImage, // We need to run a command to generate a random UUID to force the bundling to run every time command: [`echo "${randomUUID()}"`], local: { diff --git a/layers/tests/unit/layer-publisher.test.ts b/layers/tests/unit/layer-publisher.test.ts index e45a16db65..7a8cadb4c4 100644 --- a/layers/tests/unit/layer-publisher.test.ts +++ b/layers/tests/unit/layer-publisher.test.ts @@ -20,7 +20,7 @@ describe('Class: LayerPublisherStack', () => { // Assess template.resourceCountIs('AWS::Lambda::LayerVersion', 1); template.hasResourceProperties('AWS::Lambda::LayerVersion', { - CompatibleRuntimes: ['nodejs20.x', 'nodejs22.x'], + CompatibleRuntimes: ['nodejs20.x', 'nodejs22.x', 'nodejs24.x'], LicenseInfo: 'MIT-0', /* CompatibleArchitectures: [ 'x86_64', diff --git a/package-lock.json b/package-lock.json index b4499de7b3..db0f529448 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11490,7 +11490,7 @@ "license": "MIT-0", "dependencies": { "@aws-lambda-powertools/commons": "2.28.1", - "@aws/lambda-invoke-store": "0.1.1", + "@aws/lambda-invoke-store": "0.2.1", "@standard-schema/spec": "^1.0.0" }, "devDependencies": { @@ -11500,9 +11500,9 @@ } }, "packages/batch/node_modules/@aws/lambda-invoke-store": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.1.1.tgz", - "integrity": "sha512-RcLam17LdlbSOSp9VxmUu1eI6Mwxp+OwhD2QhiSNmNCzoDb0EeUXTD2n/WbcnrAYMGlmf05th6QYq23VqvJqpA==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.1.tgz", + "integrity": "sha512-sIyFcoPZkTtNu9xFeEoynMef3bPJIAbOfUh+ueYcfhVl6xm2VRtMcMclSxmZCMnHHd4hlYKJeq/aggmBEWynww==", "license": "Apache-2.0", "engines": { "node": ">=18.0.0" @@ -11513,16 +11513,16 @@ "version": "2.28.1", "license": "MIT-0", "dependencies": { - "@aws/lambda-invoke-store": "0.1.1" + "@aws/lambda-invoke-store": "0.2.1" }, "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing" } }, "packages/commons/node_modules/@aws/lambda-invoke-store": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.1.1.tgz", - "integrity": "sha512-RcLam17LdlbSOSp9VxmUu1eI6Mwxp+OwhD2QhiSNmNCzoDb0EeUXTD2n/WbcnrAYMGlmf05th6QYq23VqvJqpA==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.1.tgz", + "integrity": "sha512-sIyFcoPZkTtNu9xFeEoynMef3bPJIAbOfUh+ueYcfhVl6xm2VRtMcMclSxmZCMnHHd4hlYKJeq/aggmBEWynww==", "license": "Apache-2.0", "engines": { "node": ">=18.0.0" @@ -11622,7 +11622,7 @@ "license": "MIT-0", "dependencies": { "@aws-lambda-powertools/commons": "2.28.1", - "@aws/lambda-invoke-store": "0.1.1", + "@aws/lambda-invoke-store": "0.2.1", "lodash.merge": "^4.6.2" }, "devDependencies": { @@ -11643,9 +11643,9 @@ } }, "packages/logger/node_modules/@aws/lambda-invoke-store": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.1.1.tgz", - "integrity": "sha512-RcLam17LdlbSOSp9VxmUu1eI6Mwxp+OwhD2QhiSNmNCzoDb0EeUXTD2n/WbcnrAYMGlmf05th6QYq23VqvJqpA==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.1.tgz", + "integrity": "sha512-sIyFcoPZkTtNu9xFeEoynMef3bPJIAbOfUh+ueYcfhVl6xm2VRtMcMclSxmZCMnHHd4hlYKJeq/aggmBEWynww==", "license": "Apache-2.0", "engines": { "node": ">=18.0.0" @@ -11656,12 +11656,12 @@ "version": "2.28.1", "license": "MIT-0", "dependencies": { - "@aws-lambda-powertools/commons": "2.28.1", - "@aws/lambda-invoke-store": "0.1.1" + "@aws-lambda-powertools/commons": "2.28.1" }, "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing", "@aws-sdk/client-cloudwatch": "^3.932.0", + "@aws/lambda-invoke-store": "0.2.1", "@types/promise-retry": "^1.1.3", "promise-retry": "^2.0.1" }, @@ -11675,9 +11675,10 @@ } }, "packages/metrics/node_modules/@aws/lambda-invoke-store": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.1.1.tgz", - "integrity": "sha512-RcLam17LdlbSOSp9VxmUu1eI6Mwxp+OwhD2QhiSNmNCzoDb0EeUXTD2n/WbcnrAYMGlmf05th6QYq23VqvJqpA==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.1.tgz", + "integrity": "sha512-sIyFcoPZkTtNu9xFeEoynMef3bPJIAbOfUh+ueYcfhVl6xm2VRtMcMclSxmZCMnHHd4hlYKJeq/aggmBEWynww==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18.0.0" @@ -11760,6 +11761,7 @@ "dependencies": { "@aws-cdk/toolkit-lib": "^1.10.4", "@aws-sdk/client-lambda": "^3.932.0", + "@aws/lambda-invoke-store": "0.2.1", "@smithy/util-utf8": "^4.0.0", "aws-cdk-lib": "^2.225.0", "esbuild": "^0.27.0", @@ -11770,6 +11772,15 @@ "aws-sdk-client-mock-vitest": "^7.0.1" } }, + "packages/testing/node_modules/@aws/lambda-invoke-store": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.1.tgz", + "integrity": "sha512-sIyFcoPZkTtNu9xFeEoynMef3bPJIAbOfUh+ueYcfhVl6xm2VRtMcMclSxmZCMnHHd4hlYKJeq/aggmBEWynww==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "packages/tracer": { "name": "@aws-lambda-powertools/tracer", "version": "2.28.1", diff --git a/packages/batch/package.json b/packages/batch/package.json index 743fc81122..73e8961619 100644 --- a/packages/batch/package.json +++ b/packages/batch/package.json @@ -16,6 +16,7 @@ "test:unit:types": "echo 'Not Implemented'", "test:e2e:nodejs20x": "echo 'Not Implemented'", "test:e2e:nodejs22x": "echo 'Not Implemented'", + "test:e2e:nodejs24x": "echo 'Not Implemented'", "test:e2e": "echo 'Not Implemented'", "build:cjs": "tsc --build tsconfig.cjs.json && echo '{ \"type\": \"commonjs\" }' > lib/cjs/package.json", "build:esm": "tsc --build tsconfig.json && echo '{ \"type\": \"module\" }' > lib/esm/package.json", @@ -82,8 +83,8 @@ "nodejs" ], "dependencies": { + "@aws/lambda-invoke-store": "0.2.1", "@aws-lambda-powertools/commons": "2.28.1", - "@aws/lambda-invoke-store": "0.1.1", "@standard-schema/spec": "^1.0.0" }, "devDependencies": { diff --git a/packages/batch/src/BatchProcessingStore.ts b/packages/batch/src/BatchProcessingStore.ts index d63aa6c3c7..6081661f54 100644 --- a/packages/batch/src/BatchProcessingStore.ts +++ b/packages/batch/src/BatchProcessingStore.ts @@ -1,4 +1,5 @@ -import { InvokeStore } from '@aws/lambda-invoke-store'; +import '@aws/lambda-invoke-store'; +import { shouldUseInvokeStore } from '@aws-lambda-powertools/commons/utils/env'; import type { BaseRecord, BatchProcessingOptions, @@ -35,124 +36,152 @@ class BatchProcessingStore { #fallbackErrors: Error[] = []; public getRecords(): BaseRecord[] { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { return this.#fallbackRecords; } - return (InvokeStore.get(this.#recordsKey) as BaseRecord[]) ?? []; + + if (globalThis.awslambda?.InvokeStore === undefined) { + throw new Error('InvokeStore is not available'); + } + + const store = globalThis.awslambda.InvokeStore; + return (store.get(this.#recordsKey) as BaseRecord[]) ?? []; } public setRecords(records: BaseRecord[]): void { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { this.#fallbackRecords = records; return; } - InvokeStore.set(this.#recordsKey, records); + + if (globalThis.awslambda?.InvokeStore === undefined) { + throw new Error('InvokeStore is not available'); + } + + const store = globalThis.awslambda.InvokeStore; + store.set(this.#recordsKey, records); } public getHandler(): CallableFunction { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { return this.#fallbackHandler; } + return ( - (InvokeStore.get(this.#handlerKey) as CallableFunction) ?? (() => {}) + (globalThis.awslambda?.InvokeStore?.get( + this.#handlerKey + ) as CallableFunction) ?? (() => {}) ); } public setHandler(handler: CallableFunction): void { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { this.#fallbackHandler = handler; return; } - InvokeStore.set(this.#handlerKey, handler); + + globalThis.awslambda?.InvokeStore?.set(this.#handlerKey, handler); } public getOptions(): BatchProcessingOptions | undefined { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { return this.#fallbackOptions; } - return InvokeStore.get(this.#optionsKey) as + + return globalThis.awslambda?.InvokeStore?.get(this.#optionsKey) as | BatchProcessingOptions | undefined; } public setOptions(options: BatchProcessingOptions | undefined): void { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { this.#fallbackOptions = options; return; } - InvokeStore.set(this.#optionsKey, options); + + globalThis.awslambda?.InvokeStore?.set(this.#optionsKey, options); } public getFailureMessages(): EventSourceDataClassTypes[] { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { return this.#fallbackFailureMessages; } + return ( - (InvokeStore.get( + (globalThis.awslambda?.InvokeStore?.get( this.#failureMessagesKey ) as EventSourceDataClassTypes[]) ?? [] ); } public setFailureMessages(messages: EventSourceDataClassTypes[]): void { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { this.#fallbackFailureMessages = messages; return; } - InvokeStore.set(this.#failureMessagesKey, messages); + + globalThis.awslambda?.InvokeStore?.set(this.#failureMessagesKey, messages); } public getSuccessMessages(): EventSourceDataClassTypes[] { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { return this.#fallbackSuccessMessages; } + return ( - (InvokeStore.get( + (globalThis.awslambda?.InvokeStore?.get( this.#successMessagesKey ) as EventSourceDataClassTypes[]) ?? [] ); } public setSuccessMessages(messages: EventSourceDataClassTypes[]): void { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { this.#fallbackSuccessMessages = messages; return; } - InvokeStore.set(this.#successMessagesKey, messages); + + globalThis.awslambda?.InvokeStore?.set(this.#successMessagesKey, messages); } public getBatchResponse(): PartialItemFailureResponse { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { return this.#fallbackBatchResponse; } + return ( - (InvokeStore.get( + (globalThis.awslambda?.InvokeStore?.get( this.#batchResponseKey ) as PartialItemFailureResponse) ?? { batchItemFailures: [] } ); } public setBatchResponse(response: PartialItemFailureResponse): void { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { this.#fallbackBatchResponse = response; return; } - InvokeStore.set(this.#batchResponseKey, response); + + globalThis.awslambda?.InvokeStore?.set(this.#batchResponseKey, response); } public getErrors(): Error[] { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { return this.#fallbackErrors; } - return (InvokeStore.get(this.#errorsKey) as Error[]) ?? []; + + return ( + (globalThis.awslambda?.InvokeStore?.get(this.#errorsKey) as Error[]) ?? [] + ); } public setErrors(errors: Error[]): void { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { this.#fallbackErrors = errors; return; } - InvokeStore.set(this.#errorsKey, errors); + + globalThis.awslambda?.InvokeStore?.set(this.#errorsKey, errors); } } diff --git a/packages/batch/src/SqsFifoProcessorStore.ts b/packages/batch/src/SqsFifoProcessorStore.ts index 6763329f9f..c56e42c5ff 100644 --- a/packages/batch/src/SqsFifoProcessorStore.ts +++ b/packages/batch/src/SqsFifoProcessorStore.ts @@ -1,4 +1,5 @@ -import { InvokeStore } from '@aws/lambda-invoke-store'; +import '@aws/lambda-invoke-store'; +import { shouldUseInvokeStore } from '@aws-lambda-powertools/commons/utils/env'; /** * Manages storage of SQS FIFO processor state with automatic context detection. @@ -21,20 +22,30 @@ class SqsFifoProcessorStore { #fallbackFailedGroupIds = new Set(); public getCurrentGroupId(): string | undefined { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { return this.#fallbackCurrentGroupId; } - return InvokeStore.get(this.#currentGroupIdKey) as string | undefined; + if (globalThis.awslambda?.InvokeStore === undefined) { + throw new Error('InvokeStore is not available'); + } + + const store = globalThis.awslambda.InvokeStore; + return store.get(this.#currentGroupIdKey) as string | undefined; } public setCurrentGroupId(groupId: string | undefined): void { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { this.#fallbackCurrentGroupId = groupId; return; } - InvokeStore.set(this.#currentGroupIdKey, groupId); + if (globalThis.awslambda?.InvokeStore === undefined) { + throw new Error('InvokeStore is not available'); + } + + const store = globalThis.awslambda.InvokeStore; + store.set(this.#currentGroupIdKey, groupId); } public addFailedGroupId(groupId: string): void { @@ -46,28 +57,38 @@ class SqsFifoProcessorStore { } public getFailedGroupIds(): Set { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { return this.#fallbackFailedGroupIds; } - let failedGroupIds = InvokeStore.get(this.#failedGroupIdsKey) as + if (globalThis.awslambda?.InvokeStore === undefined) { + throw new Error('InvokeStore is not available'); + } + + const store = globalThis.awslambda.InvokeStore; + let failedGroupIds = store.get(this.#failedGroupIdsKey) as | Set | undefined; if (failedGroupIds == null) { failedGroupIds = new Set(); - InvokeStore.set(this.#failedGroupIdsKey, failedGroupIds); + store.set(this.#failedGroupIdsKey, failedGroupIds); } return failedGroupIds; } public clearFailedGroupIds(): void { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { this.#fallbackFailedGroupIds = new Set(); return; } - InvokeStore.set(this.#failedGroupIdsKey, new Set()); + if (globalThis.awslambda?.InvokeStore === undefined) { + throw new Error('InvokeStore is not available'); + } + + const store = globalThis.awslambda.InvokeStore; + store.set(this.#failedGroupIdsKey, new Set()); } } diff --git a/packages/batch/src/types.ts b/packages/batch/src/types.ts index 8cd4fa4575..35e073ed62 100644 --- a/packages/batch/src/types.ts +++ b/packages/batch/src/types.ts @@ -1,3 +1,4 @@ +import type { InvokeStoreBase } from '@aws/lambda-invoke-store'; import type { GenericLogger } from '@aws-lambda-powertools/commons/types'; import type { StandardSchemaV1 } from '@standard-schema/spec'; import type { @@ -13,6 +14,12 @@ import type { parser } from './parser.js'; import type { SqsFifoPartialProcessor } from './SqsFifoPartialProcessor.js'; import type { SqsFifoPartialProcessorAsync } from './SqsFifoPartialProcessorAsync.js'; +declare global { + namespace awslambda { + let InvokeStore: InvokeStoreBase | undefined; + } +} + /** * Options for batch processing * diff --git a/packages/batch/tests/unit/concurrency/BatchProcessingStore.test.ts b/packages/batch/tests/unit/concurrency/BatchProcessingStore.test.ts index eb8d3d1d1e..34e2363a44 100644 --- a/packages/batch/tests/unit/concurrency/BatchProcessingStore.test.ts +++ b/packages/batch/tests/unit/concurrency/BatchProcessingStore.test.ts @@ -1,6 +1,6 @@ import { sequence } from '@aws-lambda-powertools/testing-utils'; import type { SQSRecord } from 'aws-lambda'; -import { beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { BatchProcessingStore } from '../../../src/BatchProcessingStore.js'; import { sqsRecordFactory } from '../../helpers/factories.js'; @@ -9,6 +9,10 @@ describe('BatchProcessingStore concurrent invocation isolation', () => { // No mocks needed }); + afterEach(() => { + vi.unstubAllEnvs(); + }); + it.each([ { description: 'without InvokeStore', @@ -22,6 +26,9 @@ describe('BatchProcessingStore concurrent invocation isolation', () => { 'returns empty defaults when not initialized $description', async ({ useInvokeStore }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const store = new BatchProcessingStore(); // Act @@ -84,6 +91,9 @@ describe('BatchProcessingStore concurrent invocation isolation', () => { 'isolates records per invocation $description', async ({ useInvokeStore, expectedResultA, expectedResultB }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const store = new BatchProcessingStore(); const recordsA = [sqsRecordFactory('record-A')]; const recordsB = [sqsRecordFactory('record-B')]; @@ -134,6 +144,9 @@ describe('BatchProcessingStore concurrent invocation isolation', () => { 'isolates failure messages per invocation $description', async ({ useInvokeStore, expectedResultA, expectedResultB }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const store = new BatchProcessingStore(); const recordA = sqsRecordFactory('fail-A'); const recordB = sqsRecordFactory('fail-B'); @@ -186,6 +199,9 @@ describe('BatchProcessingStore concurrent invocation isolation', () => { 'isolates errors per invocation $description', async ({ useInvokeStore, expectedResultA, expectedResultB }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const store = new BatchProcessingStore(); const errorA = new Error('error-A'); const errorB = new Error('error-B'); @@ -218,4 +234,36 @@ describe('BatchProcessingStore concurrent invocation isolation', () => { expect(resultB).toEqual(expectedResultB); } ); + + describe('InvokeStore error handling', () => { + beforeEach(() => { + vi.stubGlobal('awslambda', undefined); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('throws error when getRecords is called with AWS_LAMBDA_MAX_CONCURRENCY set but InvokeStore is not available', () => { + // Prepare + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + const store = new BatchProcessingStore(); + + // Act & Assess + expect(() => { + store.getRecords(); + }).toThrow('InvokeStore is not available'); + }); + + it('throws error when setRecords is called with AWS_LAMBDA_MAX_CONCURRENCY set but InvokeStore is not available', () => { + // Prepare + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + const store = new BatchProcessingStore(); + + // Act & Assess + expect(() => { + store.setRecords([]); + }).toThrow('InvokeStore is not available'); + }); + }); }); diff --git a/packages/batch/tests/unit/concurrency/BatchProcessor.test.ts b/packages/batch/tests/unit/concurrency/BatchProcessor.test.ts index 23edc23f28..279c1c9814 100644 --- a/packages/batch/tests/unit/concurrency/BatchProcessor.test.ts +++ b/packages/batch/tests/unit/concurrency/BatchProcessor.test.ts @@ -1,6 +1,6 @@ import { sequence } from '@aws-lambda-powertools/testing-utils'; import type { SQSRecord } from 'aws-lambda'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { BatchProcessor, EventType } from '../../../src/index.js'; import { sqsRecordFactory } from '../../helpers/factories.js'; @@ -9,6 +9,10 @@ describe('BatchProcessor concurrent invocation isolation', () => { vi.clearAllMocks(); }); + afterEach(() => { + vi.unstubAllEnvs(); + }); + it.each([ { description: 'without InvokeStore', @@ -24,6 +28,9 @@ describe('BatchProcessor concurrent invocation isolation', () => { 'processes correct records per invocation $description', async ({ useInvokeStore, expectedResults }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const processor = new BatchProcessor(EventType.SQS); const recordsA = [sqsRecordFactory('record-A')]; const recordsB = [sqsRecordFactory('record-B')]; @@ -86,6 +93,9 @@ describe('BatchProcessor concurrent invocation isolation', () => { 'calls correct handler per invocation $description', async ({ useInvokeStore, expectedCalls }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const processor = new BatchProcessor(EventType.SQS); const recordsA = [sqsRecordFactory('record-A')]; const recordsB = [sqsRecordFactory('record-B')]; @@ -138,6 +148,9 @@ describe('BatchProcessor concurrent invocation isolation', () => { 'tracks failures independently per invocation $description', async ({ useInvokeStore }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const processor = new BatchProcessor(EventType.SQS); const recordsA = [sqsRecordFactory('fail')]; const recordsB = [sqsRecordFactory('success')]; @@ -208,6 +221,9 @@ describe('BatchProcessor concurrent invocation isolation', () => { 'isolates use of prepare method across invocations $description', async ({ useInvokeStore, expectedErrorCountA, expectedErrorCountB }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const processor = new BatchProcessor(EventType.SQS); const recordsA = [sqsRecordFactory('fail-1'), sqsRecordFactory('fail-2')]; const recordsB = [sqsRecordFactory('fail-3')]; diff --git a/packages/batch/tests/unit/concurrency/SqsFifoPartialProcessor.test.ts b/packages/batch/tests/unit/concurrency/SqsFifoPartialProcessor.test.ts index f9e22f5a6f..3708491772 100644 --- a/packages/batch/tests/unit/concurrency/SqsFifoPartialProcessor.test.ts +++ b/packages/batch/tests/unit/concurrency/SqsFifoPartialProcessor.test.ts @@ -1,6 +1,6 @@ import { sequence } from '@aws-lambda-powertools/testing-utils'; import type { SQSRecord } from 'aws-lambda'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { SqsFifoPartialProcessor, SqsFifoPartialProcessorAsync, @@ -47,6 +47,10 @@ describe('SQS FIFO Processors concurrent invocation isolation', () => { vi.clearAllMocks(); }); + afterEach(() => { + vi.unstubAllEnvs(); + }); + for (const { name, processorClass, isAsync } of processors) { describe(`${name}`, () => { it.each([ @@ -66,6 +70,9 @@ describe('SQS FIFO Processors concurrent invocation isolation', () => { 'processes correct records per invocation $description', async ({ useInvokeStore, expectedBodyA, expectedBodyB }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const processor = new processorClass(); const recordsA = [sqsRecordFactory('record-A', '1')]; const recordsB = [sqsRecordFactory('record-B', '2')]; @@ -139,6 +146,9 @@ describe('SQS FIFO Processors concurrent invocation isolation', () => { 'calls correct handler per invocation $description', async ({ useInvokeStore, expectedCalls }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const processor = new processorClass(); const recordsA = [sqsRecordFactory('record-A', '1')]; const recordsB = [sqsRecordFactory('record-B', '2')]; @@ -213,6 +223,9 @@ describe('SQS FIFO Processors concurrent invocation isolation', () => { expectedLengthBSync, }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const processor = new processorClass(); const recordsA = [sqsRecordFactory('body-A-2', '1')]; const recordsB = [ @@ -291,6 +304,9 @@ describe('SQS FIFO Processors concurrent invocation isolation', () => { 'skips failed group but processes other groups independently $description', async ({ useInvokeStore, expectedLengthA, expectedLengthB }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const processor = new processorClass(); const recordsA = [ sqsRecordFactory('fail', '1'), diff --git a/packages/batch/tests/unit/concurrency/SqsFifoProcessorStore.test.ts b/packages/batch/tests/unit/concurrency/SqsFifoProcessorStore.test.ts index 04e9e77435..afc0e55284 100644 --- a/packages/batch/tests/unit/concurrency/SqsFifoProcessorStore.test.ts +++ b/packages/batch/tests/unit/concurrency/SqsFifoProcessorStore.test.ts @@ -1,10 +1,68 @@ import { sequence } from '@aws-lambda-powertools/testing-utils'; -import { beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { SqsFifoProcessorStore } from '../../../src/SqsFifoProcessorStore.js'; describe('SqsFifoProcessorStore concurrent invocation isolation', () => { beforeEach(() => { - // No mocks needed + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + describe('InvokeStore error handling', () => { + beforeEach(() => { + vi.stubGlobal('awslambda', undefined); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('throws error when AWS_LAMBDA_MAX_CONCURRENCY is set but InvokeStore is not available', () => { + // Prepare + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + const store = new SqsFifoProcessorStore(); + + // Act & Assess + expect(() => { + store.addFailedGroupId('group-A'); + }).toThrow('InvokeStore is not available'); + }); + + it('throws error when getting current group id with InvokeStore unavailable', () => { + // Prepare + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + const store = new SqsFifoProcessorStore(); + + // Act & Assess + expect(() => { + store.getCurrentGroupId(); + }).toThrow('InvokeStore is not available'); + }); + + it('throws error when setting current group id with InvokeStore unavailable', () => { + // Prepare + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + const store = new SqsFifoProcessorStore(); + + // Act & Assess + expect(() => { + store.setCurrentGroupId('group-A'); + }).toThrow('InvokeStore is not available'); + }); + + it('throws error when clearing failed group ids with InvokeStore unavailable', () => { + // Prepare + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + const store = new SqsFifoProcessorStore(); + + // Act & Assess + expect(() => { + store.clearFailedGroupIds(); + }).toThrow('InvokeStore is not available'); + }); }); it.each([ @@ -24,6 +82,9 @@ describe('SqsFifoProcessorStore concurrent invocation isolation', () => { 'lazily initializes failedGroupIds independently $description', async ({ useInvokeStore, expectedResultA, expectedResultB }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const store = new SqsFifoProcessorStore(); // Act @@ -72,6 +133,9 @@ describe('SqsFifoProcessorStore concurrent invocation isolation', () => { 'isolates currentGroupId per invocation $description', async ({ useInvokeStore, expectedResultA, expectedResultB }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const store = new SqsFifoProcessorStore(); // Act @@ -120,6 +184,9 @@ describe('SqsFifoProcessorStore concurrent invocation isolation', () => { 'clears failedGroupIds independently per invocation $description', async ({ useInvokeStore, expectedResultA, expectedResultB }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const store = new SqsFifoProcessorStore(); // Act diff --git a/packages/commons/package.json b/packages/commons/package.json index 5976e69284..4bfdd4fad5 100644 --- a/packages/commons/package.json +++ b/packages/commons/package.json @@ -111,7 +111,7 @@ "nodejs" ], "dependencies": { - "@aws/lambda-invoke-store": "0.1.1" + "@aws/lambda-invoke-store": "0.2.1" }, "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing" diff --git a/packages/commons/src/constants.ts b/packages/commons/src/constants.ts index 6a7a3eb7bf..06c709a2cc 100644 --- a/packages/commons/src/constants.ts +++ b/packages/commons/src/constants.ts @@ -1,8 +1,10 @@ +const AWS_LAMBDA_MAX_CONCURRENCY = 'AWS_LAMBDA_MAX_CONCURRENCY' as const; const POWERTOOLS_DEV_ENV_VAR = 'POWERTOOLS_DEV' as const; const POWERTOOLS_SERVICE_NAME_ENV_VAR = 'POWERTOOLS_SERVICE_NAME' as const; const XRAY_TRACE_ID_ENV_VAR = '_X_AMZN_TRACE_ID' as const; export { + AWS_LAMBDA_MAX_CONCURRENCY, POWERTOOLS_DEV_ENV_VAR, POWERTOOLS_SERVICE_NAME_ENV_VAR, XRAY_TRACE_ID_ENV_VAR, diff --git a/packages/commons/src/envUtils.ts b/packages/commons/src/envUtils.ts index 751d109bfe..2a20d0ee56 100644 --- a/packages/commons/src/envUtils.ts +++ b/packages/commons/src/envUtils.ts @@ -1,5 +1,6 @@ -import { InvokeStore } from '@aws/lambda-invoke-store'; +import '@aws/lambda-invoke-store'; import { + AWS_LAMBDA_MAX_CONCURRENCY, POWERTOOLS_DEV_ENV_VAR, POWERTOOLS_SERVICE_NAME_ENV_VAR, XRAY_TRACE_ID_ENV_VAR, @@ -258,7 +259,7 @@ const getServiceName = (): string => { */ const getXrayTraceDataFromEnv = (): Record | undefined => { const xRayTraceEnv = - InvokeStore.getXRayTraceId() ?? + globalThis.awslambda?.InvokeStore?.getXRayTraceId() ?? getStringFromEnv({ key: XRAY_TRACE_ID_ENV_VAR, defaultValue: '', @@ -295,6 +296,14 @@ const isRequestXRaySampled = (): boolean => { return xRayTraceData?.Sampled === '1'; }; +const shouldUseInvokeStore = (): boolean => { + const res = getStringFromEnv({ + key: AWS_LAMBDA_MAX_CONCURRENCY, + defaultValue: '', + }); + return res !== ''; +}; + /** * AWS X-Ray Trace id from the lambda RIC async context or the `_X_AMZN_TRACE_ID` environment variable. * @@ -319,4 +328,5 @@ export { getXrayTraceDataFromEnv, isRequestXRaySampled, getXRayTraceIdFromEnv, + shouldUseInvokeStore, }; diff --git a/packages/commons/tests/unit/envUtils.test.ts b/packages/commons/tests/unit/envUtils.test.ts index 74f7df741d..75b2a43297 100644 --- a/packages/commons/tests/unit/envUtils.test.ts +++ b/packages/commons/tests/unit/envUtils.test.ts @@ -1,4 +1,4 @@ -import { InvokeStore } from '@aws/lambda-invoke-store'; +import { InvokeStore, InvokeStoreBase } from '@aws/lambda-invoke-store'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { getBooleanFromEnv, @@ -8,6 +8,7 @@ import { getXRayTraceIdFromEnv, isDevMode, isRequestXRaySampled, + shouldUseInvokeStore, } from '../../src/envUtils.js'; describe('Functions: envUtils', () => { @@ -289,10 +290,11 @@ describe('Functions: envUtils', () => { 'Root=1-6849f099-ce973f4ea2c57e4f9a382904;Parent=668bfc7d9aa5b120;Sampled=0', expected: '1-6849f099-ce973f4ea2c57e4f9a382904', }, - ])('$description', ({ traceData, expected }) => { - InvokeStore.run( + ])('$description', async ({ traceData, expected }) => { + const invokeStore = await InvokeStore.getInstanceAsync(); + invokeStore.run( { - [InvokeStore.PROTECTED_KEYS.X_RAY_TRACE_ID]: traceData, + [InvokeStoreBase.PROTECTED_KEYS.X_RAY_TRACE_ID]: traceData, }, () => { // Act @@ -367,10 +369,11 @@ describe('Functions: envUtils', () => { traceData: undefined, expected: false, }, - ])('$description', ({ traceData, expected }) => { - InvokeStore.run( + ])('$description', async ({ traceData, expected }) => { + const invokeStore = await InvokeStore.getInstanceAsync(); + invokeStore.run( { - [InvokeStore.PROTECTED_KEYS.X_RAY_TRACE_ID]: traceData, + [InvokeStoreBase.PROTECTED_KEYS.X_RAY_TRACE_ID]: traceData, }, () => { // Act @@ -382,4 +385,25 @@ describe('Functions: envUtils', () => { ); }); }); + + describe('Function: shouldUseInvokeStore', () => { + it('returns true when AWS_LAMBDA_MAX_CONCURRENCY is not set', () => { + // Act + const result = shouldUseInvokeStore(); + + // Assess + expect(result).toBe(false); + }); + + it('returns false when AWS_LAMBDA_MAX_CONCURRENCY is set', () => { + // Prepare + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + + // Act + const result = shouldUseInvokeStore(); + + // Assess + expect(result).toBe(true); + }); + }); }); diff --git a/packages/event-handler/package.json b/packages/event-handler/package.json index 05af637466..599d25ffc4 100644 --- a/packages/event-handler/package.json +++ b/packages/event-handler/package.json @@ -16,7 +16,8 @@ "test:unit:types": "echo 'Not Implemented'", "test:e2e:nodejs20x": "RUNTIME=nodejs20x vitest run tests/e2e", "test:e2e:nodejs22x": "RUNTIME=nodejs22x vitest run tests/e2e", - "test:e2e": "npm run test:e2e:nodejs20x", + "test:e2e:nodejs24x": "RUNTIME=nodejs24x vitest run tests/e2e", + "test:e2e": "vitest run tests/e2e", "build:cjs": "tsc --build tsconfig.cjs.json && echo '{ \"type\": \"commonjs\" }' > lib/cjs/package.json", "build:esm": "tsc --build tsconfig.json && echo '{ \"type\": \"module\" }' > lib/esm/package.json", "build": "npm run build:esm & npm run build:cjs", diff --git a/packages/idempotency/package.json b/packages/idempotency/package.json index ddbc0ea2cc..ad4abe876b 100644 --- a/packages/idempotency/package.json +++ b/packages/idempotency/package.json @@ -17,6 +17,7 @@ "test:unit:watch": "vitest tests/unit", "test:e2e:nodejs20x": "RUNTIME=nodejs20x vitest --run tests/e2e", "test:e2e:nodejs22x": "RUNTIME=nodejs22x vitest --run tests/e2e", + "test:e2e:nodejs24x": "RUNTIME=nodejs24x vitest --run tests/e2e", "test:e2e": "vitest --run tests/e2e", "build:cjs": "tsc --build tsconfig.cjs.json && echo '{ \"type\": \"commonjs\" }' > lib/cjs/package.json", "build:esm": "tsc --build tsconfig.json && echo '{ \"type\": \"module\" }' > lib/esm/package.json", diff --git a/packages/idempotency/tests/e2e/idempotentDecorator.test.FunctionCode.ts b/packages/idempotency/tests/e2e/idempotentDecorator.test.FunctionCode.ts index e4bb5205e9..caf8ee20e5 100644 --- a/packages/idempotency/tests/e2e/idempotentDecorator.test.FunctionCode.ts +++ b/packages/idempotency/tests/e2e/idempotentDecorator.test.FunctionCode.ts @@ -1,3 +1,4 @@ +import { setTimeout } from 'node:timers/promises'; import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; import { Logger } from '@aws-lambda-powertools/logger'; import type { Context } from 'aws-lambda'; @@ -34,7 +35,7 @@ class DefaultLambda implements LambdaInterface { ): Promise { logger.info(`${this.message} ${JSON.stringify(_event)}`); // sleep to enforce error with parallel execution - await new Promise((resolve) => setTimeout(resolve, 1000)); + await setTimeout(1000); } @idempotent({ @@ -74,7 +75,7 @@ class DefaultLambda implements LambdaInterface { public async handlerParallel(event: { foo: string }, context: Context) { logger.addContext(context); - await new Promise((resolve) => setTimeout(resolve, 1500)); + await setTimeout(1500); logger.info('Processed event', { details: event.foo }); @@ -94,7 +95,7 @@ class DefaultLambda implements LambdaInterface { logger.addContext(context); if (event.invocation === 0) { - await new Promise((resolve) => setTimeout(resolve, 4000)); + await setTimeout(4000); } logger.info('Processed event', { @@ -121,9 +122,9 @@ const handlerExpired = defaultLambda.handlerExpired.bind(defaultLambda); const logger = new Logger(); class LambdaWithKeywordArgument implements LambdaInterface { - public handler(event: { id: string }, _context: Context) { - config.registerLambdaContext(_context); - this.process(event.id, 'bar'); + public async handler(event: { id: string }, context: Context) { + config.registerLambdaContext(context); + await this.process(event.id, 'bar'); return 'Hello World Keyword Argument'; } @@ -133,7 +134,8 @@ class LambdaWithKeywordArgument implements LambdaInterface { config: config, dataIndexArgument: 1, }) - public process(id: string, foo: string) { + public async process(id: string, foo: string) { + await setTimeout(1); logger.info('Got test event', { id, foo }); return `idempotent result: ${foo}`; diff --git a/packages/kafka/package.json b/packages/kafka/package.json index 65c181428c..773e4db572 100644 --- a/packages/kafka/package.json +++ b/packages/kafka/package.json @@ -16,6 +16,7 @@ "test:unit:types": "echo 'Not Implemented'", "test:e2e:nodejs20x": "echo \"Not implemented\"", "test:e2e:nodejs22x": "echo \"Not implemented\"", + "test:e2e:nodejs24x": "echo \"Not implemented\"", "test:e2e": "echo \"Not implemented\"", "build:cjs": "tsc --build tsconfig.cjs.json && echo '{ \"type\": \"commonjs\" }' > lib/cjs/package.json", "build:esm": "tsc --build tsconfig.json && echo '{ \"type\": \"module\" }' > lib/esm/package.json", diff --git a/packages/logger/package.json b/packages/logger/package.json index b971014b14..7a13a3b259 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -17,6 +17,7 @@ "test:unit:watch": "vitest tests/unit", "test:e2e:nodejs20x": "RUNTIME=nodejs20x vitest --run tests/e2e", "test:e2e:nodejs22x": "RUNTIME=nodejs22x vitest --run tests/e2e", + "test:e2e:nodejs24x": "RUNTIME=nodejs24x vitest --run tests/e2e", "test:e2e": "vitest --run tests/e2e", "build:cjs": "tsc --build tsconfig.cjs.json && echo '{ \"type\": \"commonjs\" }' > lib/cjs/package.json", "build:esm": "tsc --build tsconfig.json && echo '{ \"type\": \"module\" }' > lib/esm/package.json", @@ -98,8 +99,8 @@ "url": "https://github.com/aws-powertools/powertools-lambda-typescript/issues" }, "dependencies": { + "@aws/lambda-invoke-store": "0.2.1", "@aws-lambda-powertools/commons": "2.28.1", - "@aws/lambda-invoke-store": "0.1.1", "lodash.merge": "^4.6.2" }, "keywords": [ diff --git a/packages/logger/src/LogAttributesStore.ts b/packages/logger/src/LogAttributesStore.ts index a274d474ab..649e8d0c12 100644 --- a/packages/logger/src/LogAttributesStore.ts +++ b/packages/logger/src/LogAttributesStore.ts @@ -1,4 +1,5 @@ -import { InvokeStore } from '@aws/lambda-invoke-store'; +import '@aws/lambda-invoke-store'; +import { shouldUseInvokeStore } from '@aws-lambda-powertools/commons/utils/env'; import type { LogAttributes } from './types/logKeys.js'; /** @@ -20,31 +21,41 @@ class LogAttributesStore { #persistentAttributes: LogAttributes = {}; #getTemporaryAttributes(): LogAttributes { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { return this.#fallbackTemporaryAttributes; } - let stored = InvokeStore.get(this.#temporaryAttributesKey) as + if (globalThis.awslambda?.InvokeStore === undefined) { + throw new Error('InvokeStore is not available'); + } + + const store = globalThis.awslambda.InvokeStore; + let stored = store.get(this.#temporaryAttributesKey) as | LogAttributes | undefined; if (stored == null) { stored = {}; - InvokeStore.set(this.#temporaryAttributesKey, stored); + store.set(this.#temporaryAttributesKey, stored); } return stored; } #getKeys(): Map { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { return this.#fallbackKeys; } - let stored = InvokeStore.get(this.#keysKey) as + if (globalThis.awslambda?.InvokeStore === undefined) { + throw new Error('InvokeStore is not available'); + } + + const store = globalThis.awslambda.InvokeStore; + let stored = store.get(this.#keysKey) as | Map | undefined; if (stored == null) { stored = new Map(); - InvokeStore.set(this.#keysKey, stored); + store.set(this.#keysKey, stored); } return stored; } @@ -90,12 +101,12 @@ class LogAttributesStore { } } - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { this.#fallbackTemporaryAttributes = {}; return; } - InvokeStore.set(this.#temporaryAttributesKey, {}); + globalThis.awslambda.InvokeStore?.set(this.#temporaryAttributesKey, {}); } public setPersistentAttributes(attributes: LogAttributes): void { diff --git a/packages/logger/tests/unit/concurrency/logAttributesStore.test.ts b/packages/logger/tests/unit/concurrency/logAttributesStore.test.ts index d64b526724..144ee69721 100644 --- a/packages/logger/tests/unit/concurrency/logAttributesStore.test.ts +++ b/packages/logger/tests/unit/concurrency/logAttributesStore.test.ts @@ -5,6 +5,7 @@ import { LogAttributesStore } from '../../../src/LogAttributesStore.js'; describe('LogAttributesStore concurrent invocation isolation', () => { beforeEach(() => { vi.clearAllMocks(); + vi.unstubAllEnvs(); }); it.each([ @@ -24,6 +25,9 @@ describe('LogAttributesStore concurrent invocation isolation', () => { 'handles storing temporary attributes $description', async ({ useInvokeStore, expectedResult1, expectedResult2 }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const store = new LogAttributesStore(); // Act @@ -62,6 +66,9 @@ describe('LogAttributesStore concurrent invocation isolation', () => { 'handles clearing temporary attributes $description', async ({ useInvokeStore, expectedResult1, expectedResult2 }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const store = new LogAttributesStore(); // Act @@ -93,6 +100,7 @@ describe('LogAttributesStore concurrent invocation isolation', () => { it('persistent attributes are shared across invocations', async () => { // Prepare + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); const store = new LogAttributesStore(); store.setPersistentAttributes({ service: 'my-service' }); @@ -131,6 +139,9 @@ describe('LogAttributesStore concurrent invocation isolation', () => { 'isolates temporary keys $description', async ({ useInvokeStore, expectedResult1, expectedResult2 }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const store = new LogAttributesStore(); // Act diff --git a/packages/logger/tests/unit/concurrency/logger.test.ts b/packages/logger/tests/unit/concurrency/logger.test.ts index 53ceccecd1..a47a434140 100644 --- a/packages/logger/tests/unit/concurrency/logger.test.ts +++ b/packages/logger/tests/unit/concurrency/logger.test.ts @@ -1,5 +1,5 @@ import { sequence } from '@aws-lambda-powertools/testing-utils'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { Logger } from '../../../src/index.js'; describe('Logger concurrent invocation isolation', () => { @@ -8,6 +8,67 @@ describe('Logger concurrent invocation isolation', () => { vi.clearAllMocks(); }); + afterEach(() => { + vi.unstubAllEnvs(); + }); + + describe('InvokeStore error handling', () => { + beforeEach(() => { + vi.stubGlobal('awslambda', undefined); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('throws error when AWS_LAMBDA_MAX_CONCURRENCY is set but InvokeStore is not available', () => { + // Prepare + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + const logger = new Logger({ serviceName: 'test' }); + + // Act & Assess + expect(() => { + logger.appendKeys({ requestId: 'req-1' }); + }).toThrow('InvokeStore is not available'); + }); + + it('throws error when clearing attributes with InvokeStore unavailable', () => { + // Prepare + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + const logger = new Logger({ serviceName: 'test' }); + + // Act & Assess + expect(() => { + logger.resetKeys(); + }).toThrow('InvokeStore is not available'); + }); + + it('throws error when clearing attributes after adding keys with InvokeStore unavailable', () => { + // Prepare + vi.unstubAllGlobals(); + const logger = new Logger({ serviceName: 'test' }); + logger.appendKeys({ requestId: 'req-1' }); + vi.stubGlobal('awslambda', undefined); + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + + // Act & Assess + expect(() => { + logger.resetKeys(); + }).toThrow('InvokeStore is not available'); + }); + + it('throws error when setting persistent attributes with InvokeStore unavailable', () => { + // Prepare + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + const logger = new Logger({ serviceName: 'test' }); + + // Act & Assess + expect(() => { + logger.appendPersistentKeys({ env: 'prod' }); + }).toThrow('InvokeStore is not available'); + }); + }); + it.each([ { description: 'without InvokeStore', @@ -26,6 +87,9 @@ describe('Logger concurrent invocation isolation', () => { 'handles temporary attributes $description', async ({ useInvokeStore, expectedKeys }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const logger = new Logger({ serviceName: 'test' }); // Act @@ -78,6 +142,9 @@ describe('Logger concurrent invocation isolation', () => { 'handles persistent attributes $description', async ({ useInvokeStore, expectedKeys }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const logger = new Logger({ serviceName: 'test', persistentKeys: { app: 'test' }, @@ -139,6 +206,9 @@ describe('Logger concurrent invocation isolation', () => { 'handles mixed temporary and persistent attributes $description', async ({ useInvokeStore, expectedKeys }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const logger = new Logger({ serviceName: 'test', persistentKeys: { app: 'test' }, @@ -193,6 +263,9 @@ describe('Logger concurrent invocation isolation', () => { 'handles clearing temporary attributes $description', async ({ useInvokeStore, shouldContain }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const logger = new Logger({ serviceName: 'test' }); // Act @@ -249,6 +322,9 @@ describe('Logger concurrent invocation isolation', () => { 'handles removing specific temporary keys $description', async ({ useInvokeStore, expectedKeys }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const logger = new Logger({ serviceName: 'test' }); // Act @@ -303,6 +379,9 @@ describe('Logger concurrent invocation isolation', () => { 'handles correlation IDs $description', async ({ useInvokeStore, expectedKeys }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const logger = new Logger({ serviceName: 'test' }); // Act diff --git a/packages/metrics/package.json b/packages/metrics/package.json index d166c215d8..e85221f9a7 100644 --- a/packages/metrics/package.json +++ b/packages/metrics/package.json @@ -17,6 +17,7 @@ "test:unit:watch": "vitest tests/unit", "test:e2e:nodejs20x": "RUNTIME=nodejs20x vitest --run tests/e2e", "test:e2e:nodejs22x": "RUNTIME=nodejs22x vitest --run tests/e2e", + "test:e2e:nodejs24x": "RUNTIME=nodejs24x vitest --run tests/e2e", "test:e2e": "vitest --run tests/e2e", "build:cjs": "tsc --build tsconfig.cjs.json && echo '{ \"type\": \"commonjs\" }' > lib/cjs/package.json", "build:esm": "tsc --build tsconfig.json && echo '{ \"type\": \"module\" }' > lib/esm/package.json", @@ -64,6 +65,7 @@ "types": "./lib/cjs/index.d.ts", "main": "./lib/cjs/index.js", "devDependencies": { + "@aws/lambda-invoke-store": "0.2.1", "@aws-lambda-powertools/testing-utils": "file:../testing", "@aws-sdk/client-cloudwatch": "^3.932.0", "@types/promise-retry": "^1.1.3", @@ -88,8 +90,7 @@ "url": "https://github.com/aws-powertools/powertools-lambda-typescript/issues" }, "dependencies": { - "@aws-lambda-powertools/commons": "2.28.1", - "@aws/lambda-invoke-store": "0.1.1" + "@aws-lambda-powertools/commons": "2.28.1" }, "keywords": [ "aws", diff --git a/packages/metrics/src/DimensionsStore.ts b/packages/metrics/src/DimensionsStore.ts index 0ce43132e7..4a665f1b5c 100644 --- a/packages/metrics/src/DimensionsStore.ts +++ b/packages/metrics/src/DimensionsStore.ts @@ -1,4 +1,5 @@ -import { InvokeStore } from '@aws/lambda-invoke-store'; +import '@aws/lambda-invoke-store'; +import { shouldUseInvokeStore } from '@aws-lambda-powertools/commons/utils/env'; import type { Dimensions } from './types/Metrics.js'; /** @@ -18,29 +19,37 @@ class DimensionsStore { #defaultDimensions: Dimensions = {}; #getDimensions(): Dimensions { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { return this.#fallbackDimensions; } - let stored = InvokeStore.get(this.#dimensionsKey) as Dimensions | undefined; + if (globalThis.awslambda?.InvokeStore === undefined) { + throw new Error('InvokeStore is not available'); + } + + const store = globalThis.awslambda.InvokeStore; + let stored = store.get(this.#dimensionsKey) as Dimensions | undefined; if (stored == null) { stored = {}; - InvokeStore.set(this.#dimensionsKey, stored); + store.set(this.#dimensionsKey, stored); } return stored; } #getDimensionSets(): Dimensions[] { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { return this.#fallbackDimensionSets; } - let stored = InvokeStore.get(this.#dimensionSetsKey) as - | Dimensions[] - | undefined; + if (globalThis.awslambda?.InvokeStore === undefined) { + throw new Error('InvokeStore is not available'); + } + + const store = globalThis.awslambda.InvokeStore; + let stored = store.get(this.#dimensionSetsKey) as Dimensions[] | undefined; if (stored == null) { stored = []; - InvokeStore.set(this.#dimensionSetsKey, stored); + store.set(this.#dimensionSetsKey, stored); } return stored; } @@ -64,14 +73,19 @@ class DimensionsStore { } public clearRequestDimensions(): void { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { this.#fallbackDimensions = {}; this.#fallbackDimensionSets = []; return; } - InvokeStore.set(this.#dimensionsKey, {}); - InvokeStore.set(this.#dimensionSetsKey, []); + if (globalThis.awslambda?.InvokeStore === undefined) { + throw new Error('InvokeStore is not available'); + } + + const store = globalThis.awslambda.InvokeStore; + store.set(this.#dimensionsKey, {}); + store.set(this.#dimensionSetsKey, []); } public clearDefaultDimensions(): void { diff --git a/packages/metrics/src/MetadataStore.ts b/packages/metrics/src/MetadataStore.ts index 833048e442..9f74a51331 100644 --- a/packages/metrics/src/MetadataStore.ts +++ b/packages/metrics/src/MetadataStore.ts @@ -1,4 +1,5 @@ -import { InvokeStore } from '@aws/lambda-invoke-store'; +import '@aws/lambda-invoke-store'; +import { shouldUseInvokeStore } from '@aws-lambda-powertools/commons/utils/env'; /** * Manages storage of metrics #metadata with automatic context detection. @@ -14,16 +15,21 @@ class MetadataStore { #fallbackStorage: Record = {}; #getStorage(): Record { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { return this.#fallbackStorage; } - let stored = InvokeStore.get(this.#metadataKey) as + if (globalThis.awslambda?.InvokeStore === undefined) { + throw new Error('InvokeStore is not available'); + } + + const store = globalThis.awslambda.InvokeStore; + let stored = store.get(this.#metadataKey) as | Record | undefined; if (stored == null) { stored = {}; - InvokeStore.set(this.#metadataKey, stored); + store.set(this.#metadataKey, stored); } return stored; } @@ -38,12 +44,17 @@ class MetadataStore { } public clear(): void { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { this.#fallbackStorage = {}; return; } - InvokeStore.set(this.#metadataKey, {}); + if (globalThis.awslambda?.InvokeStore === undefined) { + throw new Error('InvokeStore is not available'); + } + + const store = globalThis.awslambda.InvokeStore; + store.set(this.#metadataKey, {}); } } diff --git a/packages/metrics/src/MetricsStore.ts b/packages/metrics/src/MetricsStore.ts index d2c08b3559..5446465ffd 100644 --- a/packages/metrics/src/MetricsStore.ts +++ b/packages/metrics/src/MetricsStore.ts @@ -1,5 +1,6 @@ -import { InvokeStore } from '@aws/lambda-invoke-store'; +import '@aws/lambda-invoke-store'; import { isIntegerNumber } from '@aws-lambda-powertools/commons/typeutils'; +import { shouldUseInvokeStore } from '@aws-lambda-powertools/commons/utils/env'; import { MetricResolution as MetricResolutions } from './constants.js'; import type { MetricResolution, @@ -24,16 +25,19 @@ class MetricsStore { #fallbackTimestamp?: number; #getStorage(): StoredMetrics { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { return this.#fallbackStorage; } - let stored = InvokeStore.get(this.#storedMetricsKey) as - | StoredMetrics - | undefined; + if (globalThis.awslambda?.InvokeStore === undefined) { + throw new Error('InvokeStore is not available'); + } + + const store = globalThis.awslambda.InvokeStore; + let stored = store.get(this.#storedMetricsKey) as StoredMetrics | undefined; if (stored == null) { stored = {}; - InvokeStore.set(this.#storedMetricsKey, stored); + store.set(this.#storedMetricsKey, stored); } return stored; } @@ -105,14 +109,19 @@ class MetricsStore { } public clearMetrics(): void { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { this.#fallbackStorage = {}; this.#fallbackTimestamp = undefined; return; } - InvokeStore.set(this.#storedMetricsKey, {}); - InvokeStore.set(this.#timestampKey, undefined); + if (globalThis.awslambda?.InvokeStore === undefined) { + throw new Error('InvokeStore is not available'); + } + + const store = globalThis.awslambda.InvokeStore; + store.set(this.#storedMetricsKey, {}); + store.set(this.#timestampKey, undefined); } public hasMetrics(): boolean { @@ -124,22 +133,32 @@ class MetricsStore { } public getTimestamp(): number | undefined { - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { return this.#fallbackTimestamp; } - return InvokeStore.get(this.#timestampKey) as number | undefined; + if (globalThis.awslambda?.InvokeStore === undefined) { + throw new Error('InvokeStore is not available'); + } + + const store = globalThis.awslambda.InvokeStore; + return store.get(this.#timestampKey) as number | undefined; } public setTimestamp(timestamp: number | Date): number { const timestampMs = this.#convertTimestampToEmfFormat(timestamp); - if (InvokeStore.getContext() === undefined) { + if (!shouldUseInvokeStore()) { this.#fallbackTimestamp = timestampMs; return timestampMs; } - InvokeStore.set(this.#timestampKey, timestampMs); + if (globalThis.awslambda?.InvokeStore === undefined) { + throw new Error('InvokeStore is not available'); + } + + const store = globalThis.awslambda.InvokeStore; + store.set(this.#timestampKey, timestampMs); return timestampMs; } diff --git a/packages/metrics/tests/unit/concurrency/dimensionsStore.test.ts b/packages/metrics/tests/unit/concurrency/dimensionsStore.test.ts index 7080d79316..a8cb1ce3f4 100644 --- a/packages/metrics/tests/unit/concurrency/dimensionsStore.test.ts +++ b/packages/metrics/tests/unit/concurrency/dimensionsStore.test.ts @@ -1,5 +1,5 @@ import { sequence } from '@aws-lambda-powertools/testing-utils'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { DimensionsStore } from '../../../src/DimensionsStore.js'; describe('DimensionsStore concurrent invocation isolation', () => { @@ -7,6 +7,67 @@ describe('DimensionsStore concurrent invocation isolation', () => { vi.clearAllMocks(); }); + afterEach(() => { + vi.unstubAllEnvs(); + }); + + describe('InvokeStore error handling', () => { + beforeEach(() => { + vi.stubGlobal('awslambda', undefined); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('throws error when AWS_LAMBDA_MAX_CONCURRENCY is set but InvokeStore is not available', () => { + // Prepare + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + const store = new DimensionsStore(); + + // Act & Assess + expect(() => { + store.addDimension('env', 'prod'); + }).toThrow('InvokeStore is not available'); + }); + + it('throws error when clearing dimensions with InvokeStore unavailable', () => { + // Prepare + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + const store = new DimensionsStore(); + + // Act & Assess + expect(() => { + store.clearRequestDimensions(); + }).toThrow('InvokeStore is not available'); + }); + + it('throws error when clearing dimensions after adding with InvokeStore unavailable', () => { + // Prepare + vi.unstubAllGlobals(); + const store = new DimensionsStore(); + store.addDimension('env', 'prod'); + vi.stubGlobal('awslambda', undefined); + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + + // Act & Assess + expect(() => { + store.clearRequestDimensions(); + }).toThrow('InvokeStore is not available'); + }); + + it('throws error when getting dimension sets with InvokeStore unavailable', () => { + // Prepare + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + const store = new DimensionsStore(); + + // Act & Assess + expect(() => { + store.getDimensionSets(); + }).toThrow('InvokeStore is not available'); + }); + }); + it.each([ { description: 'without InvokeStore', @@ -24,6 +85,9 @@ describe('DimensionsStore concurrent invocation isolation', () => { 'handles storing dimensions $description', async ({ useInvokeStore, expectedResult1, expectedResult2 }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const store = new DimensionsStore(); // Act @@ -68,6 +132,9 @@ describe('DimensionsStore concurrent invocation isolation', () => { 'handles storing dimension sets $description', async ({ useInvokeStore, expectedResult1, expectedResult2 }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const store = new DimensionsStore(); // Act @@ -113,6 +180,9 @@ describe('DimensionsStore concurrent invocation isolation', () => { 'handles clearing the store $description', async ({ useInvokeStore, expectedResult1, expectedResult2 }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const store = new DimensionsStore(); // Act diff --git a/packages/metrics/tests/unit/concurrency/metadataStore.test.ts b/packages/metrics/tests/unit/concurrency/metadataStore.test.ts index 0f857e83cf..8ebd51cf4b 100644 --- a/packages/metrics/tests/unit/concurrency/metadataStore.test.ts +++ b/packages/metrics/tests/unit/concurrency/metadataStore.test.ts @@ -1,5 +1,5 @@ import { sequence } from '@aws-lambda-powertools/testing-utils'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { MetadataStore } from '../../../src/MetadataStore.js'; describe('MetadataStore concurrent invocation isolation', () => { @@ -7,6 +7,42 @@ describe('MetadataStore concurrent invocation isolation', () => { vi.clearAllMocks(); }); + afterEach(() => { + vi.unstubAllEnvs(); + }); + + describe('InvokeStore error handling', () => { + beforeEach(() => { + vi.stubGlobal('awslambda', undefined); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('throws error when AWS_LAMBDA_MAX_CONCURRENCY is set but InvokeStore is not available', () => { + // Prepare + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + const store = new MetadataStore(); + + // Act & Assess + expect(() => { + store.set('env', 'prod'); + }).toThrow('InvokeStore is not available'); + }); + + it('throws error when clearing metadata with InvokeStore unavailable', () => { + // Prepare + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + const store = new MetadataStore(); + + // Act & Assess + expect(() => { + store.clear(); + }).toThrow('InvokeStore is not available'); + }); + }); + it.each([ { description: 'without InvokeStore', @@ -24,6 +60,9 @@ describe('MetadataStore concurrent invocation isolation', () => { 'handles storing metadata $description', async ({ useInvokeStore, expectedResult1, expectedResult2 }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const store = new MetadataStore(); // Act @@ -62,6 +101,9 @@ describe('MetadataStore concurrent invocation isolation', () => { 'handles storing multiple metadata keys $description', async ({ useInvokeStore, expectedResult1, expectedResult2 }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const store = new MetadataStore(); // Act @@ -110,6 +152,9 @@ describe('MetadataStore concurrent invocation isolation', () => { 'handles clearing the store $description', async ({ useInvokeStore, expectedResult1, expectedResult2 }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const store = new MetadataStore(); // Act @@ -162,6 +207,9 @@ describe('MetadataStore concurrent invocation isolation', () => { 'handles overwriting same key $description', async ({ useInvokeStore, expectedResult1, expectedResult2 }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const store = new MetadataStore(); // Act diff --git a/packages/metrics/tests/unit/concurrency/metrics.test.ts b/packages/metrics/tests/unit/concurrency/metrics.test.ts index f049b90994..4898717a6d 100644 --- a/packages/metrics/tests/unit/concurrency/metrics.test.ts +++ b/packages/metrics/tests/unit/concurrency/metrics.test.ts @@ -1,5 +1,5 @@ import { sequence } from '@aws-lambda-powertools/testing-utils'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { Metrics, MetricUnit } from '../../../src/index.js'; describe('Metrics concurrent invocation isolation', () => { @@ -9,6 +9,10 @@ describe('Metrics concurrent invocation isolation', () => { vi.clearAllMocks(); }); + afterEach(() => { + vi.unstubAllEnvs(); + }); + it.each([ { description: 'without InvokeStore', @@ -34,6 +38,9 @@ describe('Metrics concurrent invocation isolation', () => { ])( 'handles metrics, metadata, and dimensions $description', async ({ useInvokeStore, expectedCallCount, expectedOutputs }) => { + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const metrics = new Metrics({ singleMetric: false }); await sequence( @@ -93,6 +100,9 @@ describe('Metrics concurrent invocation isolation', () => { ])( 'handles timestamps $description', async ({ useInvokeStore, expectedCallCount, expectedOutputs }) => { + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const metrics = new Metrics({ singleMetric: false }); const timestamp1 = 1000; const timestamp2 = 2000; diff --git a/packages/metrics/tests/unit/concurrency/metricsStore.test.ts b/packages/metrics/tests/unit/concurrency/metricsStore.test.ts index 3969f331d6..e88acfff79 100644 --- a/packages/metrics/tests/unit/concurrency/metricsStore.test.ts +++ b/packages/metrics/tests/unit/concurrency/metricsStore.test.ts @@ -1,5 +1,5 @@ import { sequence } from '@aws-lambda-powertools/testing-utils'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { MetricUnit } from '../../../src/index.js'; import { MetricsStore } from '../../../src/MetricsStore.js'; import type { StoredMetric } from '../../../src/types/index.js'; @@ -9,6 +9,64 @@ describe('MetricsStore concurrent invocation isolation', () => { vi.clearAllMocks(); }); + afterEach(() => { + vi.unstubAllEnvs(); + }); + + describe('InvokeStore error handling', () => { + beforeEach(() => { + vi.stubGlobal('awslambda', undefined); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('throws error when AWS_LAMBDA_MAX_CONCURRENCY is set but InvokeStore is not available', () => { + // Prepare + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + const store = new MetricsStore(); + + // Act & Assess + expect(() => { + store.setMetric('count', MetricUnit.Count, 1, 60); + }).toThrow('InvokeStore is not available'); + }); + + it('throws error when clearing metrics with InvokeStore unavailable', () => { + // Prepare + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + const store = new MetricsStore(); + + // Act & Assess + expect(() => { + store.clearMetrics(); + }).toThrow('InvokeStore is not available'); + }); + + it('throws error when getting timestamp with InvokeStore unavailable', () => { + // Prepare + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + const store = new MetricsStore(); + + // Act & Assess + expect(() => { + store.getTimestamp(); + }).toThrow('InvokeStore is not available'); + }); + + it('throws error when setting timestamp with InvokeStore unavailable', () => { + // Prepare + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + const store = new MetricsStore(); + + // Act & Assess + expect(() => { + store.setTimestamp(1000); + }).toThrow('InvokeStore is not available'); + }); + }); + it.each([ { description: 'without InvokeStore', @@ -46,6 +104,9 @@ describe('MetricsStore concurrent invocation isolation', () => { 'getMetric() $description', async ({ useInvokeStore, expectedResult1, expectedResult2 }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const store = new MetricsStore(); // Act @@ -107,6 +168,9 @@ describe('MetricsStore concurrent invocation isolation', () => { 'getAllMetrics() $description', async ({ useInvokeStore, expectedResult1, expectedResult2 }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const store = new MetricsStore(); // Act @@ -154,6 +218,9 @@ describe('MetricsStore concurrent invocation isolation', () => { 'timestamp $description', async ({ useInvokeStore, expectedResult1, expectedResult2 }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const store = new MetricsStore(); const timestamp1 = 1000; const timestamp2 = 2000; @@ -194,6 +261,9 @@ describe('MetricsStore concurrent invocation isolation', () => { 'clearMetrics() $description', async ({ useInvokeStore, expectedResult1, expectedResult2 }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const store = new MetricsStore(); // Act @@ -252,6 +322,9 @@ describe('MetricsStore concurrent invocation isolation', () => { 'hasMetrics() $description', async ({ useInvokeStore, expectedResult1, expectedResult2 }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const store = new MetricsStore(); // Act @@ -292,6 +365,9 @@ describe('MetricsStore concurrent invocation isolation', () => { 'getMetricsCount() $description', async ({ useInvokeStore, expectedResult1, expectedResult2 }) => { // Prepare + if (useInvokeStore) { + vi.stubEnv('AWS_LAMBDA_MAX_CONCURRENCY', '10'); + } const store = new MetricsStore(); // Act diff --git a/packages/parameters/package.json b/packages/parameters/package.json index 8af7cfa78c..0c51d2abca 100644 --- a/packages/parameters/package.json +++ b/packages/parameters/package.json @@ -17,6 +17,7 @@ "test:unit:watch": "vitest tests/unit", "test:e2e:nodejs20x": "RUNTIME=nodejs20x vitest --run tests/e2e", "test:e2e:nodejs22x": "RUNTIME=nodejs22x vitest --run tests/e2e", + "test:e2e:nodejs24x": "RUNTIME=nodejs24x vitest --run tests/e2e", "test:e2e": "vitest --run tests/e2e", "build:cjs": "tsc --build tsconfig.cjs.json && echo '{ \"type\": \"commonjs\" }' > lib/cjs/package.json", "build:esm": "tsc --build tsconfig.json && echo '{ \"type\": \"module\" }' > lib/esm/package.json", diff --git a/packages/parser/package.json b/packages/parser/package.json index 0d83b7f988..640b098ca2 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -17,6 +17,7 @@ "test:unit:watch": "vitest tests/unit", "test:e2e:nodejs20x": "echo 'Not implemented'", "test:e2e:nodejs22x": "echo 'Not implemented'", + "test:e2e:nodejs24x": "echo 'Not implemented'", "test:e2e": "echo 'Not implemented'", "build:cjs": "tsc --build tsconfig.cjs.json && echo '{ \"type\": \"commonjs\" }' > lib/cjs/package.json", "build:esm": "tsc --build tsconfig.json && echo '{ \"type\": \"module\" }' > lib/esm/package.json", diff --git a/packages/testing/package.json b/packages/testing/package.json index 94c434831f..58918f07d1 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -100,6 +100,7 @@ "dependencies": { "@aws-cdk/toolkit-lib": "^1.10.4", "@aws-sdk/client-lambda": "^3.932.0", + "@aws/lambda-invoke-store": "0.2.1", "@smithy/util-utf8": "^4.0.0", "aws-cdk-lib": "^2.225.0", "esbuild": "^0.27.0", diff --git a/packages/testing/src/constants.ts b/packages/testing/src/constants.ts index a306ffa9f4..f95779941a 100644 --- a/packages/testing/src/constants.ts +++ b/packages/testing/src/constants.ts @@ -3,14 +3,15 @@ import { Architecture, Runtime } from 'aws-cdk-lib/aws-lambda'; /** * The default AWS Lambda runtime to use when none is provided. */ -const defaultRuntime = 'nodejs22x'; +const defaultRuntime = 'nodejs24x'; /** * The AWS Lambda runtimes that are supported by the project. */ const TEST_RUNTIMES = { nodejs20x: Runtime.NODEJS_20_X, - [defaultRuntime]: Runtime.NODEJS_22_X, + nodejs22x: Runtime.NODEJS_22_X, + [defaultRuntime]: Runtime.NODEJS_24_X, } as const; /** diff --git a/packages/testing/src/helpers.ts b/packages/testing/src/helpers.ts index 4fdee898a2..7e3f417e67 100644 --- a/packages/testing/src/helpers.ts +++ b/packages/testing/src/helpers.ts @@ -10,7 +10,7 @@ import { const isValidRuntimeKey = ( runtime: string ): runtime is keyof typeof TEST_RUNTIMES => - runtime in TEST_RUNTIMES || runtime === 'nodejs22x'; + runtime in TEST_RUNTIMES || runtime === 'nodejs24x'; const getRuntimeKey = (): keyof typeof TEST_RUNTIMES => { const runtime: string = process.env.RUNTIME || defaultRuntime; @@ -194,13 +194,14 @@ const withResolvers = () => { * // Execution order: action1() → action2() → action3() → both return * ``` */ -function sequence( +async function sequence( inv1: Invocation, inv2: Invocation, options: { useInvokeStore?: boolean } ): Promise<[T1, T2]> { + const invokeStore = await InvokeStore.getInstanceAsync(); const executionEnv = (f: () => T) => - options?.useInvokeStore ? InvokeStore.run({}, f) : f(); + options?.useInvokeStore ? invokeStore.run({}, f) : f(); const inv1Barriers = inv1.sideEffects.map(() => withResolvers()); const inv2Barriers = inv2.sideEffects.map(() => withResolvers()); diff --git a/packages/tracer/package.json b/packages/tracer/package.json index 1947942f77..b218a39b89 100644 --- a/packages/tracer/package.json +++ b/packages/tracer/package.json @@ -17,6 +17,7 @@ "test:unit:watch": "vitest tests/unit", "test:e2e:nodejs20x": "RUNTIME=nodejs20x vitest --run tests/e2e", "test:e2e:nodejs22x": "RUNTIME=nodejs22x vitest --run tests/e2e", + "test:e2e:nodejs24x": "RUNTIME=nodejs24x vitest --run tests/e2e", "test:e2e": "vitest --run tests/e2e", "build:cjs": "tsc --build tsconfig.cjs.json && echo '{ \"type\": \"commonjs\" }' > lib/cjs/package.json", "build:esm": "tsc --build tsconfig.json && echo '{ \"type\": \"module\" }' > lib/esm/package.json", diff --git a/packages/validation/package.json b/packages/validation/package.json index 4ffb28cbea..bbef13f946 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -13,6 +13,7 @@ "test:unit:types": "echo 'Not Implemented'", "test:e2e:nodejs20x": "echo \"Not implemented\"", "test:e2e:nodejs22x": "echo \"Not implemented\"", + "test:e2e:nodejs24x": "echo \"Not implemented\"", "test:e2e": "echo \"Not implemented\"", "build:cjs": "tsc --build tsconfig.cjs.json && echo '{ \"type\": \"commonjs\" }' > lib/cjs/package.json", "build:esm": "tsc --build tsconfig.json && echo '{ \"type\": \"module\" }' > lib/esm/package.json",