From 18105b52487d40b952af96e4b13d57ddf0327dd8 Mon Sep 17 00:00:00 2001 From: Pooya Paridel Date: Thu, 20 Nov 2025 13:39:21 -0800 Subject: [PATCH 1/7] feat(sdk): migrate waitForCondition to DurablePromise for two-phase execution Migrated wait-for-condition-handler to use DurablePromise pattern, enabling two-phase execution where operations start immediately but only gracefully terminate when awaited. Changes: - Updated handler to return DurablePromise instead of Promise - Implemented phase 1 (immediate execution) and phase 2 (await result) - Added isAwaited flag and waitingCallback mechanism for graceful termination - Updated waitForContinuation and executeWaitForCondition to support onAwaitedChange - Fixed tests to handle synchronous validation errors and DurablePromise instances - Added comprehensive two-phase execution tests This brings waitForCondition in line with other migrated handlers (step, invoke, wait, callback) and ensures consistent behavior across the SDK. --- ...it-for-condition-handler-two-phase.test.ts | 158 +++++++++++++++++ .../wait-for-condition-handler.test.ts | 11 +- .../wait-for-condition-handler.ts | 167 +++++++++++------- 3 files changed, 268 insertions(+), 68 deletions(-) create mode 100644 packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler-two-phase.test.ts diff --git a/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler-two-phase.test.ts b/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler-two-phase.test.ts new file mode 100644 index 00000000..c92a614a --- /dev/null +++ b/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler-two-phase.test.ts @@ -0,0 +1,158 @@ +import { createWaitForConditionHandler } from "./wait-for-condition-handler"; +import { ExecutionContext, WaitForConditionCheckFunc } from "../../types"; +import { EventEmitter } from "events"; +import { DurablePromise } from "../../types/durable-promise"; + +describe("WaitForCondition Handler Two-Phase Execution", () => { + let mockContext: ExecutionContext; + let mockCheckpoint: any; + let createStepId: () => string; + let createContextLogger: (stepId: string, attempt?: number) => any; + let addRunningOperation: jest.Mock; + let removeRunningOperation: jest.Mock; + let hasRunningOperations: () => boolean; + let getOperationsEmitter: () => EventEmitter; + let stepIdCounter = 0; + + beforeEach(() => { + stepIdCounter = 0; + mockContext = { + getStepData: jest.fn().mockReturnValue(null), + durableExecutionArn: "test-arn", + terminationManager: { + shouldTerminate: jest.fn().mockReturnValue(false), + terminate: jest.fn(), + }, + } as any; + + mockCheckpoint = jest.fn().mockResolvedValue(undefined); + mockCheckpoint.force = jest.fn().mockResolvedValue(undefined); + mockCheckpoint.setTerminating = jest.fn(); + mockCheckpoint.hasPendingAncestorCompletion = jest + .fn() + .mockReturnValue(false); + + createStepId = (): string => `step-${++stepIdCounter}`; + createContextLogger = jest.fn().mockReturnValue({ + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }); + addRunningOperation = jest.fn(); + removeRunningOperation = jest.fn(); + hasRunningOperations = jest.fn().mockReturnValue(false) as () => boolean; + getOperationsEmitter = (): EventEmitter => new EventEmitter(); + }); + + it("should execute check function in phase 1 immediately", async () => { + const waitForConditionHandler = createWaitForConditionHandler( + mockContext, + mockCheckpoint, + createStepId, + createContextLogger, + addRunningOperation, + removeRunningOperation, + hasRunningOperations, + getOperationsEmitter, + undefined, + ); + + const checkFn: WaitForConditionCheckFunc = jest + .fn() + .mockResolvedValue(10); + + // Phase 1: Create the promise - this executes the logic immediately + const promise = waitForConditionHandler(checkFn, { + initialState: 0, + waitStrategy: (state) => ({ shouldContinue: false }), + }); + + // Wait for phase 1 to complete + await new Promise((resolve) => setTimeout(resolve, 50)); + + // Should return a DurablePromise + expect(promise).toBeInstanceOf(DurablePromise); + + // Phase 1 should have executed the check function + expect(checkFn).toHaveBeenCalled(); + expect(mockCheckpoint).toHaveBeenCalled(); + }); + + it("should return cached result in phase 2 when awaited", async () => { + const waitForConditionHandler = createWaitForConditionHandler( + mockContext, + mockCheckpoint, + createStepId, + createContextLogger, + addRunningOperation, + removeRunningOperation, + hasRunningOperations, + getOperationsEmitter, + undefined, + ); + + const checkFn: WaitForConditionCheckFunc = jest + .fn() + .mockResolvedValue("completed"); + + // Phase 1: Create the promise + const promise = waitForConditionHandler(checkFn, { + initialState: "initial", + waitStrategy: (state) => ({ shouldContinue: false }), + }); + + // Wait for phase 1 to complete + await new Promise((resolve) => setTimeout(resolve, 50)); + + // Phase 2: Await the promise to get the result + const result = await promise; + + expect(result).toBe("completed"); + expect(checkFn).toHaveBeenCalledTimes(1); + }); + + it("should execute check function before await", async () => { + const waitForConditionHandler = createWaitForConditionHandler( + mockContext, + mockCheckpoint, + createStepId, + createContextLogger, + addRunningOperation, + removeRunningOperation, + hasRunningOperations, + getOperationsEmitter, + undefined, + ); + + let executionOrder: string[] = []; + const checkFn: WaitForConditionCheckFunc = jest.fn(async () => { + executionOrder.push("check-executed"); + return 42; + }); + + // Phase 1: Create the promise + executionOrder.push("promise-created"); + const promise = waitForConditionHandler(checkFn, { + initialState: 0, + waitStrategy: (state) => ({ shouldContinue: false }), + }); + executionOrder.push("after-handler-call"); + + // Wait for phase 1 to complete + await new Promise((resolve) => setTimeout(resolve, 50)); + + executionOrder.push("before-await"); + const result = await promise; + executionOrder.push("after-await"); + + // Verify execution order: check should execute before await + expect(executionOrder).toEqual([ + "promise-created", + "check-executed", + "after-handler-call", + "before-await", + "after-await", + ]); + expect(result).toBe(42); + }); +}); diff --git a/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler.test.ts b/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler.test.ts index 178f762f..2aa11a57 100644 --- a/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler.test.ts +++ b/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler.test.ts @@ -9,6 +9,7 @@ import { WaitForConditionConfig, OperationSubType, Logger, + DurablePromise, } from "../../types"; import { TerminationManager } from "../../termination-manager/termination-manager"; import { TerminationReason } from "../../termination-manager/types"; @@ -125,12 +126,12 @@ describe("WaitForCondition Handler", () => { }); }); - it("should throw error if config is missing", async () => { + it("should throw error if config is missing", () => { const checkFunc: WaitForConditionCheckFunc = jest.fn(); - await expect( + expect(() => waitForConditionHandler(checkFunc, undefined as any), - ).rejects.toThrow( + ).toThrow( "waitForCondition requires config with waitStrategy and initialState", ); }); @@ -509,8 +510,8 @@ describe("WaitForCondition Handler", () => { message: "Retry scheduled for step-1", }); - // Verify that the promise is indeed never-resolving by checking its constructor - expect(promise).toBeInstanceOf(Promise); + // Verify that the promise is indeed a DurablePromise + expect(promise).toBeInstanceOf(DurablePromise); }); it("should wait for timer when status is PENDING", async () => { diff --git a/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler.ts b/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler.ts index d6fc6c9d..594b6257 100644 --- a/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler.ts +++ b/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler.ts @@ -6,6 +6,7 @@ import { OperationSubType, WaitForConditionContext, Logger, + DurablePromise, } from "../../types"; import { durationToSeconds } from "../../utils/duration/duration"; import { terminate } from "../../utils/termination-helper/termination-helper"; @@ -41,6 +42,7 @@ const waitForContinuation = async ( hasRunningOperations: () => boolean, checkpoint: ReturnType, operationsEmitter: EventEmitter, + onAwaitedChange?: (callback: () => void) => void, ): Promise => { const stepData = context.getStepData(stepId); @@ -65,6 +67,7 @@ const waitForContinuation = async ( hasRunningOperations, operationsEmitter, checkpoint, + onAwaitedChange, }); // Return to let the main loop re-evaluate step status @@ -81,11 +84,11 @@ export const createWaitForConditionHandler = ( getOperationsEmitter: () => EventEmitter, parentId: string | undefined, ) => { - return async ( + return ( nameOrCheck: string | undefined | WaitForConditionCheckFunc, checkOrConfig?: WaitForConditionCheckFunc | WaitForConditionConfig, maybeConfig?: WaitForConditionConfig, - ): Promise => { + ): DurablePromise => { let name: string | undefined; let check: WaitForConditionCheckFunc; let config: WaitForConditionConfig; @@ -114,79 +117,115 @@ export const createWaitForConditionHandler = ( config, }); - // Main waitForCondition logic - can be re-executed if step status changes - while (true) { - try { - const stepData = context.getStepData(stepId); - - // Check if already completed - if (stepData?.Status === OperationStatus.SUCCEEDED) { - return await handleCompletedWaitForCondition( - context, - stepId, - name, - config.serdes, - ); - } + // Two-phase execution: Phase 1 starts immediately, Phase 2 returns result when awaited + let phase1Result: T | undefined; + let phase1Error: unknown; + let isAwaited = false; + let waitingCallback: (() => void) | undefined; - if (stepData?.Status === OperationStatus.FAILED) { - // Return an async rejected promise to ensure it's handled asynchronously - return (async (): Promise => { - // Reconstruct the original error from stored ErrorObject - if (stepData.StepDetails?.Error) { - throw DurableOperationError.fromErrorObject( - stepData.StepDetails.Error, - ); - } else { - // Fallback for legacy data without Error field - const errorMessage = stepData?.StepDetails?.Result; - throw new WaitForConditionError( - errorMessage || "waitForCondition failed", - ); - } - })(); - } + const setWaitingCallback = (cb: () => void): void => { + waitingCallback = cb; + }; - // If PENDING, wait for timer to complete - if (stepData?.Status === OperationStatus.PENDING) { - await waitForContinuation( + // Phase 1: Start execution immediately and capture result/error + const phase1Promise = (async (): Promise => { + // Main waitForCondition logic - can be re-executed if step status changes + while (true) { + try { + const stepData = context.getStepData(stepId); + + // Check if already completed + if (stepData?.Status === OperationStatus.SUCCEEDED) { + return await handleCompletedWaitForCondition( + context, + stepId, + name, + config.serdes, + ); + } + + if (stepData?.Status === OperationStatus.FAILED) { + // Return an async rejected promise to ensure it's handled asynchronously + return (async (): Promise => { + // Reconstruct the original error from stored ErrorObject + if (stepData.StepDetails?.Error) { + throw DurableOperationError.fromErrorObject( + stepData.StepDetails.Error, + ); + } else { + // Fallback for legacy data without Error field + const errorMessage = stepData?.StepDetails?.Result; + throw new WaitForConditionError( + errorMessage || "waitForCondition failed", + ); + } + })(); + } + + // If PENDING, wait for timer to complete + if (stepData?.Status === OperationStatus.PENDING) { + await waitForContinuation( + context, + stepId, + name, + hasRunningOperations, + checkpoint, + getOperationsEmitter(), + isAwaited ? undefined : setWaitingCallback, + ); + continue; // Re-evaluate step status after waiting + } + + // Execute check function for READY, STARTED, or first time (undefined) + const result = await executeWaitForCondition( context, + checkpoint, stepId, name, + check, + config, + createContextLogger, + addRunningOperation, + removeRunningOperation, hasRunningOperations, - checkpoint, - getOperationsEmitter(), + getOperationsEmitter, + parentId, + isAwaited ? undefined : setWaitingCallback, ); - continue; // Re-evaluate step status after waiting - } - // Execute check function for READY, STARTED, or first time (undefined) - const result = await executeWaitForCondition( - context, - checkpoint, - stepId, - name, - check, - config, - createContextLogger, - addRunningOperation, - removeRunningOperation, - hasRunningOperations, - getOperationsEmitter, - parentId, - ); + // If executeWaitForCondition signals to continue the main loop, do so + if (result === CONTINUE_MAIN_LOOP) { + continue; + } - // If executeWaitForCondition signals to continue the main loop, do so - if (result === CONTINUE_MAIN_LOOP) { - continue; + return result; + } catch (error) { + // For any error from executeWaitForCondition, re-throw it + throw error; } + } + })() + .then((result) => { + phase1Result = result; + }) + .catch((error) => { + phase1Error = error; + }); - return result; - } catch (error) { - // For any error from executeWaitForCondition, re-throw it - throw error; + // Phase 2: Return DurablePromise that returns Phase 1 result when awaited + return new DurablePromise(async () => { + // When promise is awaited, mark as awaited and invoke waiting callback + isAwaited = true; + if (waitingCallback) { + waitingCallback(); } - } + + await phase1Promise; + if (phase1Error !== undefined) { + throw phase1Error; + } + return phase1Result!; + }); }; }; @@ -227,6 +266,7 @@ export const executeWaitForCondition = async ( hasRunningOperations: () => boolean, getOperationsEmitter: () => EventEmitter, parentId: string | undefined, + onAwaitedChange?: ((callback: () => void) => void) | undefined, ): Promise => { const serdes = config.serdes || defaultSerdes; @@ -383,6 +423,7 @@ export const executeWaitForCondition = async ( hasRunningOperations, checkpoint, getOperationsEmitter(), + onAwaitedChange, ); return CONTINUE_MAIN_LOOP; } From f5bc39ca40c5a5461e2b1eb1f46d1b1a20ae9d9b Mon Sep 17 00:00:00 2001 From: Pooya Paridel Date: Thu, 20 Nov 2025 13:46:30 -0800 Subject: [PATCH 2/7] test(sdk): improve two-phase execution tests with immediate assertions Updated two-phase execution tests for waitForCondition, step, and runInChildContext handlers to assert function execution immediately after a brief timeout (10ms) and before awaiting the DurablePromise. This more clearly demonstrates that phase 1 executes immediately when the handler is called, not when the promise is awaited. Changes: - Reduced timeout from 50ms to 10ms for faster test execution - Added assertions after timeout but before await to prove immediate execution - Applied pattern consistently across all three handlers - All 704 tests still pass --- ...in-child-context-handler-two-phase.test.ts | 17 ++++++++----- .../step-handler-two-phase.test.ts | 17 ++++++++----- ...it-for-condition-handler-two-phase.test.ts | 25 +++++++++++++------ 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/packages/aws-durable-execution-sdk-js/src/handlers/run-in-child-context-handler/run-in-child-context-handler-two-phase.test.ts b/packages/aws-durable-execution-sdk-js/src/handlers/run-in-child-context-handler/run-in-child-context-handler-two-phase.test.ts index 9462db4e..53bfe64d 100644 --- a/packages/aws-durable-execution-sdk-js/src/handlers/run-in-child-context-handler/run-in-child-context-handler-two-phase.test.ts +++ b/packages/aws-durable-execution-sdk-js/src/handlers/run-in-child-context-handler/run-in-child-context-handler-two-phase.test.ts @@ -58,15 +58,18 @@ describe("Run In Child Context Handler Two-Phase Execution", () => { // Phase 1: Create the promise - this executes the logic immediately const childPromise = handler(childFn); - // Wait for phase 1 to complete - await new Promise((resolve) => setTimeout(resolve, 50)); - // Should return a DurablePromise expect(childPromise).toBeInstanceOf(DurablePromise); - // Phase 1 should have executed the child function + // Wait briefly for phase 1 to start executing + await new Promise((resolve) => setTimeout(resolve, 10)); + + // Phase 1 should have executed the child function (before we await the promise) expect(childFn).toHaveBeenCalled(); expect(mockCheckpoint).toHaveBeenCalled(); + + // Now await the promise to verify it completes + await childPromise; }); it("should return cached result in phase 2 when awaited", async () => { @@ -84,10 +87,12 @@ describe("Run In Child Context Handler Two-Phase Execution", () => { // Phase 1: Create the promise const childPromise = handler(childFn); - // Wait for phase 1 to complete - await new Promise((resolve) => setTimeout(resolve, 50)); + // Wait briefly for phase 1 to execute + await new Promise((resolve) => setTimeout(resolve, 10)); + // Child function should have been called before we await the promise const phase1Calls = childFn.mock.calls.length; + expect(phase1Calls).toBeGreaterThan(0); // Phase 2: Await the promise - should return cached result const result = await childPromise; diff --git a/packages/aws-durable-execution-sdk-js/src/handlers/step-handler/step-handler-two-phase.test.ts b/packages/aws-durable-execution-sdk-js/src/handlers/step-handler/step-handler-two-phase.test.ts index 8a1bbf77..253df6db 100644 --- a/packages/aws-durable-execution-sdk-js/src/handlers/step-handler/step-handler-two-phase.test.ts +++ b/packages/aws-durable-execution-sdk-js/src/handlers/step-handler/step-handler-two-phase.test.ts @@ -68,15 +68,18 @@ describe("Step Handler Two-Phase Execution", () => { // Phase 1: Create the promise - this executes the logic immediately const stepPromise = stepHandler(stepFn); - // Wait for phase 1 to complete - await new Promise((resolve) => setTimeout(resolve, 50)); - // Should return a DurablePromise expect(stepPromise).toBeInstanceOf(DurablePromise); - // Phase 1 should have executed the step function + // Wait briefly for phase 1 to start executing + await new Promise((resolve) => setTimeout(resolve, 10)); + + // Phase 1 should have executed the step function (before we await the promise) expect(stepFn).toHaveBeenCalled(); expect(mockCheckpoint).toHaveBeenCalled(); + + // Now await the promise to verify it completes + await stepPromise; }); it("should return cached result in phase 2 when awaited", async () => { @@ -97,10 +100,12 @@ describe("Step Handler Two-Phase Execution", () => { // Phase 1: Create the promise const stepPromise = stepHandler(stepFn); - // Wait for phase 1 to complete - await new Promise((resolve) => setTimeout(resolve, 50)); + // Wait briefly for phase 1 to execute + await new Promise((resolve) => setTimeout(resolve, 10)); + // Step function should have been called before we await the promise const phase1Calls = stepFn.mock.calls.length; + expect(phase1Calls).toBeGreaterThan(0); // Phase 2: Await the promise - should return cached result const result = await stepPromise; diff --git a/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler-two-phase.test.ts b/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler-two-phase.test.ts index c92a614a..beb8e5bb 100644 --- a/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler-two-phase.test.ts +++ b/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler-two-phase.test.ts @@ -67,15 +67,18 @@ describe("WaitForCondition Handler Two-Phase Execution", () => { waitStrategy: (state) => ({ shouldContinue: false }), }); - // Wait for phase 1 to complete - await new Promise((resolve) => setTimeout(resolve, 50)); - // Should return a DurablePromise expect(promise).toBeInstanceOf(DurablePromise); - // Phase 1 should have executed the check function + // Wait briefly for phase 1 to start executing + await new Promise((resolve) => setTimeout(resolve, 10)); + + // Phase 1 should have executed the check function (before we await the promise) expect(checkFn).toHaveBeenCalled(); expect(mockCheckpoint).toHaveBeenCalled(); + + // Now await the promise to verify it completes + await promise; }); it("should return cached result in phase 2 when awaited", async () => { @@ -101,8 +104,11 @@ describe("WaitForCondition Handler Two-Phase Execution", () => { waitStrategy: (state) => ({ shouldContinue: false }), }); - // Wait for phase 1 to complete - await new Promise((resolve) => setTimeout(resolve, 50)); + // Wait briefly for phase 1 to execute + await new Promise((resolve) => setTimeout(resolve, 10)); + + // Check function should have been called before we await the promise + expect(checkFn).toHaveBeenCalledTimes(1); // Phase 2: Await the promise to get the result const result = await promise; @@ -138,8 +144,11 @@ describe("WaitForCondition Handler Two-Phase Execution", () => { }); executionOrder.push("after-handler-call"); - // Wait for phase 1 to complete - await new Promise((resolve) => setTimeout(resolve, 50)); + // Wait briefly for phase 1 to execute + await new Promise((resolve) => setTimeout(resolve, 10)); + + // Check should have executed before we await + expect(checkFn).toHaveBeenCalled(); executionOrder.push("before-await"); const result = await promise; From 87725e07a4583b20038863b39391917300563a8e Mon Sep 17 00:00:00 2001 From: Pooya Paridel Date: Thu, 20 Nov 2025 13:55:06 -0800 Subject: [PATCH 3/7] refactor(sdk): move waitForCondition validation inside phase 1 promise Moved parameter validation inside the async phase 1 promise to match the pattern used by wait-for-callback-handler. This makes validation errors async rejections instead of synchronous throws, providing consistency across handlers. Changes: - Moved validation logic inside phase 1 promise (async) - Updated test to use await expect(...).rejects.toThrow() for async rejection - Matches wait-for-callback pattern for consistency - All 704 tests pass --- .../wait-for-condition-handler.test.ts | 6 +- .../wait-for-condition-handler.ts | 59 ++++++++++--------- 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler.test.ts b/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler.test.ts index 2aa11a57..066bd136 100644 --- a/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler.test.ts +++ b/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler.test.ts @@ -126,12 +126,12 @@ describe("WaitForCondition Handler", () => { }); }); - it("should throw error if config is missing", () => { + it("should throw error if config is missing", async () => { const checkFunc: WaitForConditionCheckFunc = jest.fn(); - expect(() => + await expect( waitForConditionHandler(checkFunc, undefined as any), - ).toThrow( + ).rejects.toThrow( "waitForCondition requires config with waitStrategy and initialState", ); }); diff --git a/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler.ts b/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler.ts index 594b6257..f145d5c0 100644 --- a/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler.ts +++ b/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler.ts @@ -89,34 +89,6 @@ export const createWaitForConditionHandler = ( checkOrConfig?: WaitForConditionCheckFunc | WaitForConditionConfig, maybeConfig?: WaitForConditionConfig, ): DurablePromise => { - let name: string | undefined; - let check: WaitForConditionCheckFunc; - let config: WaitForConditionConfig; - - // Parse overloaded parameters - if (typeof nameOrCheck === "string" || nameOrCheck === undefined) { - name = nameOrCheck; - check = checkOrConfig as WaitForConditionCheckFunc; - config = maybeConfig as WaitForConditionConfig; - } else { - check = nameOrCheck; - config = checkOrConfig as WaitForConditionConfig; - } - - if (!config || !config.waitStrategy || config.initialState === undefined) { - throw new Error( - "waitForCondition requires config with waitStrategy and initialState", - ); - } - - const stepId = createStepId(); - - log("🔄", "Running waitForCondition:", { - stepId, - name, - config, - }); - // Two-phase execution: Phase 1 starts immediately, Phase 2 returns result when awaited let phase1Result: T | undefined; let phase1Error: unknown; @@ -129,6 +101,37 @@ export const createWaitForConditionHandler = ( // Phase 1: Start execution immediately and capture result/error const phase1Promise = (async (): Promise => { + let name: string | undefined; + let check: WaitForConditionCheckFunc; + let config: WaitForConditionConfig; + + // Parse overloaded parameters - validation errors thrown here are async + if (typeof nameOrCheck === "string" || nameOrCheck === undefined) { + name = nameOrCheck; + check = checkOrConfig as WaitForConditionCheckFunc; + config = maybeConfig as WaitForConditionConfig; + } else { + check = nameOrCheck; + config = checkOrConfig as WaitForConditionConfig; + } + + if ( + !config || + !config.waitStrategy || + config.initialState === undefined + ) { + throw new Error( + "waitForCondition requires config with waitStrategy and initialState", + ); + } + + const stepId = createStepId(); + + log("🔄", "Running waitForCondition:", { + stepId, + name, + config, + }); // Main waitForCondition logic - can be re-executed if step status changes while (true) { try { From b15dd265dd6cfaa5c0912840de827879d6661694 Mon Sep 17 00:00:00 2001 From: Pooya Paridel Date: Thu, 20 Nov 2025 15:09:57 -0800 Subject: [PATCH 4/7] test(examples): add retry logic for beta backend issues Configure Jest to automatically retry failed integration tests up to 2 times before marking them as failures. This helps with beta backend issues until we use production for integration tests. Changes: - Added jest.setup.integration.js with jest.retryTimes(2) - Updated jest.config.integration.js to use setupFilesAfterEnv - Logs errors before each retry for debugging --- .../jest.config.integration.js | 2 ++ .../jest.setup.integration.js | 3 +++ 2 files changed, 5 insertions(+) create mode 100644 packages/aws-durable-execution-sdk-js-examples/jest.setup.integration.js diff --git a/packages/aws-durable-execution-sdk-js-examples/jest.config.integration.js b/packages/aws-durable-execution-sdk-js-examples/jest.config.integration.js index 55df3546..e784a33b 100644 --- a/packages/aws-durable-execution-sdk-js-examples/jest.config.integration.js +++ b/packages/aws-durable-execution-sdk-js-examples/jest.config.integration.js @@ -7,4 +7,6 @@ module.exports = { ...defaultPreset, testMatch: ["**/src/examples/**/*.test.ts"], testTimeout: 90000, + // Setup file to configure retries for flaky integration tests + setupFilesAfterEnv: ["/jest.setup.integration.js"], }; diff --git a/packages/aws-durable-execution-sdk-js-examples/jest.setup.integration.js b/packages/aws-durable-execution-sdk-js-examples/jest.setup.integration.js new file mode 100644 index 00000000..814284e2 --- /dev/null +++ b/packages/aws-durable-execution-sdk-js-examples/jest.setup.integration.js @@ -0,0 +1,3 @@ +// Configure Jest to retry failed tests up to 2 times +// This helps with beta backend issues until we use production for integration tests +jest.retryTimes(2, { logErrorsBeforeRetry: true }); From 8bad6a1edfaaad8829383c8ebf32b3dc205852c2 Mon Sep 17 00:00:00 2001 From: Pooya Paridel Date: Thu, 20 Nov 2025 19:00:19 -0800 Subject: [PATCH 5/7] Revert "test(examples): add retry logic for beta backend issues" This reverts commit b15dd265dd6cfaa5c0912840de827879d6661694. --- .../jest.config.integration.js | 2 -- .../jest.setup.integration.js | 3 --- 2 files changed, 5 deletions(-) delete mode 100644 packages/aws-durable-execution-sdk-js-examples/jest.setup.integration.js diff --git a/packages/aws-durable-execution-sdk-js-examples/jest.config.integration.js b/packages/aws-durable-execution-sdk-js-examples/jest.config.integration.js index e784a33b..55df3546 100644 --- a/packages/aws-durable-execution-sdk-js-examples/jest.config.integration.js +++ b/packages/aws-durable-execution-sdk-js-examples/jest.config.integration.js @@ -7,6 +7,4 @@ module.exports = { ...defaultPreset, testMatch: ["**/src/examples/**/*.test.ts"], testTimeout: 90000, - // Setup file to configure retries for flaky integration tests - setupFilesAfterEnv: ["/jest.setup.integration.js"], }; diff --git a/packages/aws-durable-execution-sdk-js-examples/jest.setup.integration.js b/packages/aws-durable-execution-sdk-js-examples/jest.setup.integration.js deleted file mode 100644 index 814284e2..00000000 --- a/packages/aws-durable-execution-sdk-js-examples/jest.setup.integration.js +++ /dev/null @@ -1,3 +0,0 @@ -// Configure Jest to retry failed tests up to 2 times -// This helps with beta backend issues until we use production for integration tests -jest.retryTimes(2, { logErrorsBeforeRetry: true }); From 79f5f76eb95df21d6cd93556c45980d14479cb7b Mon Sep 17 00:00:00 2001 From: Pooya Paridel Date: Thu, 20 Nov 2025 19:22:08 -0800 Subject: [PATCH 6/7] fix(sdk): resolve eslint warnings in two-phase tests Fixed unused variable warnings: - Removed unused DurableContext import in run-in-child-context-handler-two-phase.test.ts - Prefixed unused state parameters with underscore in wait-for-condition-handler-two-phase.test.ts --- .../run-in-child-context-handler-two-phase.test.ts | 2 +- .../wait-for-condition-handler-two-phase.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/aws-durable-execution-sdk-js/src/handlers/run-in-child-context-handler/run-in-child-context-handler-two-phase.test.ts b/packages/aws-durable-execution-sdk-js/src/handlers/run-in-child-context-handler/run-in-child-context-handler-two-phase.test.ts index 53bfe64d..d04cf03b 100644 --- a/packages/aws-durable-execution-sdk-js/src/handlers/run-in-child-context-handler/run-in-child-context-handler-two-phase.test.ts +++ b/packages/aws-durable-execution-sdk-js/src/handlers/run-in-child-context-handler/run-in-child-context-handler-two-phase.test.ts @@ -1,5 +1,5 @@ import { createRunInChildContextHandler } from "./run-in-child-context-handler"; -import { ExecutionContext, DurableContext } from "../../types"; +import { ExecutionContext } from "../../types"; import { DurablePromise } from "../../types/durable-promise"; import { Context } from "aws-lambda"; diff --git a/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler-two-phase.test.ts b/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler-two-phase.test.ts index beb8e5bb..5c9f1b0b 100644 --- a/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler-two-phase.test.ts +++ b/packages/aws-durable-execution-sdk-js/src/handlers/wait-for-condition-handler/wait-for-condition-handler-two-phase.test.ts @@ -64,7 +64,7 @@ describe("WaitForCondition Handler Two-Phase Execution", () => { // Phase 1: Create the promise - this executes the logic immediately const promise = waitForConditionHandler(checkFn, { initialState: 0, - waitStrategy: (state) => ({ shouldContinue: false }), + waitStrategy: (_state) => ({ shouldContinue: false }), }); // Should return a DurablePromise @@ -101,7 +101,7 @@ describe("WaitForCondition Handler Two-Phase Execution", () => { // Phase 1: Create the promise const promise = waitForConditionHandler(checkFn, { initialState: "initial", - waitStrategy: (state) => ({ shouldContinue: false }), + waitStrategy: (_state) => ({ shouldContinue: false }), }); // Wait briefly for phase 1 to execute @@ -140,7 +140,7 @@ describe("WaitForCondition Handler Two-Phase Execution", () => { executionOrder.push("promise-created"); const promise = waitForConditionHandler(checkFn, { initialState: 0, - waitStrategy: (state) => ({ shouldContinue: false }), + waitStrategy: (_state) => ({ shouldContinue: false }), }); executionOrder.push("after-handler-call"); From 512cc6712a928863657fa1c613529fa72d92f7ca Mon Sep 17 00:00:00 2001 From: Pooya Paridel Date: Thu, 20 Nov 2025 21:18:13 -0800 Subject: [PATCH 7/7] fix(sdk): use withDurableModeManagement for all DurablePromise operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated all operations that return DurablePromise to use withDurableModeManagement instead of withModeManagement. This ensures proper mode management for two-phase execution across all durable operations. Changes: - invoke: Promise → DurablePromise, use withDurableModeManagement - wait: Promise → DurablePromise, use withDurableModeManagement - waitForCallback: Promise → DurablePromise, use withDurableModeManagement - waitForCondition: Promise → DurablePromise, use withDurableModeManagement - runInChildContext: Promise → DurablePromise, use withDurableModeManagement All 704 tests pass. --- .../durable-context/durable-context.ts | 57 ++++++++----------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/packages/aws-durable-execution-sdk-js/src/context/durable-context/durable-context.ts b/packages/aws-durable-execution-sdk-js/src/context/durable-context/durable-context.ts index ec517cf0..4c8f80e7 100644 --- a/packages/aws-durable-execution-sdk-js/src/context/durable-context/durable-context.ts +++ b/packages/aws-durable-execution-sdk-js/src/context/durable-context/durable-context.ts @@ -237,13 +237,13 @@ export class DurableContextImpl implements DurableContext { funcIdOrInput?: string | I, inputOrConfig?: I | InvokeConfig, maybeConfig?: InvokeConfig, - ): Promise { + ): DurablePromise { validateContextUsage( this._stepPrefix, "invoke", this.executionContext.terminationManager, ); - return this.withModeManagement(() => { + return this.withDurableModeManagement(() => { const invokeHandler = createInvokeHandler( this.executionContext, this.checkpoint, @@ -268,13 +268,13 @@ export class DurableContextImpl implements DurableContext { nameOrFn: string | undefined | ChildFunc, fnOrOptions?: ChildFunc | ChildConfig, maybeOptions?: ChildConfig, - ): Promise { + ): DurablePromise { validateContextUsage( this._stepPrefix, "runInChildContext", this.executionContext.terminationManager, ); - return this.withModeManagement(() => { + return this.withDurableModeManagement(() => { const blockHandler = createRunInChildContextHandler( this.executionContext, this.checkpoint, @@ -284,23 +284,20 @@ export class DurableContextImpl implements DurableContext { createDurableContext, this._parentId, ); - const promise = blockHandler(nameOrFn, fnOrOptions, maybeOptions); - // Prevent unhandled promise rejections - promise?.catch(() => {}); - return promise; + return blockHandler(nameOrFn, fnOrOptions, maybeOptions); }); } wait( nameOrDuration: string | Duration, maybeDuration?: Duration, - ): Promise { + ): DurablePromise { validateContextUsage( this._stepPrefix, "wait", this.executionContext.terminationManager, ); - return this.withModeManagement(() => { + return this.withDurableModeManagement(() => { const waitHandler = createWaitHandler( this.executionContext, this.checkpoint, @@ -367,27 +364,22 @@ export class DurableContextImpl implements DurableContext { nameOrSubmitter?: string | undefined | WaitForCallbackSubmitterFunc, submitterOrConfig?: WaitForCallbackSubmitterFunc | WaitForCallbackConfig, maybeConfig?: WaitForCallbackConfig, - ): Promise { + ): DurablePromise { validateContextUsage( this._stepPrefix, "waitForCallback", this.executionContext.terminationManager, ); - return this.withModeManagement(() => { + return this.withDurableModeManagement(() => { const waitForCallbackHandler = createWaitForCallbackHandler( this.executionContext, this.runInChildContext.bind(this), ); - const promise = waitForCallbackHandler( + return waitForCallbackHandler( nameOrSubmitter!, submitterOrConfig, maybeConfig, ); - // Prevent unhandled promise rejections - promise?.catch(() => {}); - return promise?.finally(() => { - this.checkAndUpdateReplayMode(); - }); }); } @@ -397,13 +389,13 @@ export class DurableContextImpl implements DurableContext { | WaitForConditionCheckFunc | WaitForConditionConfig, maybeConfig?: WaitForConditionConfig, - ): Promise { + ): DurablePromise { validateContextUsage( this._stepPrefix, "waitForCondition", this.executionContext.terminationManager, ); - return this.withModeManagement(() => { + return this.withDurableModeManagement(() => { const waitForConditionHandler = createWaitForConditionHandler( this.executionContext, this.checkpoint, @@ -416,20 +408,17 @@ export class DurableContextImpl implements DurableContext { this._parentId, ); - const promise = - typeof nameOrCheckFunc === "string" || nameOrCheckFunc === undefined - ? waitForConditionHandler( - nameOrCheckFunc, - checkFuncOrConfig as WaitForConditionCheckFunc, - maybeConfig!, - ) - : waitForConditionHandler( - nameOrCheckFunc, - checkFuncOrConfig as WaitForConditionConfig, - ); - // Prevent unhandled promise rejections - promise?.catch(() => {}); - return promise; + return typeof nameOrCheckFunc === "string" || + nameOrCheckFunc === undefined + ? waitForConditionHandler( + nameOrCheckFunc, + checkFuncOrConfig as WaitForConditionCheckFunc, + maybeConfig!, + ) + : waitForConditionHandler( + nameOrCheckFunc, + checkFuncOrConfig as WaitForConditionConfig, + ); }); }