@aws/durable-execution-sdk-js 0.0.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +412 -0
- package/dist/index.mjs +4771 -0
- package/dist/index.mjs.map +1 -0
- package/dist-cjs/index.js +4789 -0
- package/dist-cjs/index.js.map +1 -0
- package/dist-types/context/durable-context/durable-context.d.ts +76 -0
- package/dist-types/context/durable-context/durable-context.d.ts.map +1 -0
- package/dist-types/context/durable-context/durable-context.integration.test.d.ts +2 -0
- package/dist-types/context/durable-context/durable-context.integration.test.d.ts.map +1 -0
- package/dist-types/context/durable-context/durable-context.unit.test.d.ts +2 -0
- package/dist-types/context/durable-context/durable-context.unit.test.d.ts.map +1 -0
- package/dist-types/context/durable-context/logger-mode-aware.test.d.ts +2 -0
- package/dist-types/context/durable-context/logger-mode-aware.test.d.ts.map +1 -0
- package/dist-types/context/durable-context/logger-property.test.d.ts +2 -0
- package/dist-types/context/durable-context/logger-property.test.d.ts.map +1 -0
- package/dist-types/context/durable-context/mode-management/mode-management.d.ts +13 -0
- package/dist-types/context/durable-context/mode-management/mode-management.d.ts.map +1 -0
- package/dist-types/context/durable-context/mode-management/mode-management.test.d.ts +2 -0
- package/dist-types/context/durable-context/mode-management/mode-management.test.d.ts.map +1 -0
- package/dist-types/context/execution-context/execution-context.d.ts +9 -0
- package/dist-types/context/execution-context/execution-context.d.ts.map +1 -0
- package/dist-types/context/execution-context/execution-context.test.d.ts +2 -0
- package/dist-types/context/execution-context/execution-context.test.d.ts.map +1 -0
- package/dist-types/durable-execution-api-client/durable-execution-api-client-caching.test.d.ts +2 -0
- package/dist-types/durable-execution-api-client/durable-execution-api-client-caching.test.d.ts.map +1 -0
- package/dist-types/durable-execution-api-client/durable-execution-api-client.d.ts +29 -0
- package/dist-types/durable-execution-api-client/durable-execution-api-client.d.ts.map +1 -0
- package/dist-types/durable-execution-api-client/durable-execution-api-client.test.d.ts +2 -0
- package/dist-types/durable-execution-api-client/durable-execution-api-client.test.d.ts.map +1 -0
- package/dist-types/errors/callback-error/callback-error.test.d.ts +2 -0
- package/dist-types/errors/callback-error/callback-error.test.d.ts.map +1 -0
- package/dist-types/errors/checkpoint-errors/checkpoint-errors.d.ts +21 -0
- package/dist-types/errors/checkpoint-errors/checkpoint-errors.d.ts.map +1 -0
- package/dist-types/errors/checkpoint-errors/checkpoint-errors.test.d.ts +2 -0
- package/dist-types/errors/checkpoint-errors/checkpoint-errors.test.d.ts.map +1 -0
- package/dist-types/errors/durable-error/durable-error-coverage.test.d.ts +2 -0
- package/dist-types/errors/durable-error/durable-error-coverage.test.d.ts.map +1 -0
- package/dist-types/errors/durable-error/durable-error.d.ts +55 -0
- package/dist-types/errors/durable-error/durable-error.d.ts.map +1 -0
- package/dist-types/errors/durable-error/durable-error.test.d.ts +2 -0
- package/dist-types/errors/durable-error/durable-error.test.d.ts.map +1 -0
- package/dist-types/errors/durable-error/error-determinism.integration.test.d.ts +2 -0
- package/dist-types/errors/durable-error/error-determinism.integration.test.d.ts.map +1 -0
- package/dist-types/errors/non-deterministic-error/non-deterministic-error.d.ts +10 -0
- package/dist-types/errors/non-deterministic-error/non-deterministic-error.d.ts.map +1 -0
- package/dist-types/errors/serdes-errors/serdes-errors.d.ts +27 -0
- package/dist-types/errors/serdes-errors/serdes-errors.d.ts.map +1 -0
- package/dist-types/errors/serdes-errors/serdes-errors.test.d.ts +2 -0
- package/dist-types/errors/serdes-errors/serdes-errors.test.d.ts.map +1 -0
- package/dist-types/errors/step-errors/step-errors.d.ts +8 -0
- package/dist-types/errors/step-errors/step-errors.d.ts.map +1 -0
- package/dist-types/errors/unrecoverable-error/unrecoverable-error.d.ts +41 -0
- package/dist-types/errors/unrecoverable-error/unrecoverable-error.d.ts.map +1 -0
- package/dist-types/errors/unrecoverable-error/unrecoverable-error.test.d.ts +2 -0
- package/dist-types/errors/unrecoverable-error/unrecoverable-error.test.d.ts.map +1 -0
- package/dist-types/handlers/callback-handler/callback-promise.d.ts +5 -0
- package/dist-types/handlers/callback-handler/callback-promise.d.ts.map +1 -0
- package/dist-types/handlers/callback-handler/callback-promise.test.d.ts +2 -0
- package/dist-types/handlers/callback-handler/callback-promise.test.d.ts.map +1 -0
- package/dist-types/handlers/callback-handler/callback.d.ts +7 -0
- package/dist-types/handlers/callback-handler/callback.d.ts.map +1 -0
- package/dist-types/handlers/callback-handler/callback.test.d.ts +2 -0
- package/dist-types/handlers/callback-handler/callback.test.d.ts.map +1 -0
- package/dist-types/handlers/concurrent-execution-handler/batch-result.d.ts +35 -0
- package/dist-types/handlers/concurrent-execution-handler/batch-result.d.ts.map +1 -0
- package/dist-types/handlers/concurrent-execution-handler/batch-result.test.d.ts +2 -0
- package/dist-types/handlers/concurrent-execution-handler/batch-result.test.d.ts.map +1 -0
- package/dist-types/handlers/concurrent-execution-handler/concurrent-execution-handler-two-phase.test.d.ts +2 -0
- package/dist-types/handlers/concurrent-execution-handler/concurrent-execution-handler-two-phase.test.d.ts.map +1 -0
- package/dist-types/handlers/concurrent-execution-handler/concurrent-execution-handler.d.ts +12 -0
- package/dist-types/handlers/concurrent-execution-handler/concurrent-execution-handler.d.ts.map +1 -0
- package/dist-types/handlers/concurrent-execution-handler/concurrent-execution-handler.integration.test.d.ts +2 -0
- package/dist-types/handlers/concurrent-execution-handler/concurrent-execution-handler.integration.test.d.ts.map +1 -0
- package/dist-types/handlers/concurrent-execution-handler/concurrent-execution-handler.replay.test.d.ts +2 -0
- package/dist-types/handlers/concurrent-execution-handler/concurrent-execution-handler.replay.test.d.ts.map +1 -0
- package/dist-types/handlers/concurrent-execution-handler/concurrent-execution-handler.test.d.ts +2 -0
- package/dist-types/handlers/concurrent-execution-handler/concurrent-execution-handler.test.d.ts.map +1 -0
- package/dist-types/handlers/invoke-handler/invoke-handler-two-phase.test.d.ts +2 -0
- package/dist-types/handlers/invoke-handler/invoke-handler-two-phase.test.d.ts.map +1 -0
- package/dist-types/handlers/invoke-handler/invoke-handler.d.ts +8 -0
- package/dist-types/handlers/invoke-handler/invoke-handler.d.ts.map +1 -0
- package/dist-types/handlers/invoke-handler/invoke-handler.test.d.ts +2 -0
- package/dist-types/handlers/invoke-handler/invoke-handler.test.d.ts.map +1 -0
- package/dist-types/handlers/map-handler/map-handler-two-phase.test.d.ts +2 -0
- package/dist-types/handlers/map-handler/map-handler-two-phase.test.d.ts.map +1 -0
- package/dist-types/handlers/map-handler/map-handler.d.ts +3 -0
- package/dist-types/handlers/map-handler/map-handler.d.ts.map +1 -0
- package/dist-types/handlers/map-handler/map-handler.test.d.ts +2 -0
- package/dist-types/handlers/map-handler/map-handler.test.d.ts.map +1 -0
- package/dist-types/handlers/parallel-handler/parallel-handler-two-phase.test.d.ts +2 -0
- package/dist-types/handlers/parallel-handler/parallel-handler-two-phase.test.d.ts.map +1 -0
- package/dist-types/handlers/parallel-handler/parallel-handler.d.ts +3 -0
- package/dist-types/handlers/parallel-handler/parallel-handler.d.ts.map +1 -0
- package/dist-types/handlers/parallel-handler/parallel-handler.test.d.ts +2 -0
- package/dist-types/handlers/parallel-handler/parallel-handler.test.d.ts.map +1 -0
- package/dist-types/handlers/promise-handler/promise-handler.d.ts +8 -0
- package/dist-types/handlers/promise-handler/promise-handler.d.ts.map +1 -0
- package/dist-types/handlers/promise-handler/promise-handler.test.d.ts +2 -0
- package/dist-types/handlers/promise-handler/promise-handler.test.d.ts.map +1 -0
- package/dist-types/handlers/run-in-child-context-handler/run-in-child-context-handler-two-phase.test.d.ts +2 -0
- package/dist-types/handlers/run-in-child-context-handler/run-in-child-context-handler-two-phase.test.d.ts.map +1 -0
- package/dist-types/handlers/run-in-child-context-handler/run-in-child-context-handler.d.ts +10 -0
- package/dist-types/handlers/run-in-child-context-handler/run-in-child-context-handler.d.ts.map +1 -0
- package/dist-types/handlers/run-in-child-context-handler/run-in-child-context-handler.test.d.ts +2 -0
- package/dist-types/handlers/run-in-child-context-handler/run-in-child-context-handler.test.d.ts.map +1 -0
- package/dist-types/handlers/run-in-child-context-handler/run-in-child-context-integration.test.d.ts +2 -0
- package/dist-types/handlers/run-in-child-context-handler/run-in-child-context-integration.test.d.ts.map +1 -0
- package/dist-types/handlers/step-handler/step-handler-two-phase.test.d.ts +2 -0
- package/dist-types/handlers/step-handler/step-handler-two-phase.test.d.ts.map +1 -0
- package/dist-types/handlers/step-handler/step-handler.d.ts +14 -0
- package/dist-types/handlers/step-handler/step-handler.d.ts.map +1 -0
- package/dist-types/handlers/step-handler/step-handler.test.d.ts +2 -0
- package/dist-types/handlers/step-handler/step-handler.test.d.ts.map +1 -0
- package/dist-types/handlers/step-handler/step-handler.timing.test.d.ts +2 -0
- package/dist-types/handlers/step-handler/step-handler.timing.test.d.ts.map +1 -0
- package/dist-types/handlers/wait-for-callback-handler/wait-for-callback-handler.d.ts +3 -0
- package/dist-types/handlers/wait-for-callback-handler/wait-for-callback-handler.d.ts.map +1 -0
- package/dist-types/handlers/wait-for-callback-handler/wait-for-callback-handler.test.d.ts +2 -0
- package/dist-types/handlers/wait-for-callback-handler/wait-for-callback-handler.test.d.ts.map +1 -0
- package/dist-types/handlers/wait-for-condition-handler/wait-for-condition-handler-two-phase.test.d.ts +2 -0
- package/dist-types/handlers/wait-for-condition-handler/wait-for-condition-handler-two-phase.test.d.ts.map +1 -0
- package/dist-types/handlers/wait-for-condition-handler/wait-for-condition-handler.d.ts +10 -0
- package/dist-types/handlers/wait-for-condition-handler/wait-for-condition-handler.d.ts.map +1 -0
- package/dist-types/handlers/wait-for-condition-handler/wait-for-condition-handler.test.d.ts +2 -0
- package/dist-types/handlers/wait-for-condition-handler/wait-for-condition-handler.test.d.ts.map +1 -0
- package/dist-types/handlers/wait-for-condition-handler/wait-for-condition-handler.timing.test.d.ts +2 -0
- package/dist-types/handlers/wait-for-condition-handler/wait-for-condition-handler.timing.test.d.ts.map +1 -0
- package/dist-types/handlers/wait-handler/wait-handler-two-phase.test.d.ts +2 -0
- package/dist-types/handlers/wait-handler/wait-handler-two-phase.test.d.ts.map +1 -0
- package/dist-types/handlers/wait-handler/wait-handler.d.ts +9 -0
- package/dist-types/handlers/wait-handler/wait-handler.d.ts.map +1 -0
- package/dist-types/handlers/wait-handler/wait-handler.test.d.ts +2 -0
- package/dist-types/handlers/wait-handler/wait-handler.test.d.ts.map +1 -0
- package/dist-types/index.d.ts +12 -0
- package/dist-types/index.d.ts.map +1 -0
- package/dist-types/run-durable.d.ts +2 -0
- package/dist-types/run-durable.d.ts.map +1 -0
- package/dist-types/termination-manager/termination-manager-checkpoint.test.d.ts +2 -0
- package/dist-types/termination-manager/termination-manager-checkpoint.test.d.ts.map +1 -0
- package/dist-types/termination-manager/termination-manager.d.ts +15 -0
- package/dist-types/termination-manager/termination-manager.d.ts.map +1 -0
- package/dist-types/termination-manager/termination-manager.test.d.ts +2 -0
- package/dist-types/termination-manager/termination-manager.test.d.ts.map +1 -0
- package/dist-types/termination-manager/types.d.ts +26 -0
- package/dist-types/termination-manager/types.d.ts.map +1 -0
- package/dist-types/testing/create-test-checkpoint-manager.d.ts +5 -0
- package/dist-types/testing/create-test-checkpoint-manager.d.ts.map +1 -0
- package/dist-types/testing/create-test-durable-context.d.ts +32 -0
- package/dist-types/testing/create-test-durable-context.d.ts.map +1 -0
- package/dist-types/testing/mock-batch-result.d.ts +26 -0
- package/dist-types/testing/mock-batch-result.d.ts.map +1 -0
- package/dist-types/testing/mock-checkpoint-manager.d.ts +21 -0
- package/dist-types/testing/mock-checkpoint-manager.d.ts.map +1 -0
- package/dist-types/testing/mock-checkpoint.d.ts +12 -0
- package/dist-types/testing/mock-checkpoint.d.ts.map +1 -0
- package/dist-types/testing/mock-context.d.ts +10 -0
- package/dist-types/testing/mock-context.d.ts.map +1 -0
- package/dist-types/testing/test-constants.d.ts +59 -0
- package/dist-types/testing/test-constants.d.ts.map +1 -0
- package/dist-types/types/batch.d.ts +177 -0
- package/dist-types/types/batch.d.ts.map +1 -0
- package/dist-types/types/callback.d.ts +48 -0
- package/dist-types/types/callback.d.ts.map +1 -0
- package/dist-types/types/child-context.d.ts +24 -0
- package/dist-types/types/child-context.d.ts.map +1 -0
- package/dist-types/types/core.d.ts +315 -0
- package/dist-types/types/core.d.ts.map +1 -0
- package/dist-types/types/durable-context.d.ts +667 -0
- package/dist-types/types/durable-context.d.ts.map +1 -0
- package/dist-types/types/durable-execution.d.ts +192 -0
- package/dist-types/types/durable-execution.d.ts.map +1 -0
- package/dist-types/types/durable-logger.d.ts +69 -0
- package/dist-types/types/durable-logger.d.ts.map +1 -0
- package/dist-types/types/durable-promise.d.ts +80 -0
- package/dist-types/types/durable-promise.d.ts.map +1 -0
- package/dist-types/types/index.d.ts +13 -0
- package/dist-types/types/index.d.ts.map +1 -0
- package/dist-types/types/invoke.d.ts +12 -0
- package/dist-types/types/invoke.d.ts.map +1 -0
- package/dist-types/types/logger.d.ts +63 -0
- package/dist-types/types/logger.d.ts.map +1 -0
- package/dist-types/types/step.d.ts +76 -0
- package/dist-types/types/step.d.ts.map +1 -0
- package/dist-types/types/wait-condition.d.ts +46 -0
- package/dist-types/types/wait-condition.d.ts.map +1 -0
- package/dist-types/utils/checkpoint/checkpoint-ancestor-checking.test.d.ts +2 -0
- package/dist-types/utils/checkpoint/checkpoint-ancestor-checking.test.d.ts.map +1 -0
- package/dist-types/utils/checkpoint/checkpoint-error-classification.test.d.ts +2 -0
- package/dist-types/utils/checkpoint/checkpoint-error-classification.test.d.ts.map +1 -0
- package/dist-types/utils/checkpoint/checkpoint-helper.d.ts +10 -0
- package/dist-types/utils/checkpoint/checkpoint-helper.d.ts.map +1 -0
- package/dist-types/utils/checkpoint/checkpoint-integration.test.d.ts +2 -0
- package/dist-types/utils/checkpoint/checkpoint-integration.test.d.ts.map +1 -0
- package/dist-types/utils/checkpoint/checkpoint-manager.d.ts +53 -0
- package/dist-types/utils/checkpoint/checkpoint-manager.d.ts.map +1 -0
- package/dist-types/utils/checkpoint/checkpoint-queue-completion.test.d.ts +2 -0
- package/dist-types/utils/checkpoint/checkpoint-queue-completion.test.d.ts.map +1 -0
- package/dist-types/utils/checkpoint/checkpoint-stepdata-update.test.d.ts +2 -0
- package/dist-types/utils/checkpoint/checkpoint-stepdata-update.test.d.ts.map +1 -0
- package/dist-types/utils/checkpoint/checkpoint-termination.test.d.ts +2 -0
- package/dist-types/utils/checkpoint/checkpoint-termination.test.d.ts.map +1 -0
- package/dist-types/utils/checkpoint/checkpoint.test.d.ts +2 -0
- package/dist-types/utils/checkpoint/checkpoint.test.d.ts.map +1 -0
- package/dist-types/utils/constants/constants.d.ts +10 -0
- package/dist-types/utils/constants/constants.d.ts.map +1 -0
- package/dist-types/utils/context-tracker/context-tracker.d.ts +13 -0
- package/dist-types/utils/context-tracker/context-tracker.d.ts.map +1 -0
- package/dist-types/utils/context-tracker/context-tracker.test.d.ts +2 -0
- package/dist-types/utils/context-tracker/context-tracker.test.d.ts.map +1 -0
- package/dist-types/utils/durable-execution-invocation-input/durable-execution-invocation-input.d.ts +20 -0
- package/dist-types/utils/durable-execution-invocation-input/durable-execution-invocation-input.d.ts.map +1 -0
- package/dist-types/utils/duration/duration.d.ts +8 -0
- package/dist-types/utils/duration/duration.d.ts.map +1 -0
- package/dist-types/utils/duration/duration.test.d.ts +2 -0
- package/dist-types/utils/duration/duration.test.d.ts.map +1 -0
- package/dist-types/utils/error-object/error-object-coverage.test.d.ts +2 -0
- package/dist-types/utils/error-object/error-object-coverage.test.d.ts.map +1 -0
- package/dist-types/utils/error-object/error-object.d.ts +3 -0
- package/dist-types/utils/error-object/error-object.d.ts.map +1 -0
- package/dist-types/utils/error-object/error-object.test.d.ts +2 -0
- package/dist-types/utils/error-object/error-object.test.d.ts.map +1 -0
- package/dist-types/utils/logger/default-logger.d.ts +51 -0
- package/dist-types/utils/logger/default-logger.d.ts.map +1 -0
- package/dist-types/utils/logger/default-logger.test.d.ts +2 -0
- package/dist-types/utils/logger/default-logger.test.d.ts.map +1 -0
- package/dist-types/utils/logger/logger.d.ts +2 -0
- package/dist-types/utils/logger/logger.d.ts.map +1 -0
- package/dist-types/utils/logger/logger.test.d.ts +2 -0
- package/dist-types/utils/logger/logger.test.d.ts.map +1 -0
- package/dist-types/utils/logger/structured-logger-integration.test.d.ts +2 -0
- package/dist-types/utils/logger/structured-logger-integration.test.d.ts.map +1 -0
- package/dist-types/utils/replay-validation/replay-validation.d.ts +8 -0
- package/dist-types/utils/replay-validation/replay-validation.d.ts.map +1 -0
- package/dist-types/utils/replay-validation/replay-validation.test.d.ts +2 -0
- package/dist-types/utils/replay-validation/replay-validation.test.d.ts.map +1 -0
- package/dist-types/utils/retry/retry-config/index.d.ts +167 -0
- package/dist-types/utils/retry/retry-config/index.d.ts.map +1 -0
- package/dist-types/utils/retry/retry-config/index.test.d.ts +2 -0
- package/dist-types/utils/retry/retry-config/index.test.d.ts.map +1 -0
- package/dist-types/utils/retry/retry-presets/retry-presets.d.ts +35 -0
- package/dist-types/utils/retry/retry-presets/retry-presets.d.ts.map +1 -0
- package/dist-types/utils/safe-stringify/safe-stringify.d.ts +2 -0
- package/dist-types/utils/safe-stringify/safe-stringify.d.ts.map +1 -0
- package/dist-types/utils/safe-stringify/safe-stringify.test.d.ts +2 -0
- package/dist-types/utils/safe-stringify/safe-stringify.test.d.ts.map +1 -0
- package/dist-types/utils/serdes/serdes.d.ts +152 -0
- package/dist-types/utils/serdes/serdes.d.ts.map +1 -0
- package/dist-types/utils/serdes/serdes.test.d.ts +2 -0
- package/dist-types/utils/serdes/serdes.test.d.ts.map +1 -0
- package/dist-types/utils/step-id-utils/step-id-utils.d.ts +16 -0
- package/dist-types/utils/step-id-utils/step-id-utils.d.ts.map +1 -0
- package/dist-types/utils/step-id-utils/step-id-utils.test.d.ts +2 -0
- package/dist-types/utils/step-id-utils/step-id-utils.test.d.ts.map +1 -0
- package/dist-types/utils/summary-generators/summary-generators.d.ts +10 -0
- package/dist-types/utils/summary-generators/summary-generators.d.ts.map +1 -0
- package/dist-types/utils/summary-generators/summary-generators.test.d.ts +2 -0
- package/dist-types/utils/summary-generators/summary-generators.test.d.ts.map +1 -0
- package/dist-types/utils/termination-helper/active-operations-tracker.d.ts +31 -0
- package/dist-types/utils/termination-helper/active-operations-tracker.d.ts.map +1 -0
- package/dist-types/utils/termination-helper/active-operations-tracker.test.d.ts +2 -0
- package/dist-types/utils/termination-helper/active-operations-tracker.test.d.ts.map +1 -0
- package/dist-types/utils/termination-helper/termination-deferral.test.d.ts +2 -0
- package/dist-types/utils/termination-helper/termination-deferral.test.d.ts.map +1 -0
- package/dist-types/utils/termination-helper/termination-helper.d.ts +20 -0
- package/dist-types/utils/termination-helper/termination-helper.d.ts.map +1 -0
- package/dist-types/utils/termination-helper/termination-helper.test.d.ts +2 -0
- package/dist-types/utils/termination-helper/termination-helper.test.d.ts.map +1 -0
- package/dist-types/utils/wait-before-continue/wait-before-continue.d.ts +35 -0
- package/dist-types/utils/wait-before-continue/wait-before-continue.d.ts.map +1 -0
- package/dist-types/utils/wait-before-continue/wait-before-continue.test.d.ts +2 -0
- package/dist-types/utils/wait-before-continue/wait-before-continue.test.d.ts.map +1 -0
- package/dist-types/utils/wait-strategy/wait-strategy-config.d.ts +19 -0
- package/dist-types/utils/wait-strategy/wait-strategy-config.d.ts.map +1 -0
- package/dist-types/utils/wait-strategy/wait-strategy-config.test.d.ts +2 -0
- package/dist-types/utils/wait-strategy/wait-strategy-config.test.d.ts.map +1 -0
- package/dist-types/with-durable-execution-queue-completion.test.d.ts +2 -0
- package/dist-types/with-durable-execution-queue-completion.test.d.ts.map +1 -0
- package/dist-types/with-durable-execution.d.ts +75 -0
- package/dist-types/with-durable-execution.d.ts.map +1 -0
- package/dist-types/with-durable-execution.test.d.ts +2 -0
- package/dist-types/with-durable-execution.test.d.ts.map +1 -0
- package/package.json +64 -3
|
@@ -0,0 +1,4789 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var clientLambda = require('@aws-sdk/client-lambda');
|
|
4
|
+
var events = require('events');
|
|
5
|
+
var async_hooks = require('async_hooks');
|
|
6
|
+
var crypto = require('crypto');
|
|
7
|
+
var node_console = require('node:console');
|
|
8
|
+
var util = require('node:util');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
var DurableExecutionMode;
|
|
14
|
+
(function (DurableExecutionMode) {
|
|
15
|
+
DurableExecutionMode["ExecutionMode"] = "ExecutionMode";
|
|
16
|
+
DurableExecutionMode["ReplayMode"] = "ReplayMode";
|
|
17
|
+
DurableExecutionMode["ReplaySucceededContext"] = "ReplaySucceededContext";
|
|
18
|
+
})(DurableExecutionMode || (DurableExecutionMode = {}));
|
|
19
|
+
/**
|
|
20
|
+
* Status enumeration for durable execution invocation results.
|
|
21
|
+
*
|
|
22
|
+
* This enum defines the possible outcomes of a durable execution invocation,
|
|
23
|
+
* indicating whether the execution completed successfully, failed, or is
|
|
24
|
+
* continuing asynchronously.
|
|
25
|
+
*
|
|
26
|
+
* The status determines how the AWS durable execution service will handle
|
|
27
|
+
* the execution:
|
|
28
|
+
* - SUCCEEDED: Execution completed successfully with a final result
|
|
29
|
+
* - FAILED: Execution failed with an error that cannot be retried
|
|
30
|
+
* - PENDING: Execution is continuing and will be resumed later (checkpointed)
|
|
31
|
+
*
|
|
32
|
+
* @public
|
|
33
|
+
*/
|
|
34
|
+
exports.InvocationStatus = void 0;
|
|
35
|
+
(function (InvocationStatus) {
|
|
36
|
+
/**
|
|
37
|
+
* The durable execution completed successfully.
|
|
38
|
+
*
|
|
39
|
+
* This status indicates:
|
|
40
|
+
* - A final result is available (if any)
|
|
41
|
+
* - No further invocations are needed
|
|
42
|
+
* - The execution has reached its natural completion
|
|
43
|
+
*/
|
|
44
|
+
InvocationStatus["SUCCEEDED"] = "SUCCEEDED";
|
|
45
|
+
/**
|
|
46
|
+
* The durable execution failed with an unrecoverable error.
|
|
47
|
+
*
|
|
48
|
+
* This status indicates:
|
|
49
|
+
* - An error occurred that cannot be automatically retried
|
|
50
|
+
* - Error details are provided in the response
|
|
51
|
+
* - No further invocations will occur
|
|
52
|
+
*/
|
|
53
|
+
InvocationStatus["FAILED"] = "FAILED";
|
|
54
|
+
/**
|
|
55
|
+
* The durable execution is continuing asynchronously.
|
|
56
|
+
*
|
|
57
|
+
* This status indicates:
|
|
58
|
+
* - Execution was checkpointed and will resume later
|
|
59
|
+
* - Common scenarios: waiting for callbacks, retries, wait operations
|
|
60
|
+
* - The function may terminate while execution continues
|
|
61
|
+
* - Future invocations will resume from the checkpoint
|
|
62
|
+
*/
|
|
63
|
+
InvocationStatus["PENDING"] = "PENDING";
|
|
64
|
+
})(exports.InvocationStatus || (exports.InvocationStatus = {}));
|
|
65
|
+
/**
|
|
66
|
+
* Operation subtype enumeration for categorizing different types of durable operations.
|
|
67
|
+
*
|
|
68
|
+
* This enum provides fine-grained classification of durable operations beyond the
|
|
69
|
+
* basic operation types. Subtypes enable improved observability for specific
|
|
70
|
+
* operation patterns.
|
|
71
|
+
*
|
|
72
|
+
* Each subtype corresponds to a specific durable context method or execution pattern.
|
|
73
|
+
*
|
|
74
|
+
* @public
|
|
75
|
+
*/
|
|
76
|
+
exports.OperationSubType = void 0;
|
|
77
|
+
(function (OperationSubType) {
|
|
78
|
+
/**
|
|
79
|
+
* A durable step operation (`context.step`).
|
|
80
|
+
*
|
|
81
|
+
* Represents atomic operations with automatic retry and checkpointing.
|
|
82
|
+
* Steps are the fundamental building blocks of durable executions.
|
|
83
|
+
*/
|
|
84
|
+
OperationSubType["STEP"] = "Step";
|
|
85
|
+
/**
|
|
86
|
+
* A wait operation (`context.wait`).
|
|
87
|
+
*
|
|
88
|
+
* Represents time-based delays that pause execution for a specified duration.
|
|
89
|
+
* Waits allow long-running workflows without keeping invocations active.
|
|
90
|
+
*/
|
|
91
|
+
OperationSubType["WAIT"] = "Wait";
|
|
92
|
+
/**
|
|
93
|
+
* A callback creation operation (`context.createCallback`).
|
|
94
|
+
*
|
|
95
|
+
* Represents the creation of a callback that external systems can complete.
|
|
96
|
+
* Used for human-in-the-loop workflows and external system integration.
|
|
97
|
+
*/
|
|
98
|
+
OperationSubType["CALLBACK"] = "Callback";
|
|
99
|
+
/**
|
|
100
|
+
* A child context operation (`context.runInChildContext`).
|
|
101
|
+
*
|
|
102
|
+
* Represents execution within an isolated child context with its own
|
|
103
|
+
* step counter and state tracking. Used for grouping related operations.
|
|
104
|
+
*/
|
|
105
|
+
OperationSubType["RUN_IN_CHILD_CONTEXT"] = "RunInChildContext";
|
|
106
|
+
/**
|
|
107
|
+
* A map operation (`context.map`).
|
|
108
|
+
*
|
|
109
|
+
* Represents parallel processing of an array of items with concurrency control
|
|
110
|
+
* and completion policies. Each map operation coordinates multiple iterations.
|
|
111
|
+
*/
|
|
112
|
+
OperationSubType["MAP"] = "Map";
|
|
113
|
+
/**
|
|
114
|
+
* An individual iteration within a map operation.
|
|
115
|
+
*
|
|
116
|
+
* Represents the processing of a single item within a `context.map` call.
|
|
117
|
+
* Each iteration runs in its own child context with isolated state.
|
|
118
|
+
*/
|
|
119
|
+
OperationSubType["MAP_ITERATION"] = "MapIteration";
|
|
120
|
+
/**
|
|
121
|
+
* A parallel execution operation (`context.parallel`).
|
|
122
|
+
*
|
|
123
|
+
* Represents concurrent execution of multiple branches with optional
|
|
124
|
+
* concurrency control and completion policies.
|
|
125
|
+
*/
|
|
126
|
+
OperationSubType["PARALLEL"] = "Parallel";
|
|
127
|
+
/**
|
|
128
|
+
* An individual branch within a parallel operation.
|
|
129
|
+
*
|
|
130
|
+
* Represents a single branch of execution within a `context.parallel` call.
|
|
131
|
+
* Each branch runs in its own child context with isolated state.
|
|
132
|
+
*/
|
|
133
|
+
OperationSubType["PARALLEL_BRANCH"] = "ParallelBranch";
|
|
134
|
+
/**
|
|
135
|
+
* A wait for callback operation (`context.waitForCallback`).
|
|
136
|
+
*
|
|
137
|
+
* Represents waiting for an external system to complete a callback,
|
|
138
|
+
* combining callback creation with submission logic.
|
|
139
|
+
*/
|
|
140
|
+
OperationSubType["WAIT_FOR_CALLBACK"] = "WaitForCallback";
|
|
141
|
+
/**
|
|
142
|
+
* A wait for condition operation (`context.waitForCondition`).
|
|
143
|
+
*
|
|
144
|
+
* Represents periodic checking of a condition until it's met,
|
|
145
|
+
* with configurable polling intervals and wait strategies.
|
|
146
|
+
*/
|
|
147
|
+
OperationSubType["WAIT_FOR_CONDITION"] = "WaitForCondition";
|
|
148
|
+
/**
|
|
149
|
+
* A chained invocation operation (`context.invoke`).
|
|
150
|
+
*
|
|
151
|
+
* Represents calling another durable function with input parameters
|
|
152
|
+
* and waiting for its completion. Used for function composition and workflows.
|
|
153
|
+
*/
|
|
154
|
+
OperationSubType["CHAINED_INVOKE"] = "ChainedInvoke";
|
|
155
|
+
})(exports.OperationSubType || (exports.OperationSubType = {}));
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Log level supported by the durable logger
|
|
159
|
+
* @public
|
|
160
|
+
*/
|
|
161
|
+
var DurableLogLevel;
|
|
162
|
+
(function (DurableLogLevel) {
|
|
163
|
+
DurableLogLevel["INFO"] = "INFO";
|
|
164
|
+
DurableLogLevel["WARN"] = "WARN";
|
|
165
|
+
DurableLogLevel["ERROR"] = "ERROR";
|
|
166
|
+
DurableLogLevel["DEBUG"] = "DEBUG";
|
|
167
|
+
})(DurableLogLevel || (DurableLogLevel = {}));
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @public
|
|
171
|
+
*/
|
|
172
|
+
exports.StepSemantics = void 0;
|
|
173
|
+
(function (StepSemantics) {
|
|
174
|
+
StepSemantics["AtMostOncePerRetry"] = "AT_MOST_ONCE_PER_RETRY";
|
|
175
|
+
StepSemantics["AtLeastOncePerRetry"] = "AT_LEAST_ONCE_PER_RETRY";
|
|
176
|
+
})(exports.StepSemantics || (exports.StepSemantics = {}));
|
|
177
|
+
/**
|
|
178
|
+
* Jitter strategy for retry delays to prevent thundering herd. Jitter reduces simultaneous retry attempts
|
|
179
|
+
* by spreading retries out over a randomized delay interval.
|
|
180
|
+
*
|
|
181
|
+
* @public
|
|
182
|
+
*/
|
|
183
|
+
exports.JitterStrategy = void 0;
|
|
184
|
+
(function (JitterStrategy) {
|
|
185
|
+
/** No jitter - use exact calculated delay */
|
|
186
|
+
JitterStrategy["NONE"] = "NONE";
|
|
187
|
+
/** Full jitter - random delay between 0 and calculated delay */
|
|
188
|
+
JitterStrategy["FULL"] = "FULL";
|
|
189
|
+
/** Half jitter - random delay between 50% and 100% of calculated delay */
|
|
190
|
+
JitterStrategy["HALF"] = "HALF";
|
|
191
|
+
})(exports.JitterStrategy || (exports.JitterStrategy = {}));
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The status of a batch item
|
|
195
|
+
* @public
|
|
196
|
+
*/
|
|
197
|
+
exports.BatchItemStatus = void 0;
|
|
198
|
+
(function (BatchItemStatus) {
|
|
199
|
+
BatchItemStatus["SUCCEEDED"] = "SUCCEEDED";
|
|
200
|
+
BatchItemStatus["FAILED"] = "FAILED";
|
|
201
|
+
BatchItemStatus["STARTED"] = "STARTED";
|
|
202
|
+
})(exports.BatchItemStatus || (exports.BatchItemStatus = {}));
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* A promise that defers execution until it's awaited or .then/.catch/.finally is called
|
|
206
|
+
*
|
|
207
|
+
* @public
|
|
208
|
+
*/
|
|
209
|
+
class DurablePromise {
|
|
210
|
+
/**
|
|
211
|
+
* The actual promise instance, created only when execution begins.
|
|
212
|
+
* Starts as null and remains null until the DurablePromise is first awaited
|
|
213
|
+
* or chained (.then/.catch/.finally). Once created, it holds the running
|
|
214
|
+
* promise returned by the _executor function.
|
|
215
|
+
*
|
|
216
|
+
* Example lifecycle:
|
|
217
|
+
* ```typescript
|
|
218
|
+
* const dp = new DurablePromise(() => fetch('/api')); // _promise = null
|
|
219
|
+
* console.log(dp.isExecuted); // false
|
|
220
|
+
*
|
|
221
|
+
* const result = await dp; // NOW _promise = fetch('/api') promise
|
|
222
|
+
* console.log(dp.isExecuted); // true
|
|
223
|
+
* ```
|
|
224
|
+
*
|
|
225
|
+
* This lazy initialization prevents the executor from running until needed.
|
|
226
|
+
*/
|
|
227
|
+
_promise = null;
|
|
228
|
+
/**
|
|
229
|
+
* Function that contains the deferred execution logic.
|
|
230
|
+
* This function is NOT called when the DurablePromise is created - it's only
|
|
231
|
+
* executed when the promise is first awaited or chained (.then/.catch/.finally).
|
|
232
|
+
*
|
|
233
|
+
* Example:
|
|
234
|
+
* ```typescript
|
|
235
|
+
* const durablePromise = new DurablePromise(async () => {
|
|
236
|
+
* console.log("This runs ONLY when awaited, not when created");
|
|
237
|
+
* return await someAsyncOperation();
|
|
238
|
+
* });
|
|
239
|
+
*
|
|
240
|
+
* // At this point, nothing has executed yet
|
|
241
|
+
* console.log("Promise created but not executed");
|
|
242
|
+
*
|
|
243
|
+
* // NOW the executor function runs
|
|
244
|
+
* const result = await durablePromise;
|
|
245
|
+
* ```
|
|
246
|
+
*/
|
|
247
|
+
_executor;
|
|
248
|
+
/** Flag indicating whether the promise has been executed (awaited or chained) */
|
|
249
|
+
_isExecuted = false;
|
|
250
|
+
/**
|
|
251
|
+
* Creates a new DurablePromise
|
|
252
|
+
* @param executor - Function containing the deferred execution logic
|
|
253
|
+
*/
|
|
254
|
+
constructor(executor) {
|
|
255
|
+
this._executor = executor;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Ensures the promise is executed, creating the actual promise if needed
|
|
259
|
+
* @returns The underlying promise instance
|
|
260
|
+
*/
|
|
261
|
+
ensureExecution() {
|
|
262
|
+
if (!this._promise) {
|
|
263
|
+
this._isExecuted = true;
|
|
264
|
+
// Execute the promise
|
|
265
|
+
this._promise = this._executor();
|
|
266
|
+
}
|
|
267
|
+
return this._promise;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Attaches callbacks for the resolution and/or rejection of the Promise
|
|
271
|
+
* Triggers execution if not already started
|
|
272
|
+
*/
|
|
273
|
+
then(onfulfilled, onrejected) {
|
|
274
|
+
return this.ensureExecution().then(onfulfilled, onrejected);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Attaches a callback for only the rejection of the Promise
|
|
278
|
+
* Triggers execution if not already started
|
|
279
|
+
*/
|
|
280
|
+
catch(onrejected) {
|
|
281
|
+
return this.ensureExecution().catch(onrejected);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected)
|
|
285
|
+
* Triggers execution if not already started
|
|
286
|
+
*/
|
|
287
|
+
finally(onfinally) {
|
|
288
|
+
return this.ensureExecution().finally(onfinally);
|
|
289
|
+
}
|
|
290
|
+
/** Returns the string tag for the promise type */
|
|
291
|
+
get [Symbol.toStringTag]() {
|
|
292
|
+
return "DurablePromise";
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Check if the promise has been executed (awaited or had .then/.catch/.finally called)
|
|
296
|
+
* @returns true if execution has started, false otherwise
|
|
297
|
+
*/
|
|
298
|
+
get isExecuted() {
|
|
299
|
+
return this._isExecuted;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Converts a Duration object to total seconds
|
|
305
|
+
* @param duration - Duration object with at least one time unit specified
|
|
306
|
+
* @returns Total duration in seconds
|
|
307
|
+
*/
|
|
308
|
+
function durationToSeconds(duration) {
|
|
309
|
+
const days = "days" in duration ? (duration.days ?? 0) : 0;
|
|
310
|
+
const hours = "hours" in duration ? (duration.hours ?? 0) : 0;
|
|
311
|
+
const minutes = "minutes" in duration ? (duration.minutes ?? 0) : 0;
|
|
312
|
+
const seconds = "seconds" in duration ? (duration.seconds ?? 0) : 0;
|
|
313
|
+
return days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60 + seconds;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const safeStringify = (data) => {
|
|
317
|
+
try {
|
|
318
|
+
const seen = new WeakSet();
|
|
319
|
+
return JSON.stringify(data, (key, value) => {
|
|
320
|
+
if (typeof value === "object" && value !== null) {
|
|
321
|
+
if (seen.has(value))
|
|
322
|
+
return "[Circular]";
|
|
323
|
+
seen.add(value);
|
|
324
|
+
// Handle Error objects by extracting their properties
|
|
325
|
+
if (value instanceof Error) {
|
|
326
|
+
return {
|
|
327
|
+
...value,
|
|
328
|
+
name: value.name,
|
|
329
|
+
message: value.message,
|
|
330
|
+
stack: value.stack,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return value;
|
|
335
|
+
}, 2);
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
return "[Unable to stringify]";
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
/* eslint-disable no-console */
|
|
343
|
+
const log = (emoji, message, data) => {
|
|
344
|
+
if (process.env.DURABLE_VERBOSE_MODE === "true") {
|
|
345
|
+
console.debug(`${emoji} ${message}`, data ? safeStringify(data) : "");
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
var TerminationReason;
|
|
350
|
+
(function (TerminationReason) {
|
|
351
|
+
// Default termination reason
|
|
352
|
+
TerminationReason["OPERATION_TERMINATED"] = "OPERATION_TERMINATED";
|
|
353
|
+
// Retry-related reasons
|
|
354
|
+
TerminationReason["RETRY_SCHEDULED"] = "RETRY_SCHEDULED";
|
|
355
|
+
TerminationReason["RETRY_INTERRUPTED_STEP"] = "RETRY_INTERRUPTED_STEP";
|
|
356
|
+
// Wait-related reasons
|
|
357
|
+
TerminationReason["WAIT_SCHEDULED"] = "WAIT_SCHEDULED";
|
|
358
|
+
// Callback-related reasons
|
|
359
|
+
TerminationReason["CALLBACK_PENDING"] = "CALLBACK_PENDING";
|
|
360
|
+
// Error-related reasons
|
|
361
|
+
TerminationReason["CHECKPOINT_FAILED"] = "CHECKPOINT_FAILED";
|
|
362
|
+
TerminationReason["SERDES_FAILED"] = "SERDES_FAILED";
|
|
363
|
+
TerminationReason["CONTEXT_VALIDATION_ERROR"] = "CONTEXT_VALIDATION_ERROR";
|
|
364
|
+
// Custom reason
|
|
365
|
+
TerminationReason["CUSTOM"] = "CUSTOM";
|
|
366
|
+
})(TerminationReason || (TerminationReason = {}));
|
|
367
|
+
|
|
368
|
+
const asyncLocalStorage = new async_hooks.AsyncLocalStorage();
|
|
369
|
+
const getActiveContext = () => {
|
|
370
|
+
return asyncLocalStorage.getStore();
|
|
371
|
+
};
|
|
372
|
+
const runWithContext = (contextId, parentId, fn, attempt, durableExecutionMode) => {
|
|
373
|
+
return asyncLocalStorage.run({ contextId, parentId, attempt, durableExecutionMode }, fn);
|
|
374
|
+
};
|
|
375
|
+
const validateContextUsage = (operationContextId, operationName, terminationManager) => {
|
|
376
|
+
const contextId = operationContextId || "root";
|
|
377
|
+
const activeContext = getActiveContext();
|
|
378
|
+
if (!activeContext) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (activeContext.contextId !== contextId) {
|
|
382
|
+
const errorMessage = `Context usage error in "${operationName}": You are using a parent or sibling context instead of the current child context. Expected context ID: "${activeContext.contextId}", but got: "${operationContextId}". When inside runInChildContext(), you must use the child context parameter, not the parent context.`;
|
|
383
|
+
terminationManager.terminate({
|
|
384
|
+
reason: TerminationReason.CONTEXT_VALIDATION_ERROR,
|
|
385
|
+
message: errorMessage,
|
|
386
|
+
error: new Error(errorMessage),
|
|
387
|
+
});
|
|
388
|
+
// Only call termination manager, don't throw or return promise
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const HASH_LENGTH = 16;
|
|
393
|
+
/**
|
|
394
|
+
* Creates an MD5 hash of the input string for better performance than SHA-256
|
|
395
|
+
* @param input - The string to hash
|
|
396
|
+
* @returns The truncated hexadecimal hash string
|
|
397
|
+
*/
|
|
398
|
+
const hashId = (input) => {
|
|
399
|
+
return crypto.createHash("md5")
|
|
400
|
+
.update(input)
|
|
401
|
+
.digest("hex")
|
|
402
|
+
.substring(0, HASH_LENGTH);
|
|
403
|
+
};
|
|
404
|
+
/**
|
|
405
|
+
* Helper function to get step data using the original stepId
|
|
406
|
+
* This function handles the hashing internally so callers don't need to worry about it
|
|
407
|
+
* @param stepData - The stepData record from context
|
|
408
|
+
* @param stepId - The original stepId (will be hashed internally)
|
|
409
|
+
* @returns The operation data or undefined if not found
|
|
410
|
+
*/
|
|
411
|
+
const getStepData = (stepData, stepId) => {
|
|
412
|
+
const hashedId = hashId(stepId);
|
|
413
|
+
return stepData[hashedId];
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Checks if any ancestor operation in the parent chain has finished (SUCCEEDED or FAILED)
|
|
418
|
+
* or has a pending completion checkpoint
|
|
419
|
+
*/
|
|
420
|
+
function hasFinishedAncestor(context, parentId) {
|
|
421
|
+
if (!parentId) {
|
|
422
|
+
log("🔍", "hasFinishedAncestor: No parentId provided");
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
// First check if any ancestor has a pending completion checkpoint
|
|
426
|
+
if (hasPendingAncestorCompletion(context, parentId)) {
|
|
427
|
+
log("🔍", "hasFinishedAncestor: Found ancestor with pending completion!", {
|
|
428
|
+
parentId,
|
|
429
|
+
});
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
let currentHashedId = hashId(parentId);
|
|
433
|
+
log("🔍", "hasFinishedAncestor: Starting check", {
|
|
434
|
+
parentId,
|
|
435
|
+
initialHashedId: currentHashedId,
|
|
436
|
+
});
|
|
437
|
+
while (currentHashedId) {
|
|
438
|
+
const parentOperation = context._stepData[currentHashedId];
|
|
439
|
+
log("🔍", "hasFinishedAncestor: Checking operation", {
|
|
440
|
+
hashedId: currentHashedId,
|
|
441
|
+
hasOperation: !!parentOperation,
|
|
442
|
+
status: parentOperation?.Status,
|
|
443
|
+
type: parentOperation?.Type,
|
|
444
|
+
});
|
|
445
|
+
if (parentOperation?.Status === clientLambda.OperationStatus.SUCCEEDED ||
|
|
446
|
+
parentOperation?.Status === clientLambda.OperationStatus.FAILED) {
|
|
447
|
+
log("🔍", "hasFinishedAncestor: Found finished ancestor!", {
|
|
448
|
+
hashedId: currentHashedId,
|
|
449
|
+
status: parentOperation.Status,
|
|
450
|
+
});
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
currentHashedId = parentOperation?.ParentId;
|
|
454
|
+
}
|
|
455
|
+
log("🔍", "hasFinishedAncestor: No finished ancestor found");
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Checks if any ancestor has a pending completion checkpoint
|
|
460
|
+
*/
|
|
461
|
+
function hasPendingAncestorCompletion(context, stepId) {
|
|
462
|
+
let currentHashedId = hashId(stepId);
|
|
463
|
+
while (currentHashedId) {
|
|
464
|
+
if (context.pendingCompletions.has(currentHashedId)) {
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
const operation = context._stepData[currentHashedId];
|
|
468
|
+
currentHashedId = operation?.ParentId;
|
|
469
|
+
}
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Terminates execution and returns a never-resolving promise to prevent code progression
|
|
474
|
+
* @param context - The execution context containing the termination manager
|
|
475
|
+
* @param reason - The termination reason
|
|
476
|
+
* @param message - The termination message
|
|
477
|
+
* @returns A never-resolving promise
|
|
478
|
+
*/
|
|
479
|
+
function terminate(context, reason, message) {
|
|
480
|
+
const activeContext = getActiveContext();
|
|
481
|
+
// If we have a parent context, add delay to let checkpoints process
|
|
482
|
+
if (activeContext?.parentId) {
|
|
483
|
+
return new Promise(async (_resolve, _reject) => {
|
|
484
|
+
// Wait a tick to let any pending checkpoints start processing
|
|
485
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
486
|
+
log("🔍", "Terminate called - checking context:", {
|
|
487
|
+
hasActiveContext: !!activeContext,
|
|
488
|
+
contextId: activeContext?.contextId,
|
|
489
|
+
parentId: activeContext?.parentId,
|
|
490
|
+
reason,
|
|
491
|
+
message,
|
|
492
|
+
});
|
|
493
|
+
const ancestorFinished = hasFinishedAncestor(context, activeContext.parentId);
|
|
494
|
+
log("🔍", "Ancestor check result:", {
|
|
495
|
+
parentId: activeContext.parentId,
|
|
496
|
+
ancestorFinished,
|
|
497
|
+
});
|
|
498
|
+
if (ancestorFinished) {
|
|
499
|
+
log("🛑", "Skipping termination - ancestor already finished:", {
|
|
500
|
+
contextId: activeContext.contextId,
|
|
501
|
+
parentId: activeContext.parentId,
|
|
502
|
+
reason,
|
|
503
|
+
message,
|
|
504
|
+
});
|
|
505
|
+
// Return never-resolving promise without terminating
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
// Check if there are active operations before terminating
|
|
509
|
+
const tracker = context.activeOperationsTracker;
|
|
510
|
+
if (tracker && tracker.hasActive()) {
|
|
511
|
+
log("⏳", "Deferring termination - active operations in progress:", {
|
|
512
|
+
activeCount: tracker.getCount(),
|
|
513
|
+
reason,
|
|
514
|
+
message,
|
|
515
|
+
});
|
|
516
|
+
// Wait for operations to complete, then terminate
|
|
517
|
+
const checkInterval = setInterval(() => {
|
|
518
|
+
if (!tracker.hasActive()) {
|
|
519
|
+
clearInterval(checkInterval);
|
|
520
|
+
log("✅", "Active operations completed, proceeding with termination:", {
|
|
521
|
+
reason,
|
|
522
|
+
message,
|
|
523
|
+
});
|
|
524
|
+
context.terminationManager.terminate({
|
|
525
|
+
reason,
|
|
526
|
+
message,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}, 10);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
// No active operations, terminate immediately
|
|
533
|
+
context.terminationManager.terminate({
|
|
534
|
+
reason,
|
|
535
|
+
message,
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
// No parent context - check active operations and terminate
|
|
540
|
+
const tracker = context.activeOperationsTracker;
|
|
541
|
+
if (tracker && tracker.hasActive()) {
|
|
542
|
+
log("⏳", "Deferring termination - active operations in progress:", {
|
|
543
|
+
activeCount: tracker.getCount(),
|
|
544
|
+
reason,
|
|
545
|
+
message,
|
|
546
|
+
});
|
|
547
|
+
return new Promise((_resolve, _reject) => {
|
|
548
|
+
const checkInterval = setInterval(() => {
|
|
549
|
+
if (!tracker.hasActive()) {
|
|
550
|
+
clearInterval(checkInterval);
|
|
551
|
+
log("✅", "Active operations completed, proceeding with termination:", {
|
|
552
|
+
reason,
|
|
553
|
+
message,
|
|
554
|
+
});
|
|
555
|
+
context.terminationManager.terminate({
|
|
556
|
+
reason,
|
|
557
|
+
message,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}, 10);
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
// No parent, no active operations - terminate immediately
|
|
564
|
+
context.terminationManager.terminate({
|
|
565
|
+
reason,
|
|
566
|
+
message,
|
|
567
|
+
});
|
|
568
|
+
return new Promise(() => { });
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Terminates execution for unrecoverable errors and returns a never-resolving promise
|
|
572
|
+
* @param context - The execution context containing the termination manager
|
|
573
|
+
* @param error - The unrecoverable error that caused termination
|
|
574
|
+
* @param stepIdentifier - The step name or ID for error messaging
|
|
575
|
+
* @returns A never-resolving promise
|
|
576
|
+
*/
|
|
577
|
+
function terminateForUnrecoverableError(context, error, stepIdentifier) {
|
|
578
|
+
return terminate(context, error.terminationReason, `Unrecoverable error in step ${stepIdentifier}: ${error.message}`);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const DEFAULT_CONFIG$1 = {
|
|
582
|
+
maxAttempts: 3,
|
|
583
|
+
initialDelay: { seconds: 5 },
|
|
584
|
+
maxDelay: { minutes: 5 },
|
|
585
|
+
backoffRate: 2,
|
|
586
|
+
jitter: exports.JitterStrategy.FULL,
|
|
587
|
+
retryableErrors: [/.*/], // By default, retry all errors
|
|
588
|
+
retryableErrorTypes: [],
|
|
589
|
+
};
|
|
590
|
+
const applyJitter$1 = (delay, strategy) => {
|
|
591
|
+
switch (strategy) {
|
|
592
|
+
case exports.JitterStrategy.NONE:
|
|
593
|
+
return delay;
|
|
594
|
+
case exports.JitterStrategy.FULL:
|
|
595
|
+
// Random between 0 and delay
|
|
596
|
+
return Math.random() * delay;
|
|
597
|
+
case exports.JitterStrategy.HALF:
|
|
598
|
+
// Random between delay/2 and delay
|
|
599
|
+
return delay / 2 + Math.random() * (delay / 2);
|
|
600
|
+
default:
|
|
601
|
+
return delay;
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
/**
|
|
605
|
+
* Creates a retry strategy function with exponential backoff and configurable jitter
|
|
606
|
+
*
|
|
607
|
+
* @param config - Configuration options for the retry strategy
|
|
608
|
+
* @returns A function that determines whether to retry and calculates delay based on error and attempt count
|
|
609
|
+
*
|
|
610
|
+
* @remarks
|
|
611
|
+
* The returned function takes an error and attempt count, and returns a {@link RetryDecision} indicating
|
|
612
|
+
* whether to retry and the delay before the next attempt.
|
|
613
|
+
*
|
|
614
|
+
* **Delay Calculation:**
|
|
615
|
+
* - Base delay = `initialDelay × backoffRate^(attemptsMade - 1)`
|
|
616
|
+
* - Capped at `maxDelay`
|
|
617
|
+
* - Jitter applied based on `jitter` strategy
|
|
618
|
+
* - Final delay rounded to nearest second, minimum 1 second
|
|
619
|
+
*
|
|
620
|
+
* **Error Filtering:**
|
|
621
|
+
* - If neither `retryableErrors` nor `retryableErrorTypes` is specified: all errors are retried
|
|
622
|
+
* - If either is specified: only matching errors are retried
|
|
623
|
+
* - If both are specified: errors matching either criteria are retried (OR logic)
|
|
624
|
+
*
|
|
625
|
+
* @example
|
|
626
|
+
* ```typescript
|
|
627
|
+
* // Basic usage with defaults (retry all errors, 3 attempts, exponential backoff)
|
|
628
|
+
* const defaultRetry = createRetryStrategy();
|
|
629
|
+
*
|
|
630
|
+
* // Custom retry with more attempts and specific delays
|
|
631
|
+
* const customRetry = createRetryStrategy({
|
|
632
|
+
* maxAttempts: 5,
|
|
633
|
+
* initialDelay: { seconds: 10 },
|
|
634
|
+
* maxDelay: { seconds: 60 },
|
|
635
|
+
* backoffRate: 2,
|
|
636
|
+
* jitter: JitterStrategy.HALF
|
|
637
|
+
* });
|
|
638
|
+
*
|
|
639
|
+
* // Retry only specific error types
|
|
640
|
+
* class TimeoutError extends Error {}
|
|
641
|
+
* const typeBasedRetry = createRetryStrategy({
|
|
642
|
+
* retryableErrorTypes: [TimeoutError]
|
|
643
|
+
* });
|
|
644
|
+
*
|
|
645
|
+
* // Retry only errors matching message patterns
|
|
646
|
+
* const patternBasedRetry = createRetryStrategy({
|
|
647
|
+
* retryableErrors: [/timeout/i, /connection/i, "rate limit"]
|
|
648
|
+
* });
|
|
649
|
+
*
|
|
650
|
+
* // Combine error types and patterns
|
|
651
|
+
* const combinedRetry = createRetryStrategy({
|
|
652
|
+
* retryableErrorTypes: [TimeoutError],
|
|
653
|
+
* retryableErrors: [/network/i]
|
|
654
|
+
* });
|
|
655
|
+
*
|
|
656
|
+
* // Use in step configuration
|
|
657
|
+
* await context.step('api-call', async () => {
|
|
658
|
+
* return await callExternalAPI();
|
|
659
|
+
* }, { retryStrategy: customRetry });
|
|
660
|
+
* ```
|
|
661
|
+
*
|
|
662
|
+
* @see {@link RetryStrategyConfig} for configuration options
|
|
663
|
+
* @see {@link JitterStrategy} for jitter strategies
|
|
664
|
+
* @see {@link RetryDecision} for return type
|
|
665
|
+
*
|
|
666
|
+
* @public
|
|
667
|
+
*/
|
|
668
|
+
const createRetryStrategy = (config = {}) => {
|
|
669
|
+
// Only apply default retryableErrors if user didn't specify either filter
|
|
670
|
+
const shouldUseDefaultErrors = config.retryableErrors === undefined &&
|
|
671
|
+
config.retryableErrorTypes === undefined;
|
|
672
|
+
const finalConfig = {
|
|
673
|
+
...DEFAULT_CONFIG$1,
|
|
674
|
+
...config,
|
|
675
|
+
retryableErrors: config.retryableErrors ?? (shouldUseDefaultErrors ? [/.*/] : []),
|
|
676
|
+
};
|
|
677
|
+
return (error, attemptsMade) => {
|
|
678
|
+
// Check if we've exceeded max attempts
|
|
679
|
+
if (attemptsMade >= finalConfig.maxAttempts) {
|
|
680
|
+
return { shouldRetry: false };
|
|
681
|
+
}
|
|
682
|
+
// Check if error is retryable based on error message
|
|
683
|
+
const isRetryableErrorMessage = finalConfig.retryableErrors.some((pattern) => {
|
|
684
|
+
if (pattern instanceof RegExp) {
|
|
685
|
+
return pattern.test(error.message);
|
|
686
|
+
}
|
|
687
|
+
return error.message.includes(pattern);
|
|
688
|
+
});
|
|
689
|
+
// Check if error is retryable based on error type
|
|
690
|
+
const isRetryableErrorType = finalConfig.retryableErrorTypes.some((ErrorType) => error instanceof ErrorType);
|
|
691
|
+
if (!isRetryableErrorMessage && !isRetryableErrorType) {
|
|
692
|
+
return { shouldRetry: false };
|
|
693
|
+
}
|
|
694
|
+
// Calculate delay with exponential backoff
|
|
695
|
+
const initialDelaySeconds = durationToSeconds(finalConfig.initialDelay);
|
|
696
|
+
const maxDelaySeconds = durationToSeconds(finalConfig.maxDelay);
|
|
697
|
+
const baseDelay = Math.min(initialDelaySeconds * Math.pow(finalConfig.backoffRate, attemptsMade - 1), maxDelaySeconds);
|
|
698
|
+
// Apply jitter
|
|
699
|
+
const delayWithJitter = applyJitter$1(baseDelay, finalConfig.jitter);
|
|
700
|
+
// Ensure delay is an integer >= 1
|
|
701
|
+
const finalDelay = Math.max(1, Math.round(delayWithJitter));
|
|
702
|
+
return { shouldRetry: true, delay: { seconds: finalDelay } };
|
|
703
|
+
};
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Pre-configured retry strategies for common use cases
|
|
708
|
+
* @example
|
|
709
|
+
* ```typescript
|
|
710
|
+
* // Use default retry preset (3 attempts with exponential backoff)
|
|
711
|
+
* await context.step('my-step', async () => {
|
|
712
|
+
* return await someOperation();
|
|
713
|
+
* }, { retryStrategy: retryPresets.default });
|
|
714
|
+
*
|
|
715
|
+
* // Use no-retry preset (fail immediately on error)
|
|
716
|
+
* await context.step('critical-step', async () => {
|
|
717
|
+
* return await criticalOperation();
|
|
718
|
+
* }, { retryStrategy: retryPresets.noRetry });
|
|
719
|
+
* ```
|
|
720
|
+
*
|
|
721
|
+
* @public
|
|
722
|
+
*/
|
|
723
|
+
const retryPresets = {
|
|
724
|
+
/**
|
|
725
|
+
* Default retry strategy with exponential backoff
|
|
726
|
+
* - 6 total attempts (1 initial + 5 retries)
|
|
727
|
+
* - Initial delay: 5 seconds
|
|
728
|
+
* - Max delay: 60 seconds
|
|
729
|
+
* - Backoff rate: 2x
|
|
730
|
+
* - Jitter: FULL (randomizes delay between 0 and calculated delay)
|
|
731
|
+
* - Total max wait time less than 150 seconds (2:30)
|
|
732
|
+
*/
|
|
733
|
+
default: createRetryStrategy({
|
|
734
|
+
maxAttempts: 6,
|
|
735
|
+
initialDelay: { seconds: 5 },
|
|
736
|
+
maxDelay: { seconds: 60 },
|
|
737
|
+
backoffRate: 2,
|
|
738
|
+
jitter: exports.JitterStrategy.FULL,
|
|
739
|
+
}),
|
|
740
|
+
/**
|
|
741
|
+
* No retry strategy - fails immediately on first error
|
|
742
|
+
* - 1 total attempt (no retries)
|
|
743
|
+
*/
|
|
744
|
+
noRetry: createRetryStrategy({
|
|
745
|
+
maxAttempts: 1,
|
|
746
|
+
}),
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Error thrown when a step with AT_MOST_ONCE_PER_RETRY semantics was started but interrupted
|
|
751
|
+
* before completion.
|
|
752
|
+
*/
|
|
753
|
+
class StepInterruptedError extends Error {
|
|
754
|
+
constructor(_stepId, _stepName) {
|
|
755
|
+
super(`The step execution process was initiated but failed to reach completion due to an interruption.`);
|
|
756
|
+
this.name = "StepInterruptedError";
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Shared constants to avoid circular dependencies
|
|
762
|
+
*/
|
|
763
|
+
const OPERATIONS_COMPLETE_EVENT = "allOperationsComplete";
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Base class for all durable operation errors
|
|
767
|
+
*/
|
|
768
|
+
class DurableOperationError extends Error {
|
|
769
|
+
cause;
|
|
770
|
+
errorData;
|
|
771
|
+
stackTrace;
|
|
772
|
+
constructor(message, cause, errorData) {
|
|
773
|
+
super(message);
|
|
774
|
+
this.name = this.constructor.name;
|
|
775
|
+
this.cause = cause;
|
|
776
|
+
this.errorData = errorData;
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Create DurableOperationError from ErrorObject (for reconstruction during replay)
|
|
780
|
+
*/
|
|
781
|
+
static fromErrorObject(errorObject) {
|
|
782
|
+
const cause = new Error(errorObject.ErrorMessage);
|
|
783
|
+
cause.name = errorObject.ErrorType || "Error";
|
|
784
|
+
cause.stack = errorObject.StackTrace?.join("\n");
|
|
785
|
+
// Determine error type and create appropriate instance
|
|
786
|
+
switch (errorObject.ErrorType) {
|
|
787
|
+
case "StepError":
|
|
788
|
+
return new StepError(errorObject.ErrorMessage || "Step failed", cause, errorObject.ErrorData);
|
|
789
|
+
case "CallbackError":
|
|
790
|
+
return new CallbackError(errorObject.ErrorMessage || "Callback failed", cause, errorObject.ErrorData);
|
|
791
|
+
case "InvokeError":
|
|
792
|
+
return new InvokeError(errorObject.ErrorMessage || "Invoke failed", cause, errorObject.ErrorData);
|
|
793
|
+
case "ChildContextError":
|
|
794
|
+
return new ChildContextError(errorObject.ErrorMessage || "Child context failed", cause, errorObject.ErrorData);
|
|
795
|
+
case "WaitForConditionError":
|
|
796
|
+
return new WaitForConditionError(errorObject.ErrorMessage || "Wait for condition failed", cause, errorObject.ErrorData);
|
|
797
|
+
default:
|
|
798
|
+
return new StepError(errorObject.ErrorMessage || "Unknown error", cause, errorObject.ErrorData);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Convert to ErrorObject for serialization
|
|
803
|
+
*/
|
|
804
|
+
toErrorObject() {
|
|
805
|
+
return {
|
|
806
|
+
ErrorType: this.errorType,
|
|
807
|
+
ErrorMessage: this.message,
|
|
808
|
+
ErrorData: this.errorData,
|
|
809
|
+
StackTrace: undefined,
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Error thrown when a step operation fails
|
|
815
|
+
*/
|
|
816
|
+
class StepError extends DurableOperationError {
|
|
817
|
+
errorType = "StepError";
|
|
818
|
+
constructor(message, cause, errorData) {
|
|
819
|
+
super(message || "Step failed", cause, errorData);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Error thrown when a callback operation fails
|
|
824
|
+
*/
|
|
825
|
+
class CallbackError extends DurableOperationError {
|
|
826
|
+
errorType = "CallbackError";
|
|
827
|
+
constructor(message, cause, errorData) {
|
|
828
|
+
super(message || "Callback failed", cause, errorData);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Error thrown when an invoke operation fails
|
|
833
|
+
*/
|
|
834
|
+
class InvokeError extends DurableOperationError {
|
|
835
|
+
errorType = "InvokeError";
|
|
836
|
+
constructor(message, cause, errorData) {
|
|
837
|
+
super(message || "Invoke failed", cause, errorData);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Error thrown when a child context operation fails
|
|
842
|
+
*/
|
|
843
|
+
class ChildContextError extends DurableOperationError {
|
|
844
|
+
errorType = "ChildContextError";
|
|
845
|
+
constructor(message, cause, errorData) {
|
|
846
|
+
super(message || "Child context failed", cause, errorData);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Error thrown when a wait for condition operation fails
|
|
851
|
+
*/
|
|
852
|
+
class WaitForConditionError extends DurableOperationError {
|
|
853
|
+
errorType = "WaitForConditionError";
|
|
854
|
+
constructor(message, cause, errorData) {
|
|
855
|
+
super(message || "Wait for condition failed", cause, errorData);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Default Serdes implementation using JSON.stringify and JSON.parse
|
|
861
|
+
* Wrapped in Promise.resolve() to maintain async interface compatibility
|
|
862
|
+
* Ignores context parameter since it uses inline JSON serialization
|
|
863
|
+
*
|
|
864
|
+
* Note: Uses 'any' type intentionally as this is a generic serializer that must
|
|
865
|
+
* handle arbitrary JavaScript values. JSON.stringify/parse work with any type,
|
|
866
|
+
* and using more restrictive types would break compatibility with the generic
|
|
867
|
+
* Serdes<T> interface when T can be any type.
|
|
868
|
+
*
|
|
869
|
+
* @public
|
|
870
|
+
*/
|
|
871
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
872
|
+
const defaultSerdes = {
|
|
873
|
+
serialize: async (
|
|
874
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
875
|
+
value, _context) => value !== undefined ? JSON.stringify(value) : undefined,
|
|
876
|
+
deserialize: async (data, _context) => (data !== undefined ? JSON.parse(data) : undefined),
|
|
877
|
+
};
|
|
878
|
+
/**
|
|
879
|
+
* Creates a Serdes for a specific class that preserves the class type. This implementation
|
|
880
|
+
* is a basic class wrapper and does not support any complex class structures. If you need
|
|
881
|
+
* custom serialization, it is recommended to create your own custom serdes.
|
|
882
|
+
*
|
|
883
|
+
* @param cls - The class constructor (must have no required parameters)
|
|
884
|
+
* @returns A Serdes that maintains the class type during serialization/deserialization
|
|
885
|
+
*
|
|
886
|
+
* @example
|
|
887
|
+
* ```typescript
|
|
888
|
+
* class User {
|
|
889
|
+
* name: string = "";
|
|
890
|
+
* age: number = 0;
|
|
891
|
+
*
|
|
892
|
+
* greet() {
|
|
893
|
+
* return `Hello, ${this.name}`;
|
|
894
|
+
* }
|
|
895
|
+
* }
|
|
896
|
+
*
|
|
897
|
+
* const userSerdes = createClassSerdes(User);
|
|
898
|
+
*
|
|
899
|
+
* // In a durable function:
|
|
900
|
+
* const user = await context.step("create-user", async () => {
|
|
901
|
+
* const u = new User();
|
|
902
|
+
* u.name = "Alice";
|
|
903
|
+
* u.age = 30;
|
|
904
|
+
* return u;
|
|
905
|
+
* }, { serdes: userSerdes });
|
|
906
|
+
*
|
|
907
|
+
* console.log(user.greet()); // "Hello, Alice" - methods are preserved
|
|
908
|
+
* ```
|
|
909
|
+
*
|
|
910
|
+
* Limitations:
|
|
911
|
+
* - Class instances becomes plain objects and loses all class information
|
|
912
|
+
* - Constructor must have no parameters
|
|
913
|
+
* - Constructor side-effects will re-run during deserialization
|
|
914
|
+
* - Private fields (#field) cannot be serialized
|
|
915
|
+
* - Getters/setters are not preserved
|
|
916
|
+
* - Nested class instances lose their prototype
|
|
917
|
+
*
|
|
918
|
+
* For classes with Date properties, use createClassSerdesWithDates instead.
|
|
919
|
+
*
|
|
920
|
+
* @beta
|
|
921
|
+
*/
|
|
922
|
+
function createClassSerdes(cls) {
|
|
923
|
+
return {
|
|
924
|
+
serialize: async (value, _context) => value !== undefined ? JSON.stringify(value) : undefined,
|
|
925
|
+
deserialize: async (data, _context) => data !== undefined
|
|
926
|
+
? Object.assign(new cls(), JSON.parse(data))
|
|
927
|
+
: undefined,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Creates a custom Serdes for a class with special handling for Date properties. This implementation
|
|
932
|
+
* is a basic class wrapper and does not support any complex class structures. If you need
|
|
933
|
+
* custom serialization, it is recommended to create your own custom serdes.
|
|
934
|
+
*
|
|
935
|
+
* @param cls - The class constructor (must have no required parameters)
|
|
936
|
+
* @param dateProps - Array of property paths that should be converted to Date objects (supports nested paths like "metadata.createdAt")
|
|
937
|
+
* @returns A Serdes that maintains the class type and converts specified properties to Date objects
|
|
938
|
+
*
|
|
939
|
+
* @example
|
|
940
|
+
* ```typescript
|
|
941
|
+
* class Article {
|
|
942
|
+
* title: string = "";
|
|
943
|
+
* createdAt: Date = new Date();
|
|
944
|
+
* metadata: {
|
|
945
|
+
* publishedAt: Date;
|
|
946
|
+
* updatedAt: Date;
|
|
947
|
+
* } = {
|
|
948
|
+
* publishedAt: new Date(),
|
|
949
|
+
* updatedAt: new Date()
|
|
950
|
+
* };
|
|
951
|
+
*
|
|
952
|
+
* getAge() {
|
|
953
|
+
* return Date.now() - this.createdAt.getTime();
|
|
954
|
+
* }
|
|
955
|
+
* }
|
|
956
|
+
*
|
|
957
|
+
* const articleSerdes = createClassSerdesWithDates(Article, [
|
|
958
|
+
* "createdAt",
|
|
959
|
+
* "metadata.publishedAt",
|
|
960
|
+
* "metadata.updatedAt"
|
|
961
|
+
* ]);
|
|
962
|
+
*
|
|
963
|
+
* // In a durable function:
|
|
964
|
+
* const article = await context.step("create-article", async () => {
|
|
965
|
+
* const a = new Article();
|
|
966
|
+
* a.title = "My Article";
|
|
967
|
+
* return a;
|
|
968
|
+
* }, { serdes: articleSerdes });
|
|
969
|
+
*
|
|
970
|
+
* console.log(article.getAge()); // Works! Dates are properly restored
|
|
971
|
+
* ```
|
|
972
|
+
*
|
|
973
|
+
* Limitations:
|
|
974
|
+
* - Class instances becomes plain objects and loses all class information
|
|
975
|
+
* - Constructor must have no parameters
|
|
976
|
+
* - Constructor side-effects will re-run during deserialization
|
|
977
|
+
* - Private fields (#field) cannot be serialized
|
|
978
|
+
* - Getters/setters are not preserved
|
|
979
|
+
* - Nested class instances lose their prototype
|
|
980
|
+
*
|
|
981
|
+
* For classes with Date properties, use createClassSerdesWithDates instead.
|
|
982
|
+
*
|
|
983
|
+
* @beta
|
|
984
|
+
*/
|
|
985
|
+
function createClassSerdesWithDates(cls, dateProps) {
|
|
986
|
+
return {
|
|
987
|
+
serialize: async (value, _context) => value !== undefined ? JSON.stringify(value) : undefined,
|
|
988
|
+
deserialize: async (data, _context) => {
|
|
989
|
+
if (data === undefined) {
|
|
990
|
+
return undefined;
|
|
991
|
+
}
|
|
992
|
+
const parsed = JSON.parse(data);
|
|
993
|
+
const instance = new cls();
|
|
994
|
+
// Copy all properties from parsed object to the new instance
|
|
995
|
+
Object.assign(instance, parsed);
|
|
996
|
+
// Convert date strings back to Date objects (supports nested paths)
|
|
997
|
+
for (const prop of dateProps) {
|
|
998
|
+
const parts = prop.split(".");
|
|
999
|
+
let obj = instance;
|
|
1000
|
+
// Navigate to parent of target property
|
|
1001
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
1002
|
+
const next = obj[parts[i]];
|
|
1003
|
+
if (!next || typeof next !== "object")
|
|
1004
|
+
break;
|
|
1005
|
+
obj = next;
|
|
1006
|
+
}
|
|
1007
|
+
// Convert to Date if path exists
|
|
1008
|
+
const lastKey = parts[parts.length - 1];
|
|
1009
|
+
if (obj[lastKey]) {
|
|
1010
|
+
obj[lastKey] = new Date(obj[lastKey]);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return instance;
|
|
1014
|
+
},
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Base class for all unrecoverable errors
|
|
1020
|
+
* Any error that inherits from this class indicates a fatal condition
|
|
1021
|
+
*/
|
|
1022
|
+
class UnrecoverableError extends Error {
|
|
1023
|
+
originalError;
|
|
1024
|
+
isUnrecoverable = true;
|
|
1025
|
+
constructor(message, originalError) {
|
|
1026
|
+
super(message);
|
|
1027
|
+
this.originalError = originalError;
|
|
1028
|
+
this.name = this.constructor.name;
|
|
1029
|
+
// Preserve the original stack trace if available
|
|
1030
|
+
if (originalError?.stack) {
|
|
1031
|
+
this.stack = `${this.stack}\nCaused by: ${originalError.stack}`;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Base class for errors that make the entire execution unrecoverable
|
|
1037
|
+
* These errors indicate that the execution cannot continue and should be terminated completely
|
|
1038
|
+
*/
|
|
1039
|
+
class UnrecoverableExecutionError extends UnrecoverableError {
|
|
1040
|
+
isUnrecoverableExecution = true;
|
|
1041
|
+
constructor(message, originalError) {
|
|
1042
|
+
super(`[Unrecoverable Execution] ${message}`, originalError);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Base class for errors that make the current invocation unrecoverable
|
|
1047
|
+
* These errors indicate that the current Lambda invocation should be terminated,
|
|
1048
|
+
* but the execution might be able to continue with a new invocation
|
|
1049
|
+
*/
|
|
1050
|
+
class UnrecoverableInvocationError extends UnrecoverableError {
|
|
1051
|
+
isUnrecoverableInvocation = true;
|
|
1052
|
+
constructor(message, originalError) {
|
|
1053
|
+
super(`[Unrecoverable Invocation] ${message}`, originalError);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Type guard to check if an error is any kind of unrecoverable error
|
|
1058
|
+
*/
|
|
1059
|
+
function isUnrecoverableError(error) {
|
|
1060
|
+
return (error instanceof Error &&
|
|
1061
|
+
"isUnrecoverable" in error &&
|
|
1062
|
+
error.isUnrecoverable === true);
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Type guard to check if an error is an unrecoverable invocation error
|
|
1066
|
+
*/
|
|
1067
|
+
function isUnrecoverableInvocationError(error) {
|
|
1068
|
+
return (error instanceof Error &&
|
|
1069
|
+
"isUnrecoverableInvocation" in error &&
|
|
1070
|
+
error.isUnrecoverableInvocation === true);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Error thrown when serdes operation fails and terminates Lambda invocation
|
|
1075
|
+
* This is used by withDurableExecution to terminate the Lambda when serdes fails
|
|
1076
|
+
*/
|
|
1077
|
+
class SerdesFailedError extends UnrecoverableInvocationError {
|
|
1078
|
+
terminationReason = TerminationReason.SERDES_FAILED;
|
|
1079
|
+
constructor(message, originalError) {
|
|
1080
|
+
super(message || "Serdes operation failed", originalError);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Utility function to safely execute serialization with proper error handling
|
|
1085
|
+
* Instead of throwing unrecoverable errors, this directly terminates execution
|
|
1086
|
+
*/
|
|
1087
|
+
async function safeSerialize(serdes, value, stepId, stepName, terminationManager, durableExecutionArn) {
|
|
1088
|
+
try {
|
|
1089
|
+
const context = {
|
|
1090
|
+
entityId: stepId,
|
|
1091
|
+
durableExecutionArn,
|
|
1092
|
+
};
|
|
1093
|
+
return await serdes.serialize(value, context);
|
|
1094
|
+
}
|
|
1095
|
+
catch (error) {
|
|
1096
|
+
const message = `Serialization failed for step ${stepName ? `"${stepName}" ` : ""}(${stepId}): ${error instanceof Error ? error.message : "Unknown serialization error"}`;
|
|
1097
|
+
log("💥", "Serialization failed - terminating execution:", {
|
|
1098
|
+
stepId,
|
|
1099
|
+
stepName,
|
|
1100
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1101
|
+
});
|
|
1102
|
+
terminationManager.terminate({
|
|
1103
|
+
reason: TerminationReason.SERDES_FAILED,
|
|
1104
|
+
message: message,
|
|
1105
|
+
});
|
|
1106
|
+
// Return a never-resolving promise to ensure the execution doesn't continue
|
|
1107
|
+
return new Promise(() => { });
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Utility function to safely execute deserialization with proper error handling
|
|
1112
|
+
* Instead of throwing unrecoverable errors, this directly terminates execution
|
|
1113
|
+
*/
|
|
1114
|
+
async function safeDeserialize(serdes, data, stepId, stepName, terminationManager, durableExecutionArn) {
|
|
1115
|
+
try {
|
|
1116
|
+
const context = {
|
|
1117
|
+
entityId: stepId,
|
|
1118
|
+
durableExecutionArn,
|
|
1119
|
+
};
|
|
1120
|
+
return await serdes.deserialize(data, context);
|
|
1121
|
+
}
|
|
1122
|
+
catch (error) {
|
|
1123
|
+
const message = `Deserialization failed for step ${stepName ? `"${stepName}" ` : ""}(${stepId}): ${error instanceof Error ? error.message : "Unknown deserialization error"}`;
|
|
1124
|
+
log("💥", "Deserialization failed - terminating execution:", {
|
|
1125
|
+
stepId,
|
|
1126
|
+
stepName,
|
|
1127
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1128
|
+
});
|
|
1129
|
+
terminationManager.terminate({
|
|
1130
|
+
reason: TerminationReason.SERDES_FAILED,
|
|
1131
|
+
message: message,
|
|
1132
|
+
});
|
|
1133
|
+
// Return a never-resolving promise to ensure the execution doesn't continue
|
|
1134
|
+
return new Promise(() => { });
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function isErrorLike(obj) {
|
|
1139
|
+
return (obj instanceof Error ||
|
|
1140
|
+
(obj != null &&
|
|
1141
|
+
typeof obj === "object" &&
|
|
1142
|
+
"message" in obj &&
|
|
1143
|
+
"name" in obj));
|
|
1144
|
+
}
|
|
1145
|
+
function createErrorObjectFromError(error, data) {
|
|
1146
|
+
if (error instanceof DurableOperationError) {
|
|
1147
|
+
// Use DurableOperationError's built-in serialization
|
|
1148
|
+
const errorObject = error.toErrorObject();
|
|
1149
|
+
return errorObject;
|
|
1150
|
+
}
|
|
1151
|
+
if (isErrorLike(error)) {
|
|
1152
|
+
return {
|
|
1153
|
+
ErrorData: data,
|
|
1154
|
+
ErrorMessage: error.message,
|
|
1155
|
+
ErrorType: error.name,
|
|
1156
|
+
StackTrace: undefined,
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
return {
|
|
1160
|
+
ErrorData: data,
|
|
1161
|
+
ErrorMessage: "Unknown error",
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Error thrown when a checkpoint operation fails due to invocation-level issues
|
|
1167
|
+
* (e.g., 5xx errors, invalid checkpoint token)
|
|
1168
|
+
* This will terminate the current Lambda invocation, but the execution can continue with a new invocation
|
|
1169
|
+
*/
|
|
1170
|
+
class CheckpointUnrecoverableInvocationError extends UnrecoverableInvocationError {
|
|
1171
|
+
terminationReason = TerminationReason.CHECKPOINT_FAILED;
|
|
1172
|
+
constructor(message, originalError) {
|
|
1173
|
+
super(message || "Checkpoint operation failed", originalError);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Error thrown when a checkpoint operation fails due to execution-level issues
|
|
1178
|
+
* (e.g., 4xx errors other than invalid checkpoint token)
|
|
1179
|
+
* This will terminate the entire execution and cannot be recovered
|
|
1180
|
+
*/
|
|
1181
|
+
class CheckpointUnrecoverableExecutionError extends UnrecoverableExecutionError {
|
|
1182
|
+
terminationReason = TerminationReason.CHECKPOINT_FAILED;
|
|
1183
|
+
constructor(message, originalError) {
|
|
1184
|
+
super(message || "Checkpoint operation failed", originalError);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
const STEP_DATA_UPDATED_EVENT = "stepDataUpdated";
|
|
1189
|
+
class CheckpointManager {
|
|
1190
|
+
durableExecutionArn;
|
|
1191
|
+
stepData;
|
|
1192
|
+
storage;
|
|
1193
|
+
terminationManager;
|
|
1194
|
+
activeOperationsTracker;
|
|
1195
|
+
stepDataEmitter;
|
|
1196
|
+
logger;
|
|
1197
|
+
pendingCompletions;
|
|
1198
|
+
queue = [];
|
|
1199
|
+
isProcessing = false;
|
|
1200
|
+
currentTaskToken;
|
|
1201
|
+
forceCheckpointPromises = [];
|
|
1202
|
+
queueCompletionResolver = null;
|
|
1203
|
+
queueCompletionTimeout = null;
|
|
1204
|
+
MAX_PAYLOAD_SIZE = 750 * 1024; // 750KB in bytes
|
|
1205
|
+
isTerminating = false;
|
|
1206
|
+
static textEncoder = new TextEncoder();
|
|
1207
|
+
constructor(durableExecutionArn, stepData, storage, terminationManager, activeOperationsTracker, initialTaskToken, stepDataEmitter, logger, pendingCompletions) {
|
|
1208
|
+
this.durableExecutionArn = durableExecutionArn;
|
|
1209
|
+
this.stepData = stepData;
|
|
1210
|
+
this.storage = storage;
|
|
1211
|
+
this.terminationManager = terminationManager;
|
|
1212
|
+
this.activeOperationsTracker = activeOperationsTracker;
|
|
1213
|
+
this.stepDataEmitter = stepDataEmitter;
|
|
1214
|
+
this.logger = logger;
|
|
1215
|
+
this.pendingCompletions = pendingCompletions;
|
|
1216
|
+
this.currentTaskToken = initialTaskToken;
|
|
1217
|
+
}
|
|
1218
|
+
setTerminating() {
|
|
1219
|
+
this.isTerminating = true;
|
|
1220
|
+
log("🛑", "Checkpoint manager marked as terminating");
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Checks if a step ID or any of its ancestors has a pending completion
|
|
1224
|
+
*/
|
|
1225
|
+
hasPendingAncestorCompletion(stepId) {
|
|
1226
|
+
let currentHashedId = hashId(stepId);
|
|
1227
|
+
while (currentHashedId) {
|
|
1228
|
+
if (this.pendingCompletions.has(currentHashedId)) {
|
|
1229
|
+
return true;
|
|
1230
|
+
}
|
|
1231
|
+
const operation = this.stepData[currentHashedId];
|
|
1232
|
+
currentHashedId = operation?.ParentId;
|
|
1233
|
+
}
|
|
1234
|
+
return false;
|
|
1235
|
+
}
|
|
1236
|
+
async forceCheckpoint() {
|
|
1237
|
+
if (this.isTerminating) {
|
|
1238
|
+
log("⚠️", "Force checkpoint skipped - termination in progress");
|
|
1239
|
+
return new Promise(() => { }); // Never resolves during termination
|
|
1240
|
+
}
|
|
1241
|
+
return new Promise((resolve, reject) => {
|
|
1242
|
+
this.forceCheckpointPromises.push({ resolve, reject });
|
|
1243
|
+
if (!this.isProcessing) {
|
|
1244
|
+
setImmediate(() => {
|
|
1245
|
+
this.processQueue();
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
async waitForQueueCompletion() {
|
|
1251
|
+
if (this.queue.length === 0 && !this.isProcessing) {
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
return new Promise((resolve, reject) => {
|
|
1255
|
+
this.queueCompletionResolver = resolve;
|
|
1256
|
+
// Set a timeout to prevent infinite waiting
|
|
1257
|
+
this.queueCompletionTimeout = setTimeout(() => {
|
|
1258
|
+
this.queueCompletionResolver = null;
|
|
1259
|
+
this.queueCompletionTimeout = null;
|
|
1260
|
+
// Clear the queue since it's taking too long
|
|
1261
|
+
this.clearQueue();
|
|
1262
|
+
reject(new Error("Timeout waiting for checkpoint queue completion"));
|
|
1263
|
+
}, 3000); // 3 second timeout
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
clearQueue() {
|
|
1267
|
+
// Silently clear queue - we're terminating so no need to reject promises
|
|
1268
|
+
this.queue = [];
|
|
1269
|
+
this.forceCheckpointPromises = [];
|
|
1270
|
+
// Resolve any waiting queue completion promises since we're clearing
|
|
1271
|
+
this.notifyQueueCompletion();
|
|
1272
|
+
}
|
|
1273
|
+
// Alias for backward compatibility with Checkpoint interface
|
|
1274
|
+
async force() {
|
|
1275
|
+
return this.forceCheckpoint();
|
|
1276
|
+
}
|
|
1277
|
+
async checkpoint(stepId, data) {
|
|
1278
|
+
if (this.isTerminating) {
|
|
1279
|
+
log("⚠️", "Checkpoint skipped - termination in progress:", { stepId });
|
|
1280
|
+
return new Promise(() => { }); // Never resolves during termination
|
|
1281
|
+
}
|
|
1282
|
+
if (this.activeOperationsTracker) {
|
|
1283
|
+
this.activeOperationsTracker.increment();
|
|
1284
|
+
}
|
|
1285
|
+
return new Promise((resolve, reject) => {
|
|
1286
|
+
if (data.Action === clientLambda.OperationAction.SUCCEED ||
|
|
1287
|
+
data.Action === clientLambda.OperationAction.FAIL) {
|
|
1288
|
+
this.pendingCompletions.add(stepId);
|
|
1289
|
+
}
|
|
1290
|
+
const queuedItem = {
|
|
1291
|
+
stepId,
|
|
1292
|
+
data,
|
|
1293
|
+
resolve: () => {
|
|
1294
|
+
if (this.activeOperationsTracker) {
|
|
1295
|
+
this.activeOperationsTracker.decrement();
|
|
1296
|
+
}
|
|
1297
|
+
resolve();
|
|
1298
|
+
},
|
|
1299
|
+
reject: (error) => {
|
|
1300
|
+
if (this.activeOperationsTracker) {
|
|
1301
|
+
this.activeOperationsTracker.decrement();
|
|
1302
|
+
}
|
|
1303
|
+
reject(error);
|
|
1304
|
+
},
|
|
1305
|
+
};
|
|
1306
|
+
this.queue.push(queuedItem);
|
|
1307
|
+
log("📥", "Checkpoint queued:", {
|
|
1308
|
+
stepId,
|
|
1309
|
+
queueLength: this.queue.length,
|
|
1310
|
+
isProcessing: this.isProcessing,
|
|
1311
|
+
});
|
|
1312
|
+
if (!this.isProcessing) {
|
|
1313
|
+
setImmediate(() => {
|
|
1314
|
+
this.processQueue();
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
hasFinishedAncestor(parentId) {
|
|
1320
|
+
if (!parentId) {
|
|
1321
|
+
return false;
|
|
1322
|
+
}
|
|
1323
|
+
let currentHashedId = hashId(parentId);
|
|
1324
|
+
while (currentHashedId) {
|
|
1325
|
+
const parentOperation = this.stepData[currentHashedId];
|
|
1326
|
+
if (parentOperation?.Status === clientLambda.OperationStatus.SUCCEEDED ||
|
|
1327
|
+
parentOperation?.Status === clientLambda.OperationStatus.FAILED) {
|
|
1328
|
+
return true;
|
|
1329
|
+
}
|
|
1330
|
+
currentHashedId = parentOperation?.ParentId;
|
|
1331
|
+
}
|
|
1332
|
+
return false;
|
|
1333
|
+
}
|
|
1334
|
+
classifyCheckpointError(error) {
|
|
1335
|
+
const originalError = error instanceof Error ? error : new Error(String(error));
|
|
1336
|
+
const awsError = error;
|
|
1337
|
+
const statusCode = awsError.$metadata?.httpStatusCode;
|
|
1338
|
+
const errorName = awsError.name;
|
|
1339
|
+
const errorMessage = awsError.message || originalError.message;
|
|
1340
|
+
log("🔍", "Classifying checkpoint error:", {
|
|
1341
|
+
statusCode,
|
|
1342
|
+
errorName,
|
|
1343
|
+
errorMessage,
|
|
1344
|
+
});
|
|
1345
|
+
if (statusCode &&
|
|
1346
|
+
statusCode >= 400 &&
|
|
1347
|
+
statusCode < 500 &&
|
|
1348
|
+
errorName === "InvalidParameterValueException" &&
|
|
1349
|
+
errorMessage.startsWith("Invalid Checkpoint Token")) {
|
|
1350
|
+
return new CheckpointUnrecoverableInvocationError(`Checkpoint failed: ${errorMessage}`, originalError);
|
|
1351
|
+
}
|
|
1352
|
+
if (statusCode &&
|
|
1353
|
+
statusCode >= 400 &&
|
|
1354
|
+
statusCode < 500 &&
|
|
1355
|
+
statusCode !== 429) {
|
|
1356
|
+
return new CheckpointUnrecoverableExecutionError(`Checkpoint failed: ${errorMessage}`, originalError);
|
|
1357
|
+
}
|
|
1358
|
+
return new CheckpointUnrecoverableInvocationError(`Checkpoint failed: ${errorMessage}`, originalError);
|
|
1359
|
+
}
|
|
1360
|
+
async processQueue() {
|
|
1361
|
+
if (this.isProcessing) {
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
const hasQueuedItems = this.queue.length > 0;
|
|
1365
|
+
const hasForceRequests = this.forceCheckpointPromises.length > 0;
|
|
1366
|
+
if (!hasQueuedItems && !hasForceRequests) {
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
this.isProcessing = true;
|
|
1370
|
+
const batch = [];
|
|
1371
|
+
let skippedCount = 0;
|
|
1372
|
+
const baseSize = this.currentTaskToken.length + 100;
|
|
1373
|
+
let currentSize = baseSize;
|
|
1374
|
+
while (this.queue.length > 0) {
|
|
1375
|
+
const nextItem = this.queue[0];
|
|
1376
|
+
const itemSize = CheckpointManager.textEncoder.encode(JSON.stringify(nextItem)).length;
|
|
1377
|
+
if (currentSize + itemSize > this.MAX_PAYLOAD_SIZE && batch.length > 0) {
|
|
1378
|
+
break;
|
|
1379
|
+
}
|
|
1380
|
+
this.queue.shift();
|
|
1381
|
+
if (this.hasFinishedAncestor(nextItem.data.ParentId)) {
|
|
1382
|
+
log("⚠️", "Checkpoint skipped - ancestor finished:", {
|
|
1383
|
+
stepId: nextItem.stepId,
|
|
1384
|
+
parentId: nextItem.data.ParentId,
|
|
1385
|
+
});
|
|
1386
|
+
skippedCount++;
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
batch.push(nextItem);
|
|
1390
|
+
currentSize += itemSize;
|
|
1391
|
+
}
|
|
1392
|
+
log("🔄", "Processing checkpoint batch:", {
|
|
1393
|
+
batchSize: batch.length,
|
|
1394
|
+
remainingInQueue: this.queue.length,
|
|
1395
|
+
estimatedSize: currentSize,
|
|
1396
|
+
maxSize: this.MAX_PAYLOAD_SIZE,
|
|
1397
|
+
});
|
|
1398
|
+
try {
|
|
1399
|
+
if (batch.length > 0 || this.forceCheckpointPromises.length > 0) {
|
|
1400
|
+
await this.processBatch(batch);
|
|
1401
|
+
}
|
|
1402
|
+
batch.forEach((item) => {
|
|
1403
|
+
if (item.data.Action === clientLambda.OperationAction.SUCCEED ||
|
|
1404
|
+
item.data.Action === clientLambda.OperationAction.FAIL) {
|
|
1405
|
+
this.pendingCompletions.delete(item.stepId);
|
|
1406
|
+
}
|
|
1407
|
+
item.resolve();
|
|
1408
|
+
});
|
|
1409
|
+
const forcePromises = this.forceCheckpointPromises.splice(0);
|
|
1410
|
+
forcePromises.forEach((promise) => {
|
|
1411
|
+
promise.resolve();
|
|
1412
|
+
});
|
|
1413
|
+
log("✅", "Checkpoint batch processed successfully:", {
|
|
1414
|
+
batchSize: batch.length,
|
|
1415
|
+
skippedCount,
|
|
1416
|
+
forceRequests: forcePromises.length,
|
|
1417
|
+
newTaskToken: this.currentTaskToken,
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
catch (error) {
|
|
1421
|
+
log("❌", "Checkpoint batch failed:", {
|
|
1422
|
+
batchSize: batch.length,
|
|
1423
|
+
error,
|
|
1424
|
+
});
|
|
1425
|
+
const checkpointError = this.classifyCheckpointError(error);
|
|
1426
|
+
// Clear remaining queue silently - we're terminating
|
|
1427
|
+
this.clearQueue();
|
|
1428
|
+
this.terminationManager.terminate({
|
|
1429
|
+
reason: TerminationReason.CHECKPOINT_FAILED,
|
|
1430
|
+
message: checkpointError.message,
|
|
1431
|
+
error: checkpointError,
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
finally {
|
|
1435
|
+
this.isProcessing = false;
|
|
1436
|
+
if (this.queue.length > 0) {
|
|
1437
|
+
setImmediate(() => {
|
|
1438
|
+
this.processQueue();
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
else {
|
|
1442
|
+
// Queue is empty and processing is done - notify all waiting promises
|
|
1443
|
+
this.notifyQueueCompletion();
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
notifyQueueCompletion() {
|
|
1448
|
+
if (this.queueCompletionResolver) {
|
|
1449
|
+
if (this.queueCompletionTimeout) {
|
|
1450
|
+
clearTimeout(this.queueCompletionTimeout);
|
|
1451
|
+
this.queueCompletionTimeout = null;
|
|
1452
|
+
}
|
|
1453
|
+
this.queueCompletionResolver();
|
|
1454
|
+
this.queueCompletionResolver = null;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
async processBatch(batch) {
|
|
1458
|
+
const updates = batch.map((item) => {
|
|
1459
|
+
const hashedStepId = hashId(item.stepId);
|
|
1460
|
+
const update = {
|
|
1461
|
+
Type: item.data.Type || "STEP",
|
|
1462
|
+
Action: item.data.Action || "START",
|
|
1463
|
+
...item.data,
|
|
1464
|
+
Id: hashedStepId,
|
|
1465
|
+
...(item.data.ParentId && { ParentId: hashId(item.data.ParentId) }),
|
|
1466
|
+
};
|
|
1467
|
+
return update;
|
|
1468
|
+
});
|
|
1469
|
+
const checkpointData = {
|
|
1470
|
+
DurableExecutionArn: this.durableExecutionArn,
|
|
1471
|
+
CheckpointToken: this.currentTaskToken,
|
|
1472
|
+
Updates: updates,
|
|
1473
|
+
};
|
|
1474
|
+
log("⏺️", "Creating checkpoint batch:", {
|
|
1475
|
+
batchSize: updates.length,
|
|
1476
|
+
checkpointToken: this.currentTaskToken,
|
|
1477
|
+
updates: updates.map((u) => ({
|
|
1478
|
+
Id: u.Id,
|
|
1479
|
+
Action: u.Action,
|
|
1480
|
+
Type: u.Type,
|
|
1481
|
+
})),
|
|
1482
|
+
});
|
|
1483
|
+
const response = await this.storage.checkpoint(checkpointData, this.logger);
|
|
1484
|
+
if (response.CheckpointToken) {
|
|
1485
|
+
this.currentTaskToken = response.CheckpointToken;
|
|
1486
|
+
}
|
|
1487
|
+
if (response.NewExecutionState?.Operations) {
|
|
1488
|
+
this.updateStepDataFromCheckpointResponse(response.NewExecutionState.Operations);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
updateStepDataFromCheckpointResponse(operations) {
|
|
1492
|
+
log("🔄", "Updating stepData from checkpoint response:", {
|
|
1493
|
+
operationCount: operations.length,
|
|
1494
|
+
operationIds: operations.map((op) => op.Id).filter(Boolean),
|
|
1495
|
+
});
|
|
1496
|
+
operations.forEach((operation) => {
|
|
1497
|
+
if (operation.Id) {
|
|
1498
|
+
this.stepData[operation.Id] = operation;
|
|
1499
|
+
log("📝", "Updated stepData entry:", operation);
|
|
1500
|
+
this.stepDataEmitter.emit(STEP_DATA_UPDATED_EVENT, operation.Id);
|
|
1501
|
+
}
|
|
1502
|
+
});
|
|
1503
|
+
log("✅", "StepData update completed:", {
|
|
1504
|
+
totalStepDataEntries: Object.keys(this.stepData).length,
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
getQueueStatus() {
|
|
1508
|
+
return {
|
|
1509
|
+
queueLength: this.queue.length,
|
|
1510
|
+
isProcessing: this.isProcessing,
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
/**
|
|
1516
|
+
* High-level helper that waits for conditions before continuing execution.
|
|
1517
|
+
* Uses event-driven approach for both operations completion and status changes.
|
|
1518
|
+
*/
|
|
1519
|
+
async function waitBeforeContinue(options) {
|
|
1520
|
+
const { checkHasRunningOperations, checkStepStatus, checkTimer, scheduledEndTimestamp, stepId, context, hasRunningOperations, operationsEmitter, checkpoint, onAwaitedChange, } = options;
|
|
1521
|
+
const promises = [];
|
|
1522
|
+
const timers = [];
|
|
1523
|
+
const cleanupFns = [];
|
|
1524
|
+
// Cleanup function to clear all timers and listeners
|
|
1525
|
+
const cleanup = () => {
|
|
1526
|
+
timers.forEach((timer) => clearTimeout(timer));
|
|
1527
|
+
cleanupFns.forEach((fn) => fn());
|
|
1528
|
+
};
|
|
1529
|
+
// Timer promise - resolves when scheduled time is reached
|
|
1530
|
+
if (checkTimer && scheduledEndTimestamp) {
|
|
1531
|
+
const timerPromise = new Promise((resolve) => {
|
|
1532
|
+
const timeLeft = Number(scheduledEndTimestamp) - Date.now();
|
|
1533
|
+
if (timeLeft > 0) {
|
|
1534
|
+
const timer = setTimeout(() => resolve({ reason: "timer", timerExpired: true }), timeLeft);
|
|
1535
|
+
timers.push(timer);
|
|
1536
|
+
}
|
|
1537
|
+
else {
|
|
1538
|
+
resolve({ reason: "timer", timerExpired: true });
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
promises.push(timerPromise);
|
|
1542
|
+
}
|
|
1543
|
+
// Operations promise - event-driven approach
|
|
1544
|
+
if (checkHasRunningOperations) {
|
|
1545
|
+
const operationsPromise = new Promise((resolve) => {
|
|
1546
|
+
if (!hasRunningOperations()) {
|
|
1547
|
+
resolve({ reason: "operations" });
|
|
1548
|
+
}
|
|
1549
|
+
else {
|
|
1550
|
+
// Event-driven: listen for completion event
|
|
1551
|
+
const handler = () => {
|
|
1552
|
+
resolve({ reason: "operations" });
|
|
1553
|
+
};
|
|
1554
|
+
operationsEmitter.once(OPERATIONS_COMPLETE_EVENT, handler);
|
|
1555
|
+
cleanupFns.push(() => operationsEmitter.off(OPERATIONS_COMPLETE_EVENT, handler));
|
|
1556
|
+
}
|
|
1557
|
+
});
|
|
1558
|
+
promises.push(operationsPromise);
|
|
1559
|
+
}
|
|
1560
|
+
// Step status promise - event-driven approach
|
|
1561
|
+
if (checkStepStatus) {
|
|
1562
|
+
const originalStatus = context.getStepData(stepId)?.Status;
|
|
1563
|
+
const hashedStepId = hashId(stepId);
|
|
1564
|
+
const stepStatusPromise = new Promise((resolve) => {
|
|
1565
|
+
// Check if status already changed
|
|
1566
|
+
const currentStatus = context.getStepData(stepId)?.Status;
|
|
1567
|
+
if (originalStatus !== currentStatus) {
|
|
1568
|
+
resolve({ reason: "status" });
|
|
1569
|
+
}
|
|
1570
|
+
else {
|
|
1571
|
+
// Event-driven: listen for step data updates
|
|
1572
|
+
const handler = (updatedStepId) => {
|
|
1573
|
+
if (updatedStepId === hashedStepId) {
|
|
1574
|
+
const newStatus = context.getStepData(stepId)?.Status;
|
|
1575
|
+
if (originalStatus !== newStatus) {
|
|
1576
|
+
resolve({ reason: "status" });
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
};
|
|
1580
|
+
operationsEmitter.on(STEP_DATA_UPDATED_EVENT, handler);
|
|
1581
|
+
cleanupFns.push(() => operationsEmitter.off(STEP_DATA_UPDATED_EVENT, handler));
|
|
1582
|
+
}
|
|
1583
|
+
});
|
|
1584
|
+
promises.push(stepStatusPromise);
|
|
1585
|
+
}
|
|
1586
|
+
// Awaited change promise - resolves when the callback we set is invoked
|
|
1587
|
+
// Note: This is safe from race conditions because waitBeforeContinue is called
|
|
1588
|
+
// during Phase 1 execution (inside stepHandler), which happens BEFORE the user
|
|
1589
|
+
// can await the DurablePromise. The callback is registered before it can be invoked.
|
|
1590
|
+
if (onAwaitedChange) {
|
|
1591
|
+
const awaitedChangePromise = new Promise((resolve) => {
|
|
1592
|
+
// Register a callback that will be invoked when the promise is awaited
|
|
1593
|
+
onAwaitedChange(() => {
|
|
1594
|
+
resolve({ reason: "status" });
|
|
1595
|
+
});
|
|
1596
|
+
});
|
|
1597
|
+
promises.push(awaitedChangePromise);
|
|
1598
|
+
}
|
|
1599
|
+
// If no conditions provided, return immediately
|
|
1600
|
+
if (promises.length === 0) {
|
|
1601
|
+
return { reason: "timeout" };
|
|
1602
|
+
}
|
|
1603
|
+
// Wait for any condition to be met, then cleanup timers and listeners
|
|
1604
|
+
const result = await Promise.race(promises);
|
|
1605
|
+
cleanup();
|
|
1606
|
+
// If timer expired, force checkpoint to get fresh data from API
|
|
1607
|
+
if (result.reason === "timer" && result.timerExpired && checkpoint) {
|
|
1608
|
+
if (checkpoint.force) {
|
|
1609
|
+
await checkpoint.force();
|
|
1610
|
+
}
|
|
1611
|
+
else if (checkpoint.forceCheckpoint) {
|
|
1612
|
+
await checkpoint.forceCheckpoint();
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
return result;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
/**
|
|
1619
|
+
* Error thrown when non-deterministic code is detected during replay
|
|
1620
|
+
*/
|
|
1621
|
+
class NonDeterministicExecutionError extends UnrecoverableExecutionError {
|
|
1622
|
+
terminationReason = TerminationReason.CUSTOM;
|
|
1623
|
+
constructor(message) {
|
|
1624
|
+
super(message);
|
|
1625
|
+
this.name = "NonDeterministicExecutionError";
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
const validateReplayConsistency = (stepId, currentOperation, checkpointData, context) => {
|
|
1630
|
+
// Skip validation if no checkpoint data exists or if Type is undefined (first execution)
|
|
1631
|
+
if (!checkpointData || !checkpointData.Type) {
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
// Validate operation type
|
|
1635
|
+
if (checkpointData.Type !== currentOperation.type) {
|
|
1636
|
+
const error = new NonDeterministicExecutionError(`Non-deterministic execution detected: Operation type mismatch for step "${stepId}". ` +
|
|
1637
|
+
`Expected type "${checkpointData.Type}", but got "${currentOperation.type}". ` +
|
|
1638
|
+
`This indicates non-deterministic control flow in your workflow code.`);
|
|
1639
|
+
terminateForUnrecoverableError(context, error, stepId);
|
|
1640
|
+
}
|
|
1641
|
+
// Validate operation name (including undefined)
|
|
1642
|
+
if (checkpointData.Name !== currentOperation.name) {
|
|
1643
|
+
const error = new NonDeterministicExecutionError(`Non-deterministic execution detected: Operation name mismatch for step "${stepId}". ` +
|
|
1644
|
+
`Expected name "${checkpointData.Name ?? "undefined"}", but got "${currentOperation.name ?? "undefined"}". ` +
|
|
1645
|
+
`This indicates non-deterministic control flow in your workflow code.`);
|
|
1646
|
+
terminateForUnrecoverableError(context, error, stepId);
|
|
1647
|
+
}
|
|
1648
|
+
// Validate operation subtype
|
|
1649
|
+
if (checkpointData.SubType !== currentOperation.subType) {
|
|
1650
|
+
const error = new NonDeterministicExecutionError(`Non-deterministic execution detected: Operation subtype mismatch for step "${stepId}". ` +
|
|
1651
|
+
`Expected subtype "${checkpointData.SubType}", but got "${currentOperation.subType}". ` +
|
|
1652
|
+
`This indicates non-deterministic control flow in your workflow code.`);
|
|
1653
|
+
terminateForUnrecoverableError(context, error, stepId);
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1656
|
+
|
|
1657
|
+
// Special symbol to indicate that the main loop should continue
|
|
1658
|
+
const CONTINUE_MAIN_LOOP$1 = Symbol("CONTINUE_MAIN_LOOP");
|
|
1659
|
+
const waitForContinuation$1 = async (context, stepId, name, hasRunningOperations, getOperationsEmitter, checkpoint, onAwaitedChange) => {
|
|
1660
|
+
const stepData = context.getStepData(stepId);
|
|
1661
|
+
// Check if there are any ongoing operations
|
|
1662
|
+
if (!hasRunningOperations()) {
|
|
1663
|
+
// No ongoing operations - safe to terminate
|
|
1664
|
+
return terminate(context, TerminationReason.RETRY_SCHEDULED, `Retry scheduled for ${name || stepId}`);
|
|
1665
|
+
}
|
|
1666
|
+
// There are ongoing operations - wait before continuing
|
|
1667
|
+
await waitBeforeContinue({
|
|
1668
|
+
checkHasRunningOperations: true,
|
|
1669
|
+
checkStepStatus: true,
|
|
1670
|
+
checkTimer: true,
|
|
1671
|
+
scheduledEndTimestamp: stepData?.StepDetails?.NextAttemptTimestamp,
|
|
1672
|
+
stepId,
|
|
1673
|
+
context,
|
|
1674
|
+
hasRunningOperations,
|
|
1675
|
+
operationsEmitter: getOperationsEmitter(),
|
|
1676
|
+
checkpoint,
|
|
1677
|
+
onAwaitedChange,
|
|
1678
|
+
});
|
|
1679
|
+
// Return to let the main loop re-evaluate step status
|
|
1680
|
+
};
|
|
1681
|
+
/**
|
|
1682
|
+
* Creates a step handler for executing durable steps with two-phase execution.
|
|
1683
|
+
*/
|
|
1684
|
+
const createStepHandler = (context, checkpoint, parentContext, createStepId, logger, addRunningOperation, removeRunningOperation, hasRunningOperations, getOperationsEmitter, parentId) => {
|
|
1685
|
+
return (nameOrFn, fnOrOptions, maybeOptions) => {
|
|
1686
|
+
let name;
|
|
1687
|
+
let fn;
|
|
1688
|
+
let options;
|
|
1689
|
+
if (typeof nameOrFn === "string" || nameOrFn === undefined) {
|
|
1690
|
+
name = nameOrFn;
|
|
1691
|
+
fn = fnOrOptions;
|
|
1692
|
+
options = maybeOptions;
|
|
1693
|
+
}
|
|
1694
|
+
else {
|
|
1695
|
+
fn = nameOrFn;
|
|
1696
|
+
options = fnOrOptions;
|
|
1697
|
+
}
|
|
1698
|
+
const stepId = createStepId();
|
|
1699
|
+
log("▶️", "Running step:", { stepId, name, options });
|
|
1700
|
+
// Two-phase execution: Phase 1 starts immediately, Phase 2 returns result when awaited
|
|
1701
|
+
let isAwaited = false;
|
|
1702
|
+
let waitingCallback;
|
|
1703
|
+
const setWaitingCallback = (cb) => {
|
|
1704
|
+
waitingCallback = cb;
|
|
1705
|
+
};
|
|
1706
|
+
// Phase 1: Start execution immediately and capture result/error
|
|
1707
|
+
const phase1Promise = (async () => {
|
|
1708
|
+
// Main step logic - can be re-executed if step status changes
|
|
1709
|
+
while (true) {
|
|
1710
|
+
try {
|
|
1711
|
+
const stepData = context.getStepData(stepId);
|
|
1712
|
+
// Validate replay consistency
|
|
1713
|
+
validateReplayConsistency(stepId, {
|
|
1714
|
+
type: clientLambda.OperationType.STEP,
|
|
1715
|
+
name,
|
|
1716
|
+
subType: exports.OperationSubType.STEP,
|
|
1717
|
+
}, stepData, context);
|
|
1718
|
+
if (stepData?.Status === clientLambda.OperationStatus.SUCCEEDED) {
|
|
1719
|
+
return await handleCompletedStep(context, stepId, name, options?.serdes);
|
|
1720
|
+
}
|
|
1721
|
+
if (stepData?.Status === clientLambda.OperationStatus.FAILED) {
|
|
1722
|
+
// Return an async rejected promise to ensure it's handled asynchronously
|
|
1723
|
+
return (async () => {
|
|
1724
|
+
// Reconstruct the original error from stored ErrorObject
|
|
1725
|
+
if (stepData.StepDetails?.Error) {
|
|
1726
|
+
throw DurableOperationError.fromErrorObject(stepData.StepDetails.Error);
|
|
1727
|
+
}
|
|
1728
|
+
else {
|
|
1729
|
+
// Fallback for legacy data without Error field
|
|
1730
|
+
const errorMessage = stepData?.StepDetails?.Result;
|
|
1731
|
+
throw new StepError(errorMessage || "Unknown error");
|
|
1732
|
+
}
|
|
1733
|
+
})();
|
|
1734
|
+
}
|
|
1735
|
+
// If PENDING, wait for timer to complete
|
|
1736
|
+
if (stepData?.Status === clientLambda.OperationStatus.PENDING) {
|
|
1737
|
+
await waitForContinuation$1(context, stepId, name, hasRunningOperations, getOperationsEmitter, checkpoint, isAwaited ? undefined : setWaitingCallback);
|
|
1738
|
+
continue; // Re-evaluate step status after waiting
|
|
1739
|
+
}
|
|
1740
|
+
// Check for interrupted step with AT_MOST_ONCE_PER_RETRY semantics
|
|
1741
|
+
if (stepData?.Status === clientLambda.OperationStatus.STARTED) {
|
|
1742
|
+
const semantics = options?.semantics || exports.StepSemantics.AtLeastOncePerRetry;
|
|
1743
|
+
if (semantics === exports.StepSemantics.AtMostOncePerRetry) {
|
|
1744
|
+
log("⚠️", "Step was interrupted during execution:", {
|
|
1745
|
+
stepId,
|
|
1746
|
+
name,
|
|
1747
|
+
});
|
|
1748
|
+
const error = new StepInterruptedError(stepId, name);
|
|
1749
|
+
// Handle the interrupted step as a failure
|
|
1750
|
+
const currentAttempt = (stepData?.StepDetails?.Attempt || 0) + 1;
|
|
1751
|
+
let retryDecision;
|
|
1752
|
+
if (options?.retryStrategy !== undefined) {
|
|
1753
|
+
retryDecision = options.retryStrategy(error, currentAttempt);
|
|
1754
|
+
}
|
|
1755
|
+
else {
|
|
1756
|
+
retryDecision = retryPresets.default(error, currentAttempt);
|
|
1757
|
+
}
|
|
1758
|
+
log("⚠️", "Should Retry Interrupted Step:", {
|
|
1759
|
+
stepId,
|
|
1760
|
+
name,
|
|
1761
|
+
currentAttempt,
|
|
1762
|
+
shouldRetry: retryDecision.shouldRetry,
|
|
1763
|
+
delayInSeconds: retryDecision.shouldRetry
|
|
1764
|
+
? retryDecision.delay
|
|
1765
|
+
? durationToSeconds(retryDecision.delay)
|
|
1766
|
+
: undefined
|
|
1767
|
+
: undefined,
|
|
1768
|
+
});
|
|
1769
|
+
if (!retryDecision.shouldRetry) {
|
|
1770
|
+
// No retry, mark as failed
|
|
1771
|
+
await checkpoint.checkpoint(stepId, {
|
|
1772
|
+
Id: stepId,
|
|
1773
|
+
ParentId: parentId,
|
|
1774
|
+
Action: clientLambda.OperationAction.FAIL,
|
|
1775
|
+
SubType: exports.OperationSubType.STEP,
|
|
1776
|
+
Type: clientLambda.OperationType.STEP,
|
|
1777
|
+
Error: createErrorObjectFromError(error),
|
|
1778
|
+
Name: name,
|
|
1779
|
+
});
|
|
1780
|
+
// Reconstruct error from ErrorObject for deterministic behavior
|
|
1781
|
+
const errorObject = createErrorObjectFromError(error);
|
|
1782
|
+
throw DurableOperationError.fromErrorObject(errorObject);
|
|
1783
|
+
}
|
|
1784
|
+
else {
|
|
1785
|
+
// Retry
|
|
1786
|
+
await checkpoint.checkpoint(stepId, {
|
|
1787
|
+
Id: stepId,
|
|
1788
|
+
ParentId: parentId,
|
|
1789
|
+
Action: clientLambda.OperationAction.RETRY,
|
|
1790
|
+
SubType: exports.OperationSubType.STEP,
|
|
1791
|
+
Type: clientLambda.OperationType.STEP,
|
|
1792
|
+
Error: createErrorObjectFromError(error),
|
|
1793
|
+
Name: name,
|
|
1794
|
+
StepOptions: {
|
|
1795
|
+
NextAttemptDelaySeconds: retryDecision.delay
|
|
1796
|
+
? durationToSeconds(retryDecision.delay)
|
|
1797
|
+
: 1,
|
|
1798
|
+
},
|
|
1799
|
+
});
|
|
1800
|
+
await waitForContinuation$1(context, stepId, name, hasRunningOperations, getOperationsEmitter, checkpoint, isAwaited ? undefined : setWaitingCallback);
|
|
1801
|
+
continue; // Re-evaluate step status after waiting
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
// Execute step function for READY, STARTED (AtLeastOncePerRetry), or first time (undefined)
|
|
1806
|
+
const result = await executeStep(context, checkpoint, stepId, name, fn, logger, addRunningOperation, removeRunningOperation, hasRunningOperations, getOperationsEmitter, parentId, options, isAwaited ? undefined : setWaitingCallback);
|
|
1807
|
+
// If executeStep signals to continue the main loop, do so
|
|
1808
|
+
if (result === CONTINUE_MAIN_LOOP$1) {
|
|
1809
|
+
continue;
|
|
1810
|
+
}
|
|
1811
|
+
return result;
|
|
1812
|
+
}
|
|
1813
|
+
catch (error) {
|
|
1814
|
+
// Preserve DurableOperationError instances (StepInterruptedError is handled specifically where it's thrown)
|
|
1815
|
+
if (error instanceof DurableOperationError) {
|
|
1816
|
+
throw error;
|
|
1817
|
+
}
|
|
1818
|
+
// For any other error from executeStep, wrap it in StepError for consistency
|
|
1819
|
+
throw new StepError(error instanceof Error ? error.message : "Step failed", error instanceof Error ? error : undefined);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
})();
|
|
1823
|
+
// Attach catch handler to prevent unhandled promise rejections
|
|
1824
|
+
// The error will still be thrown when the DurablePromise is awaited
|
|
1825
|
+
phase1Promise.catch(() => { });
|
|
1826
|
+
// Phase 2: Return DurablePromise that returns Phase 1 result when awaited
|
|
1827
|
+
return new DurablePromise(async () => {
|
|
1828
|
+
// When promise is awaited, mark as awaited and invoke waiting callback
|
|
1829
|
+
isAwaited = true;
|
|
1830
|
+
if (waitingCallback) {
|
|
1831
|
+
waitingCallback();
|
|
1832
|
+
}
|
|
1833
|
+
return await phase1Promise;
|
|
1834
|
+
});
|
|
1835
|
+
};
|
|
1836
|
+
};
|
|
1837
|
+
const handleCompletedStep = async (context, stepId, stepName, serdes = defaultSerdes) => {
|
|
1838
|
+
log("⏭️", "Step already finished, returning cached result:", { stepId });
|
|
1839
|
+
const stepData = context.getStepData(stepId);
|
|
1840
|
+
const result = stepData?.StepDetails?.Result;
|
|
1841
|
+
return await safeDeserialize(serdes, result, stepId, stepName, context.terminationManager, context.durableExecutionArn);
|
|
1842
|
+
};
|
|
1843
|
+
const executeStep = async (context, checkpoint, stepId, name, fn, logger, addRunningOperation, removeRunningOperation, hasRunningOperations, getOperationsEmitter, parentId, options, onAwaitedChange) => {
|
|
1844
|
+
// Determine step semantics (default to AT_LEAST_ONCE_PER_RETRY if not specified)
|
|
1845
|
+
const semantics = options?.semantics || exports.StepSemantics.AtLeastOncePerRetry;
|
|
1846
|
+
const serdes = options?.serdes || defaultSerdes;
|
|
1847
|
+
// Checkpoint at start for both semantics (only if not already started)
|
|
1848
|
+
const stepData = context.getStepData(stepId);
|
|
1849
|
+
if (stepData?.Status !== clientLambda.OperationStatus.STARTED) {
|
|
1850
|
+
if (semantics === exports.StepSemantics.AtMostOncePerRetry) {
|
|
1851
|
+
// Wait for checkpoint to complete
|
|
1852
|
+
await checkpoint.checkpoint(stepId, {
|
|
1853
|
+
Id: stepId,
|
|
1854
|
+
ParentId: parentId,
|
|
1855
|
+
Action: clientLambda.OperationAction.START,
|
|
1856
|
+
SubType: exports.OperationSubType.STEP,
|
|
1857
|
+
Type: clientLambda.OperationType.STEP,
|
|
1858
|
+
Name: name,
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
else {
|
|
1862
|
+
// Fire and forget for AtLeastOncePerRetry
|
|
1863
|
+
checkpoint.checkpoint(stepId, {
|
|
1864
|
+
Id: stepId,
|
|
1865
|
+
ParentId: parentId,
|
|
1866
|
+
Action: clientLambda.OperationAction.START,
|
|
1867
|
+
SubType: exports.OperationSubType.STEP,
|
|
1868
|
+
Type: clientLambda.OperationType.STEP,
|
|
1869
|
+
Name: name,
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
try {
|
|
1874
|
+
// Get current attempt number for logger enrichment
|
|
1875
|
+
const stepData = context.getStepData(stepId);
|
|
1876
|
+
const currentAttempt = stepData?.StepDetails?.Attempt || 0;
|
|
1877
|
+
// Create step context with enriched logger
|
|
1878
|
+
const stepContext = {
|
|
1879
|
+
logger,
|
|
1880
|
+
};
|
|
1881
|
+
// Execute the step function with stepContext
|
|
1882
|
+
addRunningOperation(stepId);
|
|
1883
|
+
let result;
|
|
1884
|
+
try {
|
|
1885
|
+
result = await runWithContext(stepId, parentId, () => fn(stepContext),
|
|
1886
|
+
// The attempt that is running is the attempt from the step data (previous step attempt) + 1
|
|
1887
|
+
currentAttempt + 1,
|
|
1888
|
+
// Alwasy in execution mode when running step operations
|
|
1889
|
+
DurableExecutionMode.ExecutionMode);
|
|
1890
|
+
}
|
|
1891
|
+
finally {
|
|
1892
|
+
removeRunningOperation(stepId);
|
|
1893
|
+
}
|
|
1894
|
+
// Serialize the result for consistency
|
|
1895
|
+
const serializedResult = await safeSerialize(serdes, result, stepId, name, context.terminationManager, context.durableExecutionArn);
|
|
1896
|
+
// Always checkpoint on completion
|
|
1897
|
+
await checkpoint.checkpoint(stepId, {
|
|
1898
|
+
Id: stepId,
|
|
1899
|
+
ParentId: parentId,
|
|
1900
|
+
Action: clientLambda.OperationAction.SUCCEED,
|
|
1901
|
+
SubType: exports.OperationSubType.STEP,
|
|
1902
|
+
Type: clientLambda.OperationType.STEP,
|
|
1903
|
+
Payload: serializedResult,
|
|
1904
|
+
Name: name,
|
|
1905
|
+
});
|
|
1906
|
+
log("✅", "Step completed successfully:", {
|
|
1907
|
+
stepId,
|
|
1908
|
+
name,
|
|
1909
|
+
result,
|
|
1910
|
+
semantics,
|
|
1911
|
+
});
|
|
1912
|
+
// Deserialize the result for consistency with replay behavior
|
|
1913
|
+
return await safeDeserialize(serdes, serializedResult, stepId, name, context.terminationManager, context.durableExecutionArn);
|
|
1914
|
+
}
|
|
1915
|
+
catch (error) {
|
|
1916
|
+
log("❌", "Step failed:", {
|
|
1917
|
+
stepId,
|
|
1918
|
+
name,
|
|
1919
|
+
error,
|
|
1920
|
+
semantics,
|
|
1921
|
+
});
|
|
1922
|
+
// Handle unrecoverable errors - these should not go through retry logic
|
|
1923
|
+
if (isUnrecoverableError(error)) {
|
|
1924
|
+
log("💥", "Unrecoverable error detected:", {
|
|
1925
|
+
stepId,
|
|
1926
|
+
name,
|
|
1927
|
+
error: error.message,
|
|
1928
|
+
});
|
|
1929
|
+
return terminateForUnrecoverableError(context, error, name || stepId);
|
|
1930
|
+
}
|
|
1931
|
+
const stepData = context.getStepData(stepId);
|
|
1932
|
+
const currentAttempt = (stepData?.StepDetails?.Attempt || 0) + 1;
|
|
1933
|
+
let retryDecision;
|
|
1934
|
+
if (options?.retryStrategy !== undefined) {
|
|
1935
|
+
// Use provided retry configuration
|
|
1936
|
+
retryDecision = options.retryStrategy(error instanceof Error ? error : new Error("Unknown Error"), currentAttempt);
|
|
1937
|
+
}
|
|
1938
|
+
else {
|
|
1939
|
+
// Use default retry preset if no config provided
|
|
1940
|
+
retryDecision = retryPresets.default(error instanceof Error ? error : new Error("Unknown Error"), currentAttempt);
|
|
1941
|
+
}
|
|
1942
|
+
log("⚠️", "Should Retry:", {
|
|
1943
|
+
stepId,
|
|
1944
|
+
name,
|
|
1945
|
+
currentAttempt,
|
|
1946
|
+
shouldRetry: retryDecision.shouldRetry,
|
|
1947
|
+
delayInSeconds: retryDecision.shouldRetry
|
|
1948
|
+
? retryDecision.delay
|
|
1949
|
+
? durationToSeconds(retryDecision.delay)
|
|
1950
|
+
: undefined
|
|
1951
|
+
: undefined,
|
|
1952
|
+
semantics,
|
|
1953
|
+
});
|
|
1954
|
+
if (!retryDecision.shouldRetry) {
|
|
1955
|
+
// No retry
|
|
1956
|
+
await checkpoint.checkpoint(stepId, {
|
|
1957
|
+
Id: stepId,
|
|
1958
|
+
ParentId: parentId,
|
|
1959
|
+
Action: clientLambda.OperationAction.FAIL,
|
|
1960
|
+
SubType: exports.OperationSubType.STEP,
|
|
1961
|
+
Type: clientLambda.OperationType.STEP,
|
|
1962
|
+
Error: createErrorObjectFromError(error),
|
|
1963
|
+
Name: name,
|
|
1964
|
+
});
|
|
1965
|
+
// Reconstruct error from ErrorObject for deterministic behavior
|
|
1966
|
+
const errorObject = createErrorObjectFromError(error);
|
|
1967
|
+
throw DurableOperationError.fromErrorObject(errorObject);
|
|
1968
|
+
}
|
|
1969
|
+
else {
|
|
1970
|
+
// Retry
|
|
1971
|
+
await checkpoint.checkpoint(stepId, {
|
|
1972
|
+
Id: stepId,
|
|
1973
|
+
ParentId: parentId,
|
|
1974
|
+
Action: clientLambda.OperationAction.RETRY,
|
|
1975
|
+
SubType: exports.OperationSubType.STEP,
|
|
1976
|
+
Type: clientLambda.OperationType.STEP,
|
|
1977
|
+
Error: createErrorObjectFromError(error),
|
|
1978
|
+
Name: name,
|
|
1979
|
+
StepOptions: {
|
|
1980
|
+
NextAttemptDelaySeconds: retryDecision.delay
|
|
1981
|
+
? durationToSeconds(retryDecision.delay)
|
|
1982
|
+
: 1,
|
|
1983
|
+
},
|
|
1984
|
+
});
|
|
1985
|
+
// Wait for continuation and signal main loop to continue
|
|
1986
|
+
await waitForContinuation$1(context, stepId, name, hasRunningOperations, getOperationsEmitter, checkpoint, onAwaitedChange);
|
|
1987
|
+
return CONTINUE_MAIN_LOOP$1;
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
};
|
|
1991
|
+
|
|
1992
|
+
const createInvokeHandler = (context, checkpoint, createStepId, hasRunningOperations, getOperationsEmitter, parentId, checkAndUpdateReplayMode) => {
|
|
1993
|
+
function invokeHandler(nameOrFuncId, funcIdOrInput, inputOrConfig, maybeConfig) {
|
|
1994
|
+
const isNameFirst = typeof funcIdOrInput === "string";
|
|
1995
|
+
const name = isNameFirst ? nameOrFuncId : undefined;
|
|
1996
|
+
const funcId = isNameFirst ? funcIdOrInput : nameOrFuncId;
|
|
1997
|
+
const input = isNameFirst
|
|
1998
|
+
? inputOrConfig
|
|
1999
|
+
: funcIdOrInput;
|
|
2000
|
+
const config = isNameFirst
|
|
2001
|
+
? maybeConfig
|
|
2002
|
+
: inputOrConfig;
|
|
2003
|
+
const stepId = createStepId();
|
|
2004
|
+
// Phase 1: Only checkpoint if needed, don't execute full logic
|
|
2005
|
+
const startInvokeOperation = async () => {
|
|
2006
|
+
log("🔗", `Invoke ${name || funcId} (${stepId}) - phase 1`);
|
|
2007
|
+
// Check initial step data for replay consistency validation
|
|
2008
|
+
const initialStepData = context.getStepData(stepId);
|
|
2009
|
+
// Validate replay consistency once before any execution
|
|
2010
|
+
validateReplayConsistency(stepId, {
|
|
2011
|
+
type: clientLambda.OperationType.CHAINED_INVOKE,
|
|
2012
|
+
name,
|
|
2013
|
+
subType: exports.OperationSubType.CHAINED_INVOKE,
|
|
2014
|
+
}, initialStepData, context);
|
|
2015
|
+
// If stepData already exists, phase 1 has nothing to do
|
|
2016
|
+
if (initialStepData) {
|
|
2017
|
+
log("⏸️", `Invoke ${name || funcId} already exists (phase 1)`);
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
// No stepData exists - need to start the invoke operation
|
|
2021
|
+
// Serialize the input payload
|
|
2022
|
+
const serializedPayload = await safeSerialize(config?.payloadSerdes || defaultSerdes, input, stepId, name, context.terminationManager, context.durableExecutionArn);
|
|
2023
|
+
// Create checkpoint for the invoke operation
|
|
2024
|
+
await checkpoint.checkpoint(stepId, {
|
|
2025
|
+
Id: stepId,
|
|
2026
|
+
ParentId: parentId,
|
|
2027
|
+
Action: clientLambda.OperationAction.START,
|
|
2028
|
+
SubType: exports.OperationSubType.CHAINED_INVOKE,
|
|
2029
|
+
Type: clientLambda.OperationType.CHAINED_INVOKE,
|
|
2030
|
+
Name: name,
|
|
2031
|
+
Payload: serializedPayload,
|
|
2032
|
+
ChainedInvokeOptions: {
|
|
2033
|
+
FunctionName: funcId,
|
|
2034
|
+
},
|
|
2035
|
+
});
|
|
2036
|
+
log("🚀", `Invoke ${name || funcId} started (phase 1)`);
|
|
2037
|
+
};
|
|
2038
|
+
// Phase 2: Execute full logic including waiting and termination
|
|
2039
|
+
const continueInvokeOperation = async () => {
|
|
2040
|
+
log("🔗", `Invoke ${name || funcId} (${stepId}) - phase 2`);
|
|
2041
|
+
// Main invoke logic - can be re-executed if step status changes
|
|
2042
|
+
while (true) {
|
|
2043
|
+
// Check if we have existing step data
|
|
2044
|
+
const stepData = context.getStepData(stepId);
|
|
2045
|
+
if (stepData?.Status === clientLambda.OperationStatus.SUCCEEDED) {
|
|
2046
|
+
// Return cached result - no need to check for errors in successful operations
|
|
2047
|
+
const invokeDetails = stepData.ChainedInvokeDetails;
|
|
2048
|
+
checkAndUpdateReplayMode?.();
|
|
2049
|
+
return await safeDeserialize(config?.resultSerdes || defaultSerdes, invokeDetails?.Result, stepId, name, context.terminationManager, context.durableExecutionArn);
|
|
2050
|
+
}
|
|
2051
|
+
if (stepData?.Status === clientLambda.OperationStatus.FAILED ||
|
|
2052
|
+
stepData?.Status === clientLambda.OperationStatus.TIMED_OUT ||
|
|
2053
|
+
stepData?.Status === clientLambda.OperationStatus.STOPPED) {
|
|
2054
|
+
// Operation failed, return async rejected promise
|
|
2055
|
+
const invokeDetails = stepData.ChainedInvokeDetails;
|
|
2056
|
+
return (async () => {
|
|
2057
|
+
if (invokeDetails?.Error) {
|
|
2058
|
+
throw new InvokeError(invokeDetails.Error.ErrorMessage || "Invoke failed", invokeDetails.Error.ErrorMessage
|
|
2059
|
+
? new Error(invokeDetails.Error.ErrorMessage)
|
|
2060
|
+
: undefined, invokeDetails.Error.ErrorData);
|
|
2061
|
+
}
|
|
2062
|
+
else {
|
|
2063
|
+
throw new InvokeError("Invoke failed");
|
|
2064
|
+
}
|
|
2065
|
+
})();
|
|
2066
|
+
}
|
|
2067
|
+
if (stepData?.Status === clientLambda.OperationStatus.STARTED) {
|
|
2068
|
+
// Operation is still running
|
|
2069
|
+
if (hasRunningOperations()) {
|
|
2070
|
+
// Phase 2: Wait for other operations
|
|
2071
|
+
log("⏳", `Invoke ${name || funcId} still in progress, waiting for other operations`);
|
|
2072
|
+
await waitBeforeContinue({
|
|
2073
|
+
checkHasRunningOperations: true,
|
|
2074
|
+
checkStepStatus: true,
|
|
2075
|
+
checkTimer: false,
|
|
2076
|
+
stepId,
|
|
2077
|
+
context,
|
|
2078
|
+
hasRunningOperations,
|
|
2079
|
+
operationsEmitter: getOperationsEmitter(),
|
|
2080
|
+
});
|
|
2081
|
+
continue; // Re-evaluate status after waiting
|
|
2082
|
+
}
|
|
2083
|
+
// No other operations running - terminate
|
|
2084
|
+
log("⏳", `Invoke ${name || funcId} still in progress, terminating`);
|
|
2085
|
+
return terminate(context, TerminationReason.OPERATION_TERMINATED, stepId);
|
|
2086
|
+
}
|
|
2087
|
+
// If stepData exists but has an unexpected status, break to avoid infinite loop
|
|
2088
|
+
if (stepData && stepData.Status !== undefined) {
|
|
2089
|
+
throw new InvokeError(`Unexpected operation status: ${stepData.Status}`);
|
|
2090
|
+
}
|
|
2091
|
+
// This should not happen in phase 2 since phase 1 creates stepData
|
|
2092
|
+
throw new InvokeError("No step data found in phase 2 - this should not happen");
|
|
2093
|
+
}
|
|
2094
|
+
};
|
|
2095
|
+
// Create a promise that tracks phase 1 completion
|
|
2096
|
+
const startInvokePromise = startInvokeOperation()
|
|
2097
|
+
.then(() => {
|
|
2098
|
+
log("✅", "Invoke phase 1 complete:", { stepId, name: name || funcId });
|
|
2099
|
+
})
|
|
2100
|
+
.catch((error) => {
|
|
2101
|
+
log("❌", "Invoke phase 1 error:", { stepId, error: error.message });
|
|
2102
|
+
throw error; // Re-throw to fail phase 1
|
|
2103
|
+
});
|
|
2104
|
+
// Attach catch handler to prevent unhandled promise rejections
|
|
2105
|
+
// The error will still be thrown when the DurablePromise is awaited
|
|
2106
|
+
startInvokePromise.catch(() => { });
|
|
2107
|
+
// Return DurablePromise that will execute phase 2 when awaited
|
|
2108
|
+
return new DurablePromise(async () => {
|
|
2109
|
+
// Wait for phase 1 to complete first
|
|
2110
|
+
await startInvokePromise;
|
|
2111
|
+
// Then execute phase 2
|
|
2112
|
+
return await continueInvokeOperation();
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
return invokeHandler;
|
|
2116
|
+
};
|
|
2117
|
+
|
|
2118
|
+
// Checkpoint size limit in bytes (256KB)
|
|
2119
|
+
const CHECKPOINT_SIZE_LIMIT = 256 * 1024;
|
|
2120
|
+
const determineChildReplayMode = (context, stepId) => {
|
|
2121
|
+
const stepData = context.getStepData(stepId);
|
|
2122
|
+
if (!stepData) {
|
|
2123
|
+
return DurableExecutionMode.ExecutionMode;
|
|
2124
|
+
}
|
|
2125
|
+
if (stepData.Status === clientLambda.OperationStatus.SUCCEEDED &&
|
|
2126
|
+
stepData.ContextDetails?.ReplayChildren) {
|
|
2127
|
+
return DurableExecutionMode.ReplaySucceededContext;
|
|
2128
|
+
}
|
|
2129
|
+
if (stepData.Status === clientLambda.OperationStatus.SUCCEEDED ||
|
|
2130
|
+
stepData.Status === clientLambda.OperationStatus.FAILED) {
|
|
2131
|
+
return DurableExecutionMode.ReplayMode;
|
|
2132
|
+
}
|
|
2133
|
+
return DurableExecutionMode.ExecutionMode;
|
|
2134
|
+
};
|
|
2135
|
+
const createRunInChildContextHandler = (context, checkpoint, parentContext, createStepId, getParentLogger, createChildContext, parentId) => {
|
|
2136
|
+
return (nameOrFn, fnOrOptions, maybeOptions) => {
|
|
2137
|
+
let name;
|
|
2138
|
+
let fn;
|
|
2139
|
+
let options;
|
|
2140
|
+
if (typeof nameOrFn === "string" || nameOrFn === undefined) {
|
|
2141
|
+
name = nameOrFn;
|
|
2142
|
+
fn = fnOrOptions;
|
|
2143
|
+
options = maybeOptions;
|
|
2144
|
+
}
|
|
2145
|
+
else {
|
|
2146
|
+
fn = nameOrFn;
|
|
2147
|
+
options = fnOrOptions;
|
|
2148
|
+
}
|
|
2149
|
+
const entityId = createStepId();
|
|
2150
|
+
log("🔄", "Running child context:", {
|
|
2151
|
+
entityId,
|
|
2152
|
+
name,
|
|
2153
|
+
});
|
|
2154
|
+
const stepData = context.getStepData(entityId);
|
|
2155
|
+
// Validate replay consistency
|
|
2156
|
+
validateReplayConsistency(entityId, {
|
|
2157
|
+
type: clientLambda.OperationType.CONTEXT,
|
|
2158
|
+
name,
|
|
2159
|
+
subType: options?.subType ||
|
|
2160
|
+
exports.OperationSubType.RUN_IN_CHILD_CONTEXT,
|
|
2161
|
+
}, stepData, context);
|
|
2162
|
+
// Two-phase execution: Phase 1 starts immediately, Phase 2 returns result when awaited
|
|
2163
|
+
let phase1Result;
|
|
2164
|
+
let phase1Error;
|
|
2165
|
+
// Phase 1: Start execution immediately and capture result/error
|
|
2166
|
+
const phase1Promise = (async () => {
|
|
2167
|
+
const currentStepData = context.getStepData(entityId);
|
|
2168
|
+
// If already completed, return cached result
|
|
2169
|
+
if (currentStepData?.Status === clientLambda.OperationStatus.SUCCEEDED ||
|
|
2170
|
+
currentStepData?.Status === clientLambda.OperationStatus.FAILED) {
|
|
2171
|
+
return handleCompletedChildContext(context, parentContext, entityId, name, fn, options, getParentLogger, createChildContext);
|
|
2172
|
+
}
|
|
2173
|
+
// Execute if not completed
|
|
2174
|
+
return executeChildContext(context, checkpoint, parentContext, entityId, name, fn, options, getParentLogger, createChildContext, parentId);
|
|
2175
|
+
})()
|
|
2176
|
+
.then((result) => {
|
|
2177
|
+
phase1Result = result;
|
|
2178
|
+
})
|
|
2179
|
+
.catch((error) => {
|
|
2180
|
+
phase1Error = error;
|
|
2181
|
+
});
|
|
2182
|
+
// Phase 2: Return DurablePromise that returns Phase 1 result when awaited
|
|
2183
|
+
return new DurablePromise(async () => {
|
|
2184
|
+
await phase1Promise;
|
|
2185
|
+
if (phase1Error !== undefined) {
|
|
2186
|
+
throw phase1Error;
|
|
2187
|
+
}
|
|
2188
|
+
return phase1Result;
|
|
2189
|
+
});
|
|
2190
|
+
};
|
|
2191
|
+
};
|
|
2192
|
+
const handleCompletedChildContext = async (context, parentContext, entityId, stepName, fn, options, getParentLogger, createChildContext) => {
|
|
2193
|
+
const serdes = options?.serdes || defaultSerdes;
|
|
2194
|
+
const stepData = context.getStepData(entityId);
|
|
2195
|
+
const result = stepData?.ContextDetails?.Result;
|
|
2196
|
+
// Handle failed child context
|
|
2197
|
+
if (stepData?.Status === clientLambda.OperationStatus.FAILED) {
|
|
2198
|
+
if (stepData.ContextDetails?.Error) {
|
|
2199
|
+
const originalError = DurableOperationError.fromErrorObject(stepData.ContextDetails.Error);
|
|
2200
|
+
throw new ChildContextError(originalError.message, originalError);
|
|
2201
|
+
}
|
|
2202
|
+
else {
|
|
2203
|
+
throw new ChildContextError("Child context failed");
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
// Check if we need to replay children due to large payload
|
|
2207
|
+
if (stepData?.ContextDetails?.ReplayChildren) {
|
|
2208
|
+
log("🔄", "ReplayChildren mode: Re-executing child context due to large payload:", { entityId, stepName });
|
|
2209
|
+
// Re-execute the child context to reconstruct the result
|
|
2210
|
+
const durableChildContext = createChildContext(context, parentContext, DurableExecutionMode.ReplaySucceededContext, getParentLogger(), entityId, undefined, entityId);
|
|
2211
|
+
return await runWithContext(entityId, entityId, () => fn(durableChildContext));
|
|
2212
|
+
}
|
|
2213
|
+
log("⏭️", "Child context already finished, returning cached result:", {
|
|
2214
|
+
entityId,
|
|
2215
|
+
});
|
|
2216
|
+
return await safeDeserialize(serdes, result, entityId, stepName, context.terminationManager, context.durableExecutionArn);
|
|
2217
|
+
};
|
|
2218
|
+
const executeChildContext = async (context, checkpoint, parentContext, entityId, name, fn, options, getParentLogger, createChildContext, parentId) => {
|
|
2219
|
+
const serdes = options?.serdes || defaultSerdes;
|
|
2220
|
+
// Checkpoint at start if not already started (fire-and-forget for performance)
|
|
2221
|
+
if (context.getStepData(entityId) === undefined) {
|
|
2222
|
+
const subType = options?.subType || exports.OperationSubType.RUN_IN_CHILD_CONTEXT;
|
|
2223
|
+
checkpoint.checkpoint(entityId, {
|
|
2224
|
+
Id: entityId,
|
|
2225
|
+
ParentId: parentId,
|
|
2226
|
+
Action: clientLambda.OperationAction.START,
|
|
2227
|
+
SubType: subType,
|
|
2228
|
+
Type: clientLambda.OperationType.CONTEXT,
|
|
2229
|
+
Name: name,
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
const childReplayMode = determineChildReplayMode(context, entityId);
|
|
2233
|
+
// Create a child context with the entity ID as prefix
|
|
2234
|
+
const durableChildContext = createChildContext(context, parentContext, childReplayMode, getParentLogger(), entityId, undefined, entityId);
|
|
2235
|
+
try {
|
|
2236
|
+
// Execute the child context function with context tracking
|
|
2237
|
+
const result = await runWithContext(entityId, parentId, () => fn(durableChildContext), undefined, childReplayMode);
|
|
2238
|
+
// Serialize the result for consistency
|
|
2239
|
+
const serializedResult = await safeSerialize(serdes, result, entityId, name, context.terminationManager, context.durableExecutionArn);
|
|
2240
|
+
// Check if payload is too large for adaptive mode
|
|
2241
|
+
let payloadToCheckpoint = serializedResult;
|
|
2242
|
+
let replayChildren = false;
|
|
2243
|
+
if (serializedResult &&
|
|
2244
|
+
Buffer.byteLength(serializedResult, "utf8") > CHECKPOINT_SIZE_LIMIT) {
|
|
2245
|
+
replayChildren = true;
|
|
2246
|
+
// Use summary generator if provided, otherwise use empty string
|
|
2247
|
+
if (options?.summaryGenerator) {
|
|
2248
|
+
payloadToCheckpoint = options.summaryGenerator(result);
|
|
2249
|
+
}
|
|
2250
|
+
else {
|
|
2251
|
+
payloadToCheckpoint = "";
|
|
2252
|
+
}
|
|
2253
|
+
log("📦", "Large payload detected, using ReplayChildren mode:", {
|
|
2254
|
+
entityId,
|
|
2255
|
+
name,
|
|
2256
|
+
payloadSize: Buffer.byteLength(serializedResult, "utf8"),
|
|
2257
|
+
limit: CHECKPOINT_SIZE_LIMIT,
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
const subType = options?.subType || exports.OperationSubType.RUN_IN_CHILD_CONTEXT;
|
|
2261
|
+
await checkpoint.checkpoint(entityId, {
|
|
2262
|
+
Id: entityId,
|
|
2263
|
+
ParentId: parentId,
|
|
2264
|
+
Action: clientLambda.OperationAction.SUCCEED,
|
|
2265
|
+
SubType: subType,
|
|
2266
|
+
Type: clientLambda.OperationType.CONTEXT,
|
|
2267
|
+
Payload: payloadToCheckpoint,
|
|
2268
|
+
ContextOptions: replayChildren ? { ReplayChildren: true } : undefined,
|
|
2269
|
+
Name: name,
|
|
2270
|
+
});
|
|
2271
|
+
log("✅", "Child context completed successfully:", {
|
|
2272
|
+
entityId,
|
|
2273
|
+
name,
|
|
2274
|
+
});
|
|
2275
|
+
return result;
|
|
2276
|
+
}
|
|
2277
|
+
catch (error) {
|
|
2278
|
+
log("❌", "Child context failed:", {
|
|
2279
|
+
entityId,
|
|
2280
|
+
name,
|
|
2281
|
+
error,
|
|
2282
|
+
});
|
|
2283
|
+
// Always checkpoint failures
|
|
2284
|
+
const subType = options?.subType || exports.OperationSubType.RUN_IN_CHILD_CONTEXT;
|
|
2285
|
+
await checkpoint.checkpoint(entityId, {
|
|
2286
|
+
Id: entityId,
|
|
2287
|
+
ParentId: parentId,
|
|
2288
|
+
Action: clientLambda.OperationAction.FAIL,
|
|
2289
|
+
SubType: subType,
|
|
2290
|
+
Type: clientLambda.OperationType.CONTEXT,
|
|
2291
|
+
Error: createErrorObjectFromError(error),
|
|
2292
|
+
Name: name,
|
|
2293
|
+
});
|
|
2294
|
+
// Reconstruct error from ErrorObject for deterministic behavior
|
|
2295
|
+
const errorObject = createErrorObjectFromError(error);
|
|
2296
|
+
const reconstructedError = DurableOperationError.fromErrorObject(errorObject);
|
|
2297
|
+
throw new ChildContextError(reconstructedError.message, reconstructedError);
|
|
2298
|
+
}
|
|
2299
|
+
};
|
|
2300
|
+
|
|
2301
|
+
const createWaitHandler = (context, checkpoint, createStepId, hasRunningOperations, getOperationsEmitter, parentId, checkAndUpdateReplayMode) => {
|
|
2302
|
+
function waitHandler(nameOrDuration, duration) {
|
|
2303
|
+
const isNameFirst = typeof nameOrDuration === "string";
|
|
2304
|
+
const actualName = isNameFirst ? nameOrDuration : undefined;
|
|
2305
|
+
const actualDuration = isNameFirst ? duration : nameOrDuration;
|
|
2306
|
+
const actualSeconds = durationToSeconds(actualDuration);
|
|
2307
|
+
const stepId = createStepId();
|
|
2308
|
+
// Shared wait logic for both phases
|
|
2309
|
+
const executeWaitLogic = async (canTerminate) => {
|
|
2310
|
+
log("⏲️", `Wait executing (${canTerminate ? "phase 2" : "phase 1"}):`, {
|
|
2311
|
+
stepId,
|
|
2312
|
+
name: actualName,
|
|
2313
|
+
duration: actualDuration,
|
|
2314
|
+
seconds: actualSeconds,
|
|
2315
|
+
});
|
|
2316
|
+
let stepData = context.getStepData(stepId);
|
|
2317
|
+
// Validate replay consistency once before loop
|
|
2318
|
+
validateReplayConsistency(stepId, {
|
|
2319
|
+
type: clientLambda.OperationType.WAIT,
|
|
2320
|
+
name: actualName,
|
|
2321
|
+
subType: exports.OperationSubType.WAIT,
|
|
2322
|
+
}, stepData, context);
|
|
2323
|
+
// Main wait logic - can be re-executed if step data changes
|
|
2324
|
+
while (true) {
|
|
2325
|
+
stepData = context.getStepData(stepId);
|
|
2326
|
+
if (stepData?.Status === clientLambda.OperationStatus.SUCCEEDED) {
|
|
2327
|
+
log("⏭️", "Wait already completed:", { stepId });
|
|
2328
|
+
checkAndUpdateReplayMode?.();
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
// Only checkpoint START if we haven't started this wait before
|
|
2332
|
+
if (!stepData) {
|
|
2333
|
+
await checkpoint.checkpoint(stepId, {
|
|
2334
|
+
Id: stepId,
|
|
2335
|
+
ParentId: parentId,
|
|
2336
|
+
Action: clientLambda.OperationAction.START,
|
|
2337
|
+
SubType: exports.OperationSubType.WAIT,
|
|
2338
|
+
Type: clientLambda.OperationType.WAIT,
|
|
2339
|
+
Name: actualName,
|
|
2340
|
+
WaitOptions: {
|
|
2341
|
+
WaitSeconds: actualSeconds,
|
|
2342
|
+
},
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2345
|
+
// Always refresh stepData to ensure it's up-to-date before proceeding
|
|
2346
|
+
stepData = context.getStepData(stepId);
|
|
2347
|
+
// Check if there are any ongoing operations
|
|
2348
|
+
if (!hasRunningOperations()) {
|
|
2349
|
+
// Phase 1: Just return without terminating
|
|
2350
|
+
// Phase 2: Terminate
|
|
2351
|
+
if (canTerminate) {
|
|
2352
|
+
return terminate(context, TerminationReason.WAIT_SCHEDULED, `Operation ${actualName || stepId} scheduled to wait`);
|
|
2353
|
+
}
|
|
2354
|
+
else {
|
|
2355
|
+
log("⏸️", "Wait ready but not terminating (phase 1):", { stepId });
|
|
2356
|
+
return;
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
// There are ongoing operations - wait before continuing
|
|
2360
|
+
await waitBeforeContinue({
|
|
2361
|
+
checkHasRunningOperations: true,
|
|
2362
|
+
checkStepStatus: true,
|
|
2363
|
+
checkTimer: true,
|
|
2364
|
+
scheduledEndTimestamp: stepData?.WaitDetails?.ScheduledEndTimestamp,
|
|
2365
|
+
stepId,
|
|
2366
|
+
context,
|
|
2367
|
+
hasRunningOperations,
|
|
2368
|
+
operationsEmitter: getOperationsEmitter(),
|
|
2369
|
+
checkpoint,
|
|
2370
|
+
});
|
|
2371
|
+
// Continue the loop to re-evaluate all conditions from the beginning
|
|
2372
|
+
}
|
|
2373
|
+
};
|
|
2374
|
+
// Create a promise that tracks phase 1 completion
|
|
2375
|
+
const phase1Promise = executeWaitLogic(false).then(() => {
|
|
2376
|
+
log("✅", "Wait phase 1 complete:", { stepId, name: actualName });
|
|
2377
|
+
});
|
|
2378
|
+
// Attach catch handler to prevent unhandled promise rejections
|
|
2379
|
+
// The error will still be thrown when the DurablePromise is awaited
|
|
2380
|
+
phase1Promise.catch(() => { });
|
|
2381
|
+
// Return DurablePromise that will execute phase 2 when awaited
|
|
2382
|
+
return new DurablePromise(async () => {
|
|
2383
|
+
// Wait for phase 1 to complete first
|
|
2384
|
+
await phase1Promise;
|
|
2385
|
+
// Then execute phase 2
|
|
2386
|
+
await executeWaitLogic(true);
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2389
|
+
return waitHandler;
|
|
2390
|
+
};
|
|
2391
|
+
|
|
2392
|
+
// Special symbol to indicate that the main loop should continue
|
|
2393
|
+
const CONTINUE_MAIN_LOOP = Symbol("CONTINUE_MAIN_LOOP");
|
|
2394
|
+
const waitForContinuation = async (context, stepId, name, hasRunningOperations, checkpoint, operationsEmitter, onAwaitedChange) => {
|
|
2395
|
+
const stepData = context.getStepData(stepId);
|
|
2396
|
+
// Check if there are any ongoing operations
|
|
2397
|
+
if (!hasRunningOperations()) {
|
|
2398
|
+
// No ongoing operations - safe to terminate
|
|
2399
|
+
return terminate(context, TerminationReason.RETRY_SCHEDULED, `Retry scheduled for ${name || stepId}`);
|
|
2400
|
+
}
|
|
2401
|
+
// There are ongoing operations - wait before continuing
|
|
2402
|
+
await waitBeforeContinue({
|
|
2403
|
+
checkHasRunningOperations: true,
|
|
2404
|
+
checkStepStatus: true,
|
|
2405
|
+
checkTimer: true,
|
|
2406
|
+
scheduledEndTimestamp: stepData?.StepDetails?.NextAttemptTimestamp,
|
|
2407
|
+
stepId,
|
|
2408
|
+
context,
|
|
2409
|
+
hasRunningOperations,
|
|
2410
|
+
operationsEmitter,
|
|
2411
|
+
checkpoint,
|
|
2412
|
+
onAwaitedChange,
|
|
2413
|
+
});
|
|
2414
|
+
// Return to let the main loop re-evaluate step status
|
|
2415
|
+
};
|
|
2416
|
+
const createWaitForConditionHandler = (context, checkpoint, createStepId, logger, addRunningOperation, removeRunningOperation, hasRunningOperations, getOperationsEmitter, parentId) => {
|
|
2417
|
+
return (nameOrCheck, checkOrConfig, maybeConfig) => {
|
|
2418
|
+
// Two-phase execution: Phase 1 starts immediately, Phase 2 returns result when awaited
|
|
2419
|
+
let isAwaited = false;
|
|
2420
|
+
let waitingCallback;
|
|
2421
|
+
const setWaitingCallback = (cb) => {
|
|
2422
|
+
waitingCallback = cb;
|
|
2423
|
+
};
|
|
2424
|
+
// Phase 1: Start execution immediately and capture result/error
|
|
2425
|
+
const phase1Promise = (async () => {
|
|
2426
|
+
let name;
|
|
2427
|
+
let check;
|
|
2428
|
+
let config;
|
|
2429
|
+
// Parse overloaded parameters - validation errors thrown here are async
|
|
2430
|
+
if (typeof nameOrCheck === "string" || nameOrCheck === undefined) {
|
|
2431
|
+
name = nameOrCheck;
|
|
2432
|
+
check = checkOrConfig;
|
|
2433
|
+
config = maybeConfig;
|
|
2434
|
+
}
|
|
2435
|
+
else {
|
|
2436
|
+
check = nameOrCheck;
|
|
2437
|
+
config = checkOrConfig;
|
|
2438
|
+
}
|
|
2439
|
+
if (!config ||
|
|
2440
|
+
!config.waitStrategy ||
|
|
2441
|
+
config.initialState === undefined) {
|
|
2442
|
+
throw new Error("waitForCondition requires config with waitStrategy and initialState");
|
|
2443
|
+
}
|
|
2444
|
+
const stepId = createStepId();
|
|
2445
|
+
log("🔄", "Running waitForCondition:", {
|
|
2446
|
+
stepId,
|
|
2447
|
+
name,
|
|
2448
|
+
config,
|
|
2449
|
+
});
|
|
2450
|
+
// Main waitForCondition logic - can be re-executed if step status changes
|
|
2451
|
+
while (true) {
|
|
2452
|
+
try {
|
|
2453
|
+
const stepData = context.getStepData(stepId);
|
|
2454
|
+
// Check if already completed
|
|
2455
|
+
if (stepData?.Status === clientLambda.OperationStatus.SUCCEEDED) {
|
|
2456
|
+
return await handleCompletedWaitForCondition(context, stepId, name, config.serdes);
|
|
2457
|
+
}
|
|
2458
|
+
if (stepData?.Status === clientLambda.OperationStatus.FAILED) {
|
|
2459
|
+
// Return an async rejected promise to ensure it's handled asynchronously
|
|
2460
|
+
return (async () => {
|
|
2461
|
+
// Reconstruct the original error from stored ErrorObject
|
|
2462
|
+
if (stepData.StepDetails?.Error) {
|
|
2463
|
+
throw DurableOperationError.fromErrorObject(stepData.StepDetails.Error);
|
|
2464
|
+
}
|
|
2465
|
+
else {
|
|
2466
|
+
// Fallback for legacy data without Error field
|
|
2467
|
+
const errorMessage = stepData?.StepDetails?.Result;
|
|
2468
|
+
throw new WaitForConditionError(errorMessage || "waitForCondition failed");
|
|
2469
|
+
}
|
|
2470
|
+
})();
|
|
2471
|
+
}
|
|
2472
|
+
// If PENDING, wait for timer to complete
|
|
2473
|
+
if (stepData?.Status === clientLambda.OperationStatus.PENDING) {
|
|
2474
|
+
await waitForContinuation(context, stepId, name, hasRunningOperations, checkpoint, getOperationsEmitter(), isAwaited ? undefined : setWaitingCallback);
|
|
2475
|
+
continue; // Re-evaluate step status after waiting
|
|
2476
|
+
}
|
|
2477
|
+
// Execute check function for READY, STARTED, or first time (undefined)
|
|
2478
|
+
const result = await executeWaitForCondition(context, checkpoint, stepId, name, check, config, logger, addRunningOperation, removeRunningOperation, hasRunningOperations, getOperationsEmitter, parentId, isAwaited ? undefined : setWaitingCallback);
|
|
2479
|
+
// If executeWaitForCondition signals to continue the main loop, do so
|
|
2480
|
+
if (result === CONTINUE_MAIN_LOOP) {
|
|
2481
|
+
continue;
|
|
2482
|
+
}
|
|
2483
|
+
return result;
|
|
2484
|
+
}
|
|
2485
|
+
catch (error) {
|
|
2486
|
+
// For any error from executeWaitForCondition, re-throw it
|
|
2487
|
+
throw error;
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
})();
|
|
2491
|
+
// Attach catch handler to prevent unhandled promise rejections
|
|
2492
|
+
// The error will still be thrown when the DurablePromise is awaited
|
|
2493
|
+
phase1Promise.catch(() => { });
|
|
2494
|
+
// Phase 2: Return DurablePromise that returns Phase 1 result when awaited
|
|
2495
|
+
return new DurablePromise(async () => {
|
|
2496
|
+
// When promise is awaited, mark as awaited and invoke waiting callback
|
|
2497
|
+
isAwaited = true;
|
|
2498
|
+
if (waitingCallback) {
|
|
2499
|
+
waitingCallback();
|
|
2500
|
+
}
|
|
2501
|
+
return await phase1Promise;
|
|
2502
|
+
});
|
|
2503
|
+
};
|
|
2504
|
+
};
|
|
2505
|
+
const handleCompletedWaitForCondition = async (context, stepId, stepName, serdes = defaultSerdes) => {
|
|
2506
|
+
log("⏭️", "waitForCondition already finished, returning cached result:", {
|
|
2507
|
+
stepId,
|
|
2508
|
+
});
|
|
2509
|
+
const stepData = context.getStepData(stepId);
|
|
2510
|
+
const result = stepData?.StepDetails?.Result;
|
|
2511
|
+
return await safeDeserialize(serdes, result, stepId, stepName, context.terminationManager, context.durableExecutionArn);
|
|
2512
|
+
};
|
|
2513
|
+
const executeWaitForCondition = async (context, checkpoint, stepId, name, check, config, logger, addRunningOperation, removeRunningOperation, hasRunningOperations, getOperationsEmitter, parentId, onAwaitedChange) => {
|
|
2514
|
+
const serdes = config.serdes || defaultSerdes;
|
|
2515
|
+
// Get current state from previous checkpoint or use initial state
|
|
2516
|
+
let currentState;
|
|
2517
|
+
const existingOperation = context.getStepData(stepId);
|
|
2518
|
+
if (existingOperation?.Status === clientLambda.OperationStatus.STARTED ||
|
|
2519
|
+
existingOperation?.Status === clientLambda.OperationStatus.READY) {
|
|
2520
|
+
// This is a retry - get state from previous checkpoint
|
|
2521
|
+
const checkpointData = existingOperation.StepDetails?.Result;
|
|
2522
|
+
if (checkpointData) {
|
|
2523
|
+
try {
|
|
2524
|
+
// Try to deserialize the checkpoint data directly
|
|
2525
|
+
const serdesContext = {
|
|
2526
|
+
entityId: stepId,
|
|
2527
|
+
durableExecutionArn: context.durableExecutionArn,
|
|
2528
|
+
};
|
|
2529
|
+
currentState = await serdes.deserialize(checkpointData, serdesContext);
|
|
2530
|
+
}
|
|
2531
|
+
catch (error) {
|
|
2532
|
+
log("⚠️", "Failed to deserialize checkpoint data, using initial state:", {
|
|
2533
|
+
stepId,
|
|
2534
|
+
name,
|
|
2535
|
+
error,
|
|
2536
|
+
});
|
|
2537
|
+
currentState = config.initialState;
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
else {
|
|
2541
|
+
currentState = config.initialState;
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
else {
|
|
2545
|
+
// First execution
|
|
2546
|
+
currentState = config.initialState;
|
|
2547
|
+
}
|
|
2548
|
+
// Get the current attempt number (1-based for wait strategy consistency)
|
|
2549
|
+
const currentAttempt = existingOperation?.StepDetails?.Attempt || 1;
|
|
2550
|
+
// Checkpoint START for observability (fire and forget) - only if not already started
|
|
2551
|
+
const stepData = context.getStepData(stepId);
|
|
2552
|
+
if (stepData?.Status !== clientLambda.OperationStatus.STARTED) {
|
|
2553
|
+
checkpoint.checkpoint(stepId, {
|
|
2554
|
+
Id: stepId,
|
|
2555
|
+
ParentId: parentId,
|
|
2556
|
+
Action: clientLambda.OperationAction.START,
|
|
2557
|
+
SubType: exports.OperationSubType.WAIT_FOR_CONDITION,
|
|
2558
|
+
Type: clientLambda.OperationType.STEP,
|
|
2559
|
+
Name: name,
|
|
2560
|
+
});
|
|
2561
|
+
}
|
|
2562
|
+
try {
|
|
2563
|
+
// Create WaitForConditionContext with enriched logger for the check function
|
|
2564
|
+
const waitForConditionContext = {
|
|
2565
|
+
logger,
|
|
2566
|
+
};
|
|
2567
|
+
// Execute the check function
|
|
2568
|
+
addRunningOperation(stepId);
|
|
2569
|
+
let newState;
|
|
2570
|
+
try {
|
|
2571
|
+
newState = await runWithContext(stepId, parentId, () => check(currentState, waitForConditionContext), currentAttempt + 1, DurableExecutionMode.ExecutionMode);
|
|
2572
|
+
}
|
|
2573
|
+
finally {
|
|
2574
|
+
removeRunningOperation(stepId);
|
|
2575
|
+
}
|
|
2576
|
+
// Serialize the new state for consistency
|
|
2577
|
+
const serializedState = await safeSerialize(serdes, newState, stepId, name, context.terminationManager, context.durableExecutionArn);
|
|
2578
|
+
// Deserialize for consistency with replay behavior
|
|
2579
|
+
const deserializedState = await safeDeserialize(serdes, serializedState, stepId, name, context.terminationManager, context.durableExecutionArn);
|
|
2580
|
+
// Check if condition is met using the wait strategy
|
|
2581
|
+
const decision = config.waitStrategy(deserializedState, currentAttempt);
|
|
2582
|
+
log("🔍", "waitForCondition check completed:", {
|
|
2583
|
+
stepId,
|
|
2584
|
+
name,
|
|
2585
|
+
currentAttempt: currentAttempt,
|
|
2586
|
+
shouldContinue: decision.shouldContinue,
|
|
2587
|
+
delayInSeconds: decision.shouldContinue
|
|
2588
|
+
? durationToSeconds(decision.delay)
|
|
2589
|
+
: undefined,
|
|
2590
|
+
});
|
|
2591
|
+
if (!decision.shouldContinue) {
|
|
2592
|
+
// Condition is met - complete successfully
|
|
2593
|
+
await checkpoint.checkpoint(stepId, {
|
|
2594
|
+
Id: stepId,
|
|
2595
|
+
ParentId: parentId,
|
|
2596
|
+
Action: clientLambda.OperationAction.SUCCEED,
|
|
2597
|
+
SubType: exports.OperationSubType.WAIT_FOR_CONDITION,
|
|
2598
|
+
Type: clientLambda.OperationType.STEP,
|
|
2599
|
+
Payload: serializedState,
|
|
2600
|
+
Name: name,
|
|
2601
|
+
});
|
|
2602
|
+
log("✅", "waitForCondition completed successfully:", {
|
|
2603
|
+
stepId,
|
|
2604
|
+
name,
|
|
2605
|
+
result: deserializedState,
|
|
2606
|
+
totalAttempts: currentAttempt,
|
|
2607
|
+
});
|
|
2608
|
+
return deserializedState;
|
|
2609
|
+
}
|
|
2610
|
+
else {
|
|
2611
|
+
// Condition not met - schedule retry
|
|
2612
|
+
// Only checkpoint the state, not the attempt number (system handles that)
|
|
2613
|
+
await checkpoint.checkpoint(stepId, {
|
|
2614
|
+
Id: stepId,
|
|
2615
|
+
ParentId: parentId,
|
|
2616
|
+
Action: clientLambda.OperationAction.RETRY,
|
|
2617
|
+
SubType: exports.OperationSubType.WAIT_FOR_CONDITION,
|
|
2618
|
+
Type: clientLambda.OperationType.STEP,
|
|
2619
|
+
Payload: serializedState, // Just the state, not wrapped in an object
|
|
2620
|
+
Name: name,
|
|
2621
|
+
StepOptions: {
|
|
2622
|
+
NextAttemptDelaySeconds: durationToSeconds(decision.delay),
|
|
2623
|
+
},
|
|
2624
|
+
});
|
|
2625
|
+
// Wait for continuation and signal main loop to continue
|
|
2626
|
+
await waitForContinuation(context, stepId, name, hasRunningOperations, checkpoint, getOperationsEmitter(), onAwaitedChange);
|
|
2627
|
+
return CONTINUE_MAIN_LOOP;
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
catch (error) {
|
|
2631
|
+
log("❌", "waitForCondition check function failed:", {
|
|
2632
|
+
stepId,
|
|
2633
|
+
name,
|
|
2634
|
+
error,
|
|
2635
|
+
currentAttempt: currentAttempt,
|
|
2636
|
+
});
|
|
2637
|
+
// Mark as failed - waitForCondition doesn't have its own retry logic for errors
|
|
2638
|
+
// If the check function throws, it's considered a failure
|
|
2639
|
+
await checkpoint.checkpoint(stepId, {
|
|
2640
|
+
Id: stepId,
|
|
2641
|
+
ParentId: parentId,
|
|
2642
|
+
Action: clientLambda.OperationAction.FAIL,
|
|
2643
|
+
SubType: exports.OperationSubType.WAIT_FOR_CONDITION,
|
|
2644
|
+
Type: clientLambda.OperationType.STEP,
|
|
2645
|
+
Error: createErrorObjectFromError(error),
|
|
2646
|
+
Name: name,
|
|
2647
|
+
});
|
|
2648
|
+
// Reconstruct error from ErrorObject for deterministic behavior
|
|
2649
|
+
const errorObject = createErrorObjectFromError(error);
|
|
2650
|
+
throw DurableOperationError.fromErrorObject(errorObject);
|
|
2651
|
+
}
|
|
2652
|
+
};
|
|
2653
|
+
|
|
2654
|
+
const createCallbackPromise = (context, stepId, stepName, serdes, hasRunningOperations, operationsEmitter, terminationMessage, checkAndUpdateReplayMode) => {
|
|
2655
|
+
return new DurablePromise(async () => {
|
|
2656
|
+
log("🔄", "Callback promise phase 2 executing:", { stepId, stepName });
|
|
2657
|
+
// Main callback logic - can be re-executed if step status changes
|
|
2658
|
+
while (true) {
|
|
2659
|
+
const stepData = context.getStepData(stepId);
|
|
2660
|
+
// Handle case where stepData doesn't exist yet
|
|
2661
|
+
// While Phase 1 should create stepData via checkpoint before Phase 2 starts,
|
|
2662
|
+
// this can be undefined in test scenarios
|
|
2663
|
+
if (!stepData) {
|
|
2664
|
+
log("⚠️", "Step data not found, waiting for callback creation:", {
|
|
2665
|
+
stepId,
|
|
2666
|
+
});
|
|
2667
|
+
if (hasRunningOperations()) {
|
|
2668
|
+
await waitBeforeContinue({
|
|
2669
|
+
checkHasRunningOperations: true,
|
|
2670
|
+
checkStepStatus: true,
|
|
2671
|
+
checkTimer: false,
|
|
2672
|
+
stepId,
|
|
2673
|
+
context,
|
|
2674
|
+
hasRunningOperations,
|
|
2675
|
+
operationsEmitter,
|
|
2676
|
+
});
|
|
2677
|
+
continue; // Re-evaluate after waiting
|
|
2678
|
+
}
|
|
2679
|
+
// No other operations and no step data - terminate gracefully
|
|
2680
|
+
log("⏳", "No step data found and no running operations, terminating");
|
|
2681
|
+
return terminate(context, TerminationReason.CALLBACK_PENDING, terminationMessage);
|
|
2682
|
+
}
|
|
2683
|
+
if (stepData.Status === clientLambda.OperationStatus.SUCCEEDED) {
|
|
2684
|
+
const callbackData = stepData.CallbackDetails;
|
|
2685
|
+
if (!callbackData?.CallbackId) {
|
|
2686
|
+
throw new CallbackError(`No callback ID found for completed callback: ${stepId}`);
|
|
2687
|
+
}
|
|
2688
|
+
const result = await safeDeserialize(serdes, callbackData.Result, stepId, stepName, context.terminationManager, context.durableExecutionArn);
|
|
2689
|
+
// Check and update replay mode after callback completion
|
|
2690
|
+
checkAndUpdateReplayMode();
|
|
2691
|
+
return result;
|
|
2692
|
+
}
|
|
2693
|
+
if (stepData.Status === clientLambda.OperationStatus.FAILED ||
|
|
2694
|
+
stepData.Status === clientLambda.OperationStatus.TIMED_OUT) {
|
|
2695
|
+
const callbackData = stepData.CallbackDetails;
|
|
2696
|
+
const error = callbackData?.Error;
|
|
2697
|
+
if (error) {
|
|
2698
|
+
const cause = new Error(error.ErrorMessage);
|
|
2699
|
+
cause.name = error.ErrorType || "Error";
|
|
2700
|
+
cause.stack = error.StackTrace?.join("\n");
|
|
2701
|
+
throw new CallbackError(error.ErrorMessage || "Callback failed", cause, error.ErrorData);
|
|
2702
|
+
}
|
|
2703
|
+
throw new CallbackError("Callback failed");
|
|
2704
|
+
}
|
|
2705
|
+
if (stepData.Status === clientLambda.OperationStatus.STARTED) {
|
|
2706
|
+
// Callback is still pending
|
|
2707
|
+
if (hasRunningOperations()) {
|
|
2708
|
+
// Wait for other operations or callback completion
|
|
2709
|
+
log("⏳", "Callback still pending, waiting for other operations");
|
|
2710
|
+
await waitBeforeContinue({
|
|
2711
|
+
checkHasRunningOperations: true,
|
|
2712
|
+
checkStepStatus: true,
|
|
2713
|
+
checkTimer: false,
|
|
2714
|
+
stepId,
|
|
2715
|
+
context,
|
|
2716
|
+
hasRunningOperations,
|
|
2717
|
+
operationsEmitter,
|
|
2718
|
+
});
|
|
2719
|
+
continue; // Re-evaluate status after waiting
|
|
2720
|
+
}
|
|
2721
|
+
// No other operations running - terminate
|
|
2722
|
+
log("⏳", "Callback still pending, terminating");
|
|
2723
|
+
return terminate(context, TerminationReason.CALLBACK_PENDING, terminationMessage);
|
|
2724
|
+
}
|
|
2725
|
+
// Should not reach here, but handle unexpected status
|
|
2726
|
+
throw new CallbackError(`Unexpected callback status: ${stepData.Status}`);
|
|
2727
|
+
}
|
|
2728
|
+
});
|
|
2729
|
+
};
|
|
2730
|
+
|
|
2731
|
+
const createPassThroughSerdes = () => ({
|
|
2732
|
+
serialize: async (value) => value,
|
|
2733
|
+
deserialize: async (data) => data,
|
|
2734
|
+
});
|
|
2735
|
+
const createCallback = (context, checkpoint, createStepId, hasRunningOperations, getOperationsEmitter, checkAndUpdateReplayMode, parentId) => {
|
|
2736
|
+
return (nameOrConfig, maybeConfig) => {
|
|
2737
|
+
let name;
|
|
2738
|
+
let config;
|
|
2739
|
+
if (typeof nameOrConfig === "string" || nameOrConfig === undefined) {
|
|
2740
|
+
name = nameOrConfig;
|
|
2741
|
+
config = maybeConfig;
|
|
2742
|
+
}
|
|
2743
|
+
else {
|
|
2744
|
+
config = nameOrConfig;
|
|
2745
|
+
}
|
|
2746
|
+
const stepId = createStepId();
|
|
2747
|
+
const serdes = config?.serdes || createPassThroughSerdes();
|
|
2748
|
+
// Validate replay consistency first
|
|
2749
|
+
const stepData = context.getStepData(stepId);
|
|
2750
|
+
validateReplayConsistency(stepId, {
|
|
2751
|
+
type: clientLambda.OperationType.CALLBACK,
|
|
2752
|
+
name,
|
|
2753
|
+
subType: exports.OperationSubType.CALLBACK,
|
|
2754
|
+
}, stepData, context);
|
|
2755
|
+
// Phase 1: Setup and checkpoint (immediate execution)
|
|
2756
|
+
const setupPromise = (async () => {
|
|
2757
|
+
log("📞", "Creating callback phase 1:", { stepId, name, config });
|
|
2758
|
+
// Handle already completed callbacks
|
|
2759
|
+
if (stepData?.Status === clientLambda.OperationStatus.SUCCEEDED) {
|
|
2760
|
+
log("⏭️", "Callback already completed in phase 1:", { stepId });
|
|
2761
|
+
return { wasNewCallback: false };
|
|
2762
|
+
}
|
|
2763
|
+
if (stepData?.Status === clientLambda.OperationStatus.FAILED ||
|
|
2764
|
+
stepData?.Status === clientLambda.OperationStatus.TIMED_OUT) {
|
|
2765
|
+
log("❌", "Callback already failed in phase 1:", { stepId });
|
|
2766
|
+
return { wasNewCallback: false };
|
|
2767
|
+
}
|
|
2768
|
+
// Handle already started callbacks
|
|
2769
|
+
if (stepData?.Status === clientLambda.OperationStatus.STARTED) {
|
|
2770
|
+
log("⏳", "Callback already started in phase 1:", { stepId });
|
|
2771
|
+
return { wasNewCallback: false };
|
|
2772
|
+
}
|
|
2773
|
+
// Create new callback - checkpoint START operation
|
|
2774
|
+
log("🆕", "Creating new callback in phase 1:", { stepId, name });
|
|
2775
|
+
await checkpoint.checkpoint(stepId, {
|
|
2776
|
+
Id: stepId,
|
|
2777
|
+
ParentId: parentId,
|
|
2778
|
+
Action: "START",
|
|
2779
|
+
SubType: exports.OperationSubType.CALLBACK,
|
|
2780
|
+
Type: clientLambda.OperationType.CALLBACK,
|
|
2781
|
+
Name: name,
|
|
2782
|
+
CallbackOptions: {
|
|
2783
|
+
TimeoutSeconds: config?.timeout
|
|
2784
|
+
? durationToSeconds(config.timeout)
|
|
2785
|
+
: undefined,
|
|
2786
|
+
HeartbeatTimeoutSeconds: config?.heartbeatTimeout
|
|
2787
|
+
? durationToSeconds(config.heartbeatTimeout)
|
|
2788
|
+
: undefined,
|
|
2789
|
+
},
|
|
2790
|
+
});
|
|
2791
|
+
log("✅", "Callback checkpoint completed in phase 1:", { stepId });
|
|
2792
|
+
return { wasNewCallback: true };
|
|
2793
|
+
})().catch((error) => {
|
|
2794
|
+
log("❌", "Callback phase 1 error:", { stepId, error: error.message });
|
|
2795
|
+
throw error;
|
|
2796
|
+
});
|
|
2797
|
+
// Return DurablePromise that executes phase 2 when awaited
|
|
2798
|
+
return new DurablePromise(async () => {
|
|
2799
|
+
// Wait for phase 1 to complete
|
|
2800
|
+
const { wasNewCallback } = await setupPromise;
|
|
2801
|
+
// Phase 2: Handle results and create callback promise
|
|
2802
|
+
log("🔄", "Callback phase 2 executing:", { stepId, name });
|
|
2803
|
+
const stepData = context.getStepData(stepId);
|
|
2804
|
+
// Handle completed callbacks
|
|
2805
|
+
if (stepData?.Status === clientLambda.OperationStatus.SUCCEEDED) {
|
|
2806
|
+
const callbackData = stepData.CallbackDetails;
|
|
2807
|
+
if (!callbackData?.CallbackId) {
|
|
2808
|
+
throw new CallbackError(`No callback ID found for completed callback: ${stepId}`);
|
|
2809
|
+
}
|
|
2810
|
+
const deserializedResult = await safeDeserialize(serdes, callbackData.Result, stepId, name, context.terminationManager, context.durableExecutionArn);
|
|
2811
|
+
const resolvedPromise = new DurablePromise(async () => deserializedResult);
|
|
2812
|
+
// Check and update replay mode after callback completion
|
|
2813
|
+
checkAndUpdateReplayMode();
|
|
2814
|
+
return [resolvedPromise, callbackData.CallbackId];
|
|
2815
|
+
}
|
|
2816
|
+
// Handle failed callbacks
|
|
2817
|
+
if (stepData?.Status === clientLambda.OperationStatus.FAILED ||
|
|
2818
|
+
stepData?.Status === clientLambda.OperationStatus.TIMED_OUT) {
|
|
2819
|
+
const callbackData = stepData.CallbackDetails;
|
|
2820
|
+
if (!callbackData?.CallbackId) {
|
|
2821
|
+
throw new CallbackError(`No callback ID found for failed callback: ${stepId}`);
|
|
2822
|
+
}
|
|
2823
|
+
const error = stepData.CallbackDetails?.Error;
|
|
2824
|
+
const callbackError = error
|
|
2825
|
+
? (() => {
|
|
2826
|
+
const cause = new Error(error.ErrorMessage);
|
|
2827
|
+
cause.name = error.ErrorType || "Error";
|
|
2828
|
+
cause.stack = error.StackTrace?.join("\n");
|
|
2829
|
+
return new CallbackError(error.ErrorMessage || "Callback failed", cause, error.ErrorData);
|
|
2830
|
+
})()
|
|
2831
|
+
: new CallbackError("Callback failed");
|
|
2832
|
+
const rejectedPromise = new DurablePromise(async () => {
|
|
2833
|
+
throw callbackError;
|
|
2834
|
+
});
|
|
2835
|
+
return [rejectedPromise, callbackData.CallbackId];
|
|
2836
|
+
}
|
|
2837
|
+
// Handle started or new callbacks
|
|
2838
|
+
const callbackData = stepData?.CallbackDetails;
|
|
2839
|
+
if (!callbackData?.CallbackId) {
|
|
2840
|
+
const errorMessage = wasNewCallback
|
|
2841
|
+
? `Callback ID not found in stepData after checkpoint: ${stepId}`
|
|
2842
|
+
: `No callback ID found for started callback: ${stepId}`;
|
|
2843
|
+
throw new CallbackError(errorMessage);
|
|
2844
|
+
}
|
|
2845
|
+
const callbackId = callbackData.CallbackId;
|
|
2846
|
+
// Create callback promise that handles completion
|
|
2847
|
+
const terminationMessage = wasNewCallback
|
|
2848
|
+
? `Callback ${name || stepId} created and pending external completion`
|
|
2849
|
+
: `Callback ${name || stepId} is pending external completion`;
|
|
2850
|
+
const callbackPromise = createCallbackPromise(context, stepId, name, serdes, hasRunningOperations, getOperationsEmitter(), terminationMessage, checkAndUpdateReplayMode);
|
|
2851
|
+
log("✅", "Callback created successfully in phase 2:", {
|
|
2852
|
+
stepId,
|
|
2853
|
+
name,
|
|
2854
|
+
callbackId,
|
|
2855
|
+
});
|
|
2856
|
+
return [callbackPromise, callbackId];
|
|
2857
|
+
});
|
|
2858
|
+
};
|
|
2859
|
+
};
|
|
2860
|
+
|
|
2861
|
+
const createWaitForCallbackHandler = (context, getNextStepId, runInChildContext) => {
|
|
2862
|
+
return (nameOrSubmitter, submitterOrConfig, maybeConfig) => {
|
|
2863
|
+
let name;
|
|
2864
|
+
let submitter;
|
|
2865
|
+
let config;
|
|
2866
|
+
// Parse the overloaded parameters - validation errors thrown here are async
|
|
2867
|
+
if (typeof nameOrSubmitter === "string" || nameOrSubmitter === undefined) {
|
|
2868
|
+
// Case: waitForCallback("name", submitterFunc, config?) or waitForCallback(undefined, submitterFunc, config?)
|
|
2869
|
+
name = nameOrSubmitter;
|
|
2870
|
+
if (typeof submitterOrConfig === "function") {
|
|
2871
|
+
submitter = submitterOrConfig;
|
|
2872
|
+
config = maybeConfig;
|
|
2873
|
+
}
|
|
2874
|
+
else {
|
|
2875
|
+
return new DurablePromise(() => Promise.reject(new Error("waitForCallback requires a submitter function when name is provided")));
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
else if (typeof nameOrSubmitter === "function") {
|
|
2879
|
+
// Case: waitForCallback(submitterFunc, config?)
|
|
2880
|
+
submitter = nameOrSubmitter;
|
|
2881
|
+
config = submitterOrConfig;
|
|
2882
|
+
}
|
|
2883
|
+
else {
|
|
2884
|
+
return new DurablePromise(() => Promise.reject(new Error("waitForCallback requires a submitter function")));
|
|
2885
|
+
}
|
|
2886
|
+
// Two-phase execution: Phase 1 starts immediately, Phase 2 returns result when awaited
|
|
2887
|
+
// Phase 1: Start execution immediately and capture result/error
|
|
2888
|
+
const phase1Promise = (async () => {
|
|
2889
|
+
log("📞", "WaitForCallback requested:", {
|
|
2890
|
+
name,
|
|
2891
|
+
hasSubmitter: !!submitter,
|
|
2892
|
+
config,
|
|
2893
|
+
});
|
|
2894
|
+
// Use runInChildContext to ensure proper ID generation and isolation
|
|
2895
|
+
const childFunction = async (childCtx) => {
|
|
2896
|
+
// Convert WaitForCallbackConfig to CreateCallbackConfig
|
|
2897
|
+
const createCallbackConfig = config
|
|
2898
|
+
? {
|
|
2899
|
+
timeout: config.timeout,
|
|
2900
|
+
heartbeatTimeout: config.heartbeatTimeout,
|
|
2901
|
+
}
|
|
2902
|
+
: undefined;
|
|
2903
|
+
// Create callback and get the promise + callbackId
|
|
2904
|
+
const [callbackPromise, callbackId] = await childCtx.createCallback(createCallbackConfig);
|
|
2905
|
+
log("🆔", "Callback created:", {
|
|
2906
|
+
callbackId,
|
|
2907
|
+
name,
|
|
2908
|
+
});
|
|
2909
|
+
// Execute the submitter step (submitter is now mandatory)
|
|
2910
|
+
await childCtx.step(async (stepContext) => {
|
|
2911
|
+
// Use the step's built-in logger instead of creating a new one
|
|
2912
|
+
const callbackContext = {
|
|
2913
|
+
logger: stepContext.logger,
|
|
2914
|
+
};
|
|
2915
|
+
log("📤", "Executing submitter:", {
|
|
2916
|
+
callbackId,
|
|
2917
|
+
name,
|
|
2918
|
+
});
|
|
2919
|
+
await submitter(callbackId, callbackContext);
|
|
2920
|
+
log("✅", "Submitter completed:", {
|
|
2921
|
+
callbackId,
|
|
2922
|
+
name,
|
|
2923
|
+
});
|
|
2924
|
+
}, config?.retryStrategy
|
|
2925
|
+
? { retryStrategy: config.retryStrategy }
|
|
2926
|
+
: undefined);
|
|
2927
|
+
log("⏳", "Waiting for callback completion:", {
|
|
2928
|
+
callbackId,
|
|
2929
|
+
name,
|
|
2930
|
+
});
|
|
2931
|
+
// Return just the callback promise result
|
|
2932
|
+
return await callbackPromise;
|
|
2933
|
+
};
|
|
2934
|
+
const stepId = getNextStepId();
|
|
2935
|
+
return {
|
|
2936
|
+
result: await runInChildContext(name, childFunction, {
|
|
2937
|
+
subType: exports.OperationSubType.WAIT_FOR_CALLBACK,
|
|
2938
|
+
}),
|
|
2939
|
+
stepId,
|
|
2940
|
+
};
|
|
2941
|
+
})();
|
|
2942
|
+
// Attach catch handler to prevent unhandled promise rejections
|
|
2943
|
+
// The error will still be thrown when the DurablePromise is awaited
|
|
2944
|
+
phase1Promise.catch(() => { });
|
|
2945
|
+
// Phase 2: Return DurablePromise that returns Phase 1 result when awaited
|
|
2946
|
+
return new DurablePromise(async () => {
|
|
2947
|
+
const { result, stepId } = await phase1Promise;
|
|
2948
|
+
// Always deserialize the result since it's a string
|
|
2949
|
+
return (await safeDeserialize(config?.serdes ?? createPassThroughSerdes(), result, stepId, name, context.terminationManager, context.durableExecutionArn));
|
|
2950
|
+
});
|
|
2951
|
+
};
|
|
2952
|
+
};
|
|
2953
|
+
|
|
2954
|
+
/**
|
|
2955
|
+
* Creates a predefined summary generator for parallel operations
|
|
2956
|
+
*/
|
|
2957
|
+
const createParallelSummaryGenerator = () => (result) => {
|
|
2958
|
+
return JSON.stringify({
|
|
2959
|
+
type: "ParallelResult",
|
|
2960
|
+
totalCount: result.totalCount,
|
|
2961
|
+
successCount: result.successCount,
|
|
2962
|
+
failureCount: result.failureCount,
|
|
2963
|
+
startedCount: result.startedCount,
|
|
2964
|
+
completionReason: result.completionReason,
|
|
2965
|
+
status: result.status,
|
|
2966
|
+
});
|
|
2967
|
+
};
|
|
2968
|
+
/**
|
|
2969
|
+
* Creates a predefined summary generator for map operations
|
|
2970
|
+
*/
|
|
2971
|
+
const createMapSummaryGenerator = () => (result) => {
|
|
2972
|
+
return JSON.stringify({
|
|
2973
|
+
type: "MapResult",
|
|
2974
|
+
totalCount: result.totalCount,
|
|
2975
|
+
successCount: result.successCount,
|
|
2976
|
+
failureCount: result.failureCount,
|
|
2977
|
+
completionReason: result.completionReason,
|
|
2978
|
+
status: result.status,
|
|
2979
|
+
});
|
|
2980
|
+
};
|
|
2981
|
+
|
|
2982
|
+
const createMapHandler = (context, executeConcurrently) => {
|
|
2983
|
+
return (nameOrItems, itemsOrMapFunc, mapFuncOrConfig, maybeConfig) => {
|
|
2984
|
+
// Phase 1: Parse parameters and start execution immediately
|
|
2985
|
+
const phase1Promise = (async () => {
|
|
2986
|
+
let name;
|
|
2987
|
+
let items;
|
|
2988
|
+
let mapFunc;
|
|
2989
|
+
let config;
|
|
2990
|
+
// Parse overloaded parameters
|
|
2991
|
+
if (typeof nameOrItems === "string" || nameOrItems === undefined) {
|
|
2992
|
+
// Case: map(name, items, mapFunc, config?)
|
|
2993
|
+
name = nameOrItems;
|
|
2994
|
+
items = itemsOrMapFunc;
|
|
2995
|
+
mapFunc = mapFuncOrConfig;
|
|
2996
|
+
config = maybeConfig;
|
|
2997
|
+
}
|
|
2998
|
+
else {
|
|
2999
|
+
// Case: map(items, mapFunc, config?)
|
|
3000
|
+
items = nameOrItems;
|
|
3001
|
+
mapFunc = itemsOrMapFunc;
|
|
3002
|
+
config = mapFuncOrConfig;
|
|
3003
|
+
}
|
|
3004
|
+
log("🗺️", "Starting map operation:", {
|
|
3005
|
+
name,
|
|
3006
|
+
itemCount: items.length,
|
|
3007
|
+
maxConcurrency: config?.maxConcurrency,
|
|
3008
|
+
});
|
|
3009
|
+
// Validate inputs
|
|
3010
|
+
if (!Array.isArray(items)) {
|
|
3011
|
+
throw new Error("Map operation requires an array of items");
|
|
3012
|
+
}
|
|
3013
|
+
if (typeof mapFunc !== "function") {
|
|
3014
|
+
throw new Error("Map operation requires a function to process items");
|
|
3015
|
+
}
|
|
3016
|
+
// Convert to concurrent execution items
|
|
3017
|
+
const executionItems = items.map((item, index) => ({
|
|
3018
|
+
id: `map-item-${index}`,
|
|
3019
|
+
data: item,
|
|
3020
|
+
index,
|
|
3021
|
+
name: config?.itemNamer ? config.itemNamer(item, index) : undefined,
|
|
3022
|
+
}));
|
|
3023
|
+
// Create executor that calls mapFunc
|
|
3024
|
+
const executor = async (executionItem, childContext) => mapFunc(childContext, executionItem.data, executionItem.index, items);
|
|
3025
|
+
const result = await executeConcurrently(name, executionItems, executor, {
|
|
3026
|
+
maxConcurrency: config?.maxConcurrency,
|
|
3027
|
+
topLevelSubType: exports.OperationSubType.MAP,
|
|
3028
|
+
iterationSubType: exports.OperationSubType.MAP_ITERATION,
|
|
3029
|
+
summaryGenerator: createMapSummaryGenerator(),
|
|
3030
|
+
completionConfig: config?.completionConfig,
|
|
3031
|
+
serdes: config?.serdes,
|
|
3032
|
+
itemSerdes: config?.itemSerdes,
|
|
3033
|
+
});
|
|
3034
|
+
log("🗺️", "Map operation completed successfully:", {
|
|
3035
|
+
resultCount: result.totalCount,
|
|
3036
|
+
});
|
|
3037
|
+
return result;
|
|
3038
|
+
})();
|
|
3039
|
+
// Attach catch handler to prevent unhandled promise rejections
|
|
3040
|
+
// The error will still be thrown when the DurablePromise is awaited
|
|
3041
|
+
phase1Promise.catch(() => { });
|
|
3042
|
+
// Phase 2: Return DurablePromise that returns Phase 1 result when awaited
|
|
3043
|
+
return new DurablePromise(async () => {
|
|
3044
|
+
return await phase1Promise;
|
|
3045
|
+
});
|
|
3046
|
+
};
|
|
3047
|
+
};
|
|
3048
|
+
|
|
3049
|
+
const createParallelHandler = (context, executeConcurrently) => {
|
|
3050
|
+
return (nameOrBranches, branchesOrConfig, maybeConfig) => {
|
|
3051
|
+
// Phase 1: Parse parameters and start execution immediately
|
|
3052
|
+
const phase1Promise = (async () => {
|
|
3053
|
+
let name;
|
|
3054
|
+
let branches;
|
|
3055
|
+
let config;
|
|
3056
|
+
// Parse overloaded parameters
|
|
3057
|
+
if (typeof nameOrBranches === "string" || nameOrBranches === undefined) {
|
|
3058
|
+
// Case: parallel(name, branches, config?)
|
|
3059
|
+
name = nameOrBranches;
|
|
3060
|
+
branches = branchesOrConfig;
|
|
3061
|
+
config = maybeConfig;
|
|
3062
|
+
}
|
|
3063
|
+
else {
|
|
3064
|
+
// Case: parallel(branches, config?)
|
|
3065
|
+
branches = nameOrBranches;
|
|
3066
|
+
config = branchesOrConfig;
|
|
3067
|
+
}
|
|
3068
|
+
// Validate inputs
|
|
3069
|
+
if (!Array.isArray(branches)) {
|
|
3070
|
+
throw new Error("Parallel operation requires an array of branch functions");
|
|
3071
|
+
}
|
|
3072
|
+
log("🔀", "Starting parallel operation:", {
|
|
3073
|
+
name,
|
|
3074
|
+
branchCount: branches.length,
|
|
3075
|
+
maxConcurrency: config?.maxConcurrency,
|
|
3076
|
+
});
|
|
3077
|
+
if (branches.some((branch) => typeof branch !== "function" &&
|
|
3078
|
+
(typeof branch !== "object" || typeof branch.func !== "function"))) {
|
|
3079
|
+
throw new Error("All branches must be functions or NamedParallelBranch objects");
|
|
3080
|
+
}
|
|
3081
|
+
// Convert to concurrent execution items
|
|
3082
|
+
const executionItems = branches.map((branch, index) => {
|
|
3083
|
+
const isNamedBranch = typeof branch === "object" && "func" in branch;
|
|
3084
|
+
const func = isNamedBranch ? branch.func : branch;
|
|
3085
|
+
const branchName = isNamedBranch ? branch.name : undefined;
|
|
3086
|
+
return {
|
|
3087
|
+
id: `parallel-branch-${index}`,
|
|
3088
|
+
data: func,
|
|
3089
|
+
index,
|
|
3090
|
+
name: branchName,
|
|
3091
|
+
};
|
|
3092
|
+
});
|
|
3093
|
+
// Create executor that calls the branch function
|
|
3094
|
+
const executor = async (executionItem, childContext) => {
|
|
3095
|
+
log("🔀", "Processing parallel branch:", {
|
|
3096
|
+
index: executionItem.index,
|
|
3097
|
+
});
|
|
3098
|
+
const result = await executionItem.data(childContext);
|
|
3099
|
+
log("✅", "Parallel branch completed:", {
|
|
3100
|
+
index: executionItem.index,
|
|
3101
|
+
result,
|
|
3102
|
+
});
|
|
3103
|
+
return result;
|
|
3104
|
+
};
|
|
3105
|
+
const result = await executeConcurrently(name, executionItems, executor, {
|
|
3106
|
+
maxConcurrency: config?.maxConcurrency,
|
|
3107
|
+
topLevelSubType: exports.OperationSubType.PARALLEL,
|
|
3108
|
+
iterationSubType: exports.OperationSubType.PARALLEL_BRANCH,
|
|
3109
|
+
summaryGenerator: createParallelSummaryGenerator(),
|
|
3110
|
+
completionConfig: config?.completionConfig,
|
|
3111
|
+
serdes: config?.serdes,
|
|
3112
|
+
itemSerdes: config?.itemSerdes,
|
|
3113
|
+
});
|
|
3114
|
+
log("🔀", "Parallel operation completed successfully:", {
|
|
3115
|
+
resultCount: result.totalCount,
|
|
3116
|
+
});
|
|
3117
|
+
return result;
|
|
3118
|
+
})();
|
|
3119
|
+
// Attach catch handler to prevent unhandled promise rejections
|
|
3120
|
+
// The error will still be thrown when the DurablePromise is awaited
|
|
3121
|
+
phase1Promise.catch(() => { });
|
|
3122
|
+
// Phase 2: Return DurablePromise that returns Phase 1 result when awaited
|
|
3123
|
+
return new DurablePromise(async () => {
|
|
3124
|
+
return await phase1Promise;
|
|
3125
|
+
});
|
|
3126
|
+
};
|
|
3127
|
+
};
|
|
3128
|
+
|
|
3129
|
+
// Minimal error decoration for Promise.allSettled results
|
|
3130
|
+
function decorateErrors(value) {
|
|
3131
|
+
return value.map((item) => {
|
|
3132
|
+
if (item && item.status === "rejected" && item.reason instanceof Error) {
|
|
3133
|
+
return {
|
|
3134
|
+
...item,
|
|
3135
|
+
reason: {
|
|
3136
|
+
message: item.reason.message,
|
|
3137
|
+
name: item.reason.name,
|
|
3138
|
+
stack: item.reason.stack,
|
|
3139
|
+
},
|
|
3140
|
+
};
|
|
3141
|
+
}
|
|
3142
|
+
return item;
|
|
3143
|
+
});
|
|
3144
|
+
}
|
|
3145
|
+
// Error restoration for Promise.allSettled results
|
|
3146
|
+
function restoreErrors(value) {
|
|
3147
|
+
return value.map((item) => {
|
|
3148
|
+
if (item &&
|
|
3149
|
+
item.status === "rejected" &&
|
|
3150
|
+
item.reason &&
|
|
3151
|
+
typeof item.reason === "object" &&
|
|
3152
|
+
item.reason.message) {
|
|
3153
|
+
const error = new Error(item.reason.message);
|
|
3154
|
+
error.name = item.reason.name || "Error";
|
|
3155
|
+
if (item.reason.stack)
|
|
3156
|
+
error.stack = item.reason.stack;
|
|
3157
|
+
return {
|
|
3158
|
+
...item,
|
|
3159
|
+
reason: error,
|
|
3160
|
+
};
|
|
3161
|
+
}
|
|
3162
|
+
return item;
|
|
3163
|
+
});
|
|
3164
|
+
}
|
|
3165
|
+
// Custom serdes for promise results with error handling
|
|
3166
|
+
function createErrorAwareSerdes() {
|
|
3167
|
+
return {
|
|
3168
|
+
serialize: async (value, _context) => value !== undefined ? JSON.stringify(decorateErrors(value)) : undefined,
|
|
3169
|
+
deserialize: async (data, _context) => data !== undefined
|
|
3170
|
+
? restoreErrors(JSON.parse(data))
|
|
3171
|
+
: undefined,
|
|
3172
|
+
};
|
|
3173
|
+
}
|
|
3174
|
+
// No-retry strategy for promise combinators
|
|
3175
|
+
const stepConfig = {
|
|
3176
|
+
retryStrategy: () => ({
|
|
3177
|
+
shouldRetry: false,
|
|
3178
|
+
}),
|
|
3179
|
+
};
|
|
3180
|
+
const createPromiseHandler = (step) => {
|
|
3181
|
+
const parseParams = (nameOrPromises, maybePromises) => {
|
|
3182
|
+
if (typeof nameOrPromises === "string" || nameOrPromises === undefined) {
|
|
3183
|
+
return { name: nameOrPromises, promises: maybePromises };
|
|
3184
|
+
}
|
|
3185
|
+
return { name: undefined, promises: nameOrPromises };
|
|
3186
|
+
};
|
|
3187
|
+
const all = (nameOrPromises, maybePromises) => {
|
|
3188
|
+
return new DurablePromise(async () => {
|
|
3189
|
+
const { name, promises } = parseParams(nameOrPromises, maybePromises);
|
|
3190
|
+
// Wrap Promise.all execution in a step for persistence
|
|
3191
|
+
return await step(name, () => Promise.all(promises), stepConfig);
|
|
3192
|
+
});
|
|
3193
|
+
};
|
|
3194
|
+
const allSettled = (nameOrPromises, maybePromises) => {
|
|
3195
|
+
return new DurablePromise(async () => {
|
|
3196
|
+
const { name, promises } = parseParams(nameOrPromises, maybePromises);
|
|
3197
|
+
// Wrap Promise.allSettled execution in a step for persistence
|
|
3198
|
+
return await step(name, () => Promise.allSettled(promises), {
|
|
3199
|
+
...stepConfig,
|
|
3200
|
+
serdes: createErrorAwareSerdes(),
|
|
3201
|
+
});
|
|
3202
|
+
});
|
|
3203
|
+
};
|
|
3204
|
+
const any = (nameOrPromises, maybePromises) => {
|
|
3205
|
+
return new DurablePromise(async () => {
|
|
3206
|
+
const { name, promises } = parseParams(nameOrPromises, maybePromises);
|
|
3207
|
+
// Wrap Promise.any execution in a step for persistence
|
|
3208
|
+
return await step(name, () => Promise.any(promises), stepConfig);
|
|
3209
|
+
});
|
|
3210
|
+
};
|
|
3211
|
+
const race = (nameOrPromises, maybePromises) => {
|
|
3212
|
+
return new DurablePromise(async () => {
|
|
3213
|
+
const { name, promises } = parseParams(nameOrPromises, maybePromises);
|
|
3214
|
+
// Wrap Promise.race execution in a step for persistence
|
|
3215
|
+
return await step(name, () => Promise.race(promises), stepConfig);
|
|
3216
|
+
});
|
|
3217
|
+
};
|
|
3218
|
+
return {
|
|
3219
|
+
all,
|
|
3220
|
+
allSettled,
|
|
3221
|
+
any,
|
|
3222
|
+
race,
|
|
3223
|
+
};
|
|
3224
|
+
};
|
|
3225
|
+
|
|
3226
|
+
class BatchResultImpl {
|
|
3227
|
+
all;
|
|
3228
|
+
completionReason;
|
|
3229
|
+
constructor(all, completionReason) {
|
|
3230
|
+
this.all = all;
|
|
3231
|
+
this.completionReason = completionReason;
|
|
3232
|
+
}
|
|
3233
|
+
succeeded() {
|
|
3234
|
+
return this.all.filter((item) => item.status === exports.BatchItemStatus.SUCCEEDED && item.result !== undefined);
|
|
3235
|
+
}
|
|
3236
|
+
failed() {
|
|
3237
|
+
return this.all.filter((item) => item.status === exports.BatchItemStatus.FAILED && item.error !== undefined);
|
|
3238
|
+
}
|
|
3239
|
+
started() {
|
|
3240
|
+
return this.all.filter((item) => item.status === exports.BatchItemStatus.STARTED);
|
|
3241
|
+
}
|
|
3242
|
+
get status() {
|
|
3243
|
+
return this.hasFailure ? exports.BatchItemStatus.FAILED : exports.BatchItemStatus.SUCCEEDED;
|
|
3244
|
+
}
|
|
3245
|
+
get hasFailure() {
|
|
3246
|
+
return this.all.some((item) => item.status === exports.BatchItemStatus.FAILED);
|
|
3247
|
+
}
|
|
3248
|
+
throwIfError() {
|
|
3249
|
+
const firstError = this.all.find((item) => item.status === exports.BatchItemStatus.FAILED)?.error;
|
|
3250
|
+
if (firstError) {
|
|
3251
|
+
throw firstError;
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
getResults() {
|
|
3255
|
+
return this.succeeded().map((item) => item.result);
|
|
3256
|
+
}
|
|
3257
|
+
getErrors() {
|
|
3258
|
+
return this.failed().map((item) => item.error);
|
|
3259
|
+
}
|
|
3260
|
+
get successCount() {
|
|
3261
|
+
return this.all.filter((item) => item.status === exports.BatchItemStatus.SUCCEEDED)
|
|
3262
|
+
.length;
|
|
3263
|
+
}
|
|
3264
|
+
get failureCount() {
|
|
3265
|
+
return this.all.filter((item) => item.status === exports.BatchItemStatus.FAILED)
|
|
3266
|
+
.length;
|
|
3267
|
+
}
|
|
3268
|
+
get startedCount() {
|
|
3269
|
+
return this.all.filter((item) => item.status === exports.BatchItemStatus.STARTED)
|
|
3270
|
+
.length;
|
|
3271
|
+
}
|
|
3272
|
+
get totalCount() {
|
|
3273
|
+
return this.all.length;
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
/**
|
|
3277
|
+
* Restores methods to deserialized BatchResult data
|
|
3278
|
+
*/
|
|
3279
|
+
function restoreBatchResult(data) {
|
|
3280
|
+
if (data &&
|
|
3281
|
+
typeof data === "object" &&
|
|
3282
|
+
"all" in data &&
|
|
3283
|
+
Array.isArray(data.all)) {
|
|
3284
|
+
const serializedData = data;
|
|
3285
|
+
// Restore Error objects
|
|
3286
|
+
const restoredItems = serializedData.all.map((item) => ({
|
|
3287
|
+
...item,
|
|
3288
|
+
result: item.result,
|
|
3289
|
+
error: item.error
|
|
3290
|
+
? DurableOperationError.fromErrorObject(item.error)
|
|
3291
|
+
: undefined,
|
|
3292
|
+
}));
|
|
3293
|
+
return new BatchResultImpl(restoredItems, serializedData.completionReason);
|
|
3294
|
+
}
|
|
3295
|
+
return new BatchResultImpl([], "ALL_COMPLETED");
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
class ConcurrencyController {
|
|
3299
|
+
operationName;
|
|
3300
|
+
skipNextOperation;
|
|
3301
|
+
constructor(operationName, skipNextOperation) {
|
|
3302
|
+
this.operationName = operationName;
|
|
3303
|
+
this.skipNextOperation = skipNextOperation;
|
|
3304
|
+
}
|
|
3305
|
+
isChildEntityCompleted(executionContext, parentEntityId, completedCount) {
|
|
3306
|
+
const childEntityId = `${parentEntityId}-${completedCount + 1}`;
|
|
3307
|
+
const childStepData = executionContext.getStepData(childEntityId);
|
|
3308
|
+
return !!(childStepData &&
|
|
3309
|
+
(childStepData.Status === clientLambda.OperationStatus.SUCCEEDED ||
|
|
3310
|
+
childStepData.Status === clientLambda.OperationStatus.FAILED));
|
|
3311
|
+
}
|
|
3312
|
+
async executeItems(items, executor, parentContext, config, durableExecutionMode = DurableExecutionMode.ExecutionMode, entityId, executionContext) {
|
|
3313
|
+
// In replay mode, we're reconstructing the result from child contexts
|
|
3314
|
+
if (durableExecutionMode === DurableExecutionMode.ReplaySucceededContext) {
|
|
3315
|
+
log("🔄", `Replay mode: Reconstructing ${this.operationName} result:`, {
|
|
3316
|
+
itemCount: items.length,
|
|
3317
|
+
});
|
|
3318
|
+
// Try to get the target count from step data
|
|
3319
|
+
let targetTotalCount;
|
|
3320
|
+
if (entityId && executionContext) {
|
|
3321
|
+
const stepData = executionContext.getStepData(entityId);
|
|
3322
|
+
const summaryPayload = stepData?.ContextDetails?.Result;
|
|
3323
|
+
if (summaryPayload) {
|
|
3324
|
+
try {
|
|
3325
|
+
const serdes = config.serdes || defaultSerdes;
|
|
3326
|
+
const parsedSummary = await serdes.deserialize(summaryPayload, {
|
|
3327
|
+
entityId: entityId,
|
|
3328
|
+
durableExecutionArn: executionContext.durableExecutionArn,
|
|
3329
|
+
});
|
|
3330
|
+
if (parsedSummary &&
|
|
3331
|
+
typeof parsedSummary === "object" &&
|
|
3332
|
+
"totalCount" in parsedSummary) {
|
|
3333
|
+
// Read totalCount directly from summary metadata
|
|
3334
|
+
targetTotalCount = parsedSummary.totalCount;
|
|
3335
|
+
log("📊", "Found initial execution count:", {
|
|
3336
|
+
targetTotalCount,
|
|
3337
|
+
});
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
catch (error) {
|
|
3341
|
+
log("⚠️", "Could not parse initial result summary:", error);
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
// If we have target count and required context, use optimized replay; otherwise fallback to concurrent execution
|
|
3346
|
+
if (targetTotalCount !== undefined && entityId && executionContext) {
|
|
3347
|
+
return await this.replayItems(items, executor, parentContext, config, targetTotalCount, executionContext, entityId);
|
|
3348
|
+
}
|
|
3349
|
+
else {
|
|
3350
|
+
log("⚠️", "No valid target count or context found, falling back to concurrent execution");
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
// First-time execution or fallback: use normal concurrent execution logic
|
|
3354
|
+
return await this.executeItemsConcurrently(items, executor, parentContext, config);
|
|
3355
|
+
}
|
|
3356
|
+
async replayItems(items, executor, parentContext, config, targetTotalCount, executionContext, parentEntityId) {
|
|
3357
|
+
const resultItems = [];
|
|
3358
|
+
log("🔄", `Replaying ${items.length} items sequentially`, {
|
|
3359
|
+
targetTotalCount,
|
|
3360
|
+
});
|
|
3361
|
+
let completedCount = 0;
|
|
3362
|
+
let stepCounter = 0;
|
|
3363
|
+
// Replay items sequentially until we reach the target count
|
|
3364
|
+
for (const item of items) {
|
|
3365
|
+
// Stop if we've replayed all items that completed in initial execution
|
|
3366
|
+
if (completedCount >= targetTotalCount) {
|
|
3367
|
+
log("✅", "Reached target count, stopping replay", {
|
|
3368
|
+
completedCount,
|
|
3369
|
+
targetTotalCount,
|
|
3370
|
+
});
|
|
3371
|
+
break;
|
|
3372
|
+
}
|
|
3373
|
+
// Calculate the child entity ID that runInChildContext will create
|
|
3374
|
+
// It uses the parent's next step ID, which is parentEntityId-{counter}
|
|
3375
|
+
const childEntityId = `${parentEntityId}-${stepCounter + 1}`;
|
|
3376
|
+
if (!this.isChildEntityCompleted(executionContext, parentEntityId, stepCounter)) {
|
|
3377
|
+
log("⏭️", `Skipping incomplete item:`, {
|
|
3378
|
+
index: item.index,
|
|
3379
|
+
itemId: item.id,
|
|
3380
|
+
childEntityId,
|
|
3381
|
+
});
|
|
3382
|
+
// Increment step counter to maintain consistency
|
|
3383
|
+
this.skipNextOperation();
|
|
3384
|
+
stepCounter++;
|
|
3385
|
+
continue;
|
|
3386
|
+
}
|
|
3387
|
+
try {
|
|
3388
|
+
const result = await parentContext.runInChildContext(item.name || item.id, (childContext) => executor(item, childContext), { subType: config.iterationSubType, serdes: config.itemSerdes });
|
|
3389
|
+
resultItems.push({
|
|
3390
|
+
result,
|
|
3391
|
+
index: item.index,
|
|
3392
|
+
status: exports.BatchItemStatus.SUCCEEDED,
|
|
3393
|
+
});
|
|
3394
|
+
completedCount++;
|
|
3395
|
+
stepCounter++;
|
|
3396
|
+
log("✅", `Replayed ${this.operationName} item:`, {
|
|
3397
|
+
index: item.index,
|
|
3398
|
+
itemId: item.id,
|
|
3399
|
+
completedCount,
|
|
3400
|
+
});
|
|
3401
|
+
}
|
|
3402
|
+
catch (error) {
|
|
3403
|
+
const err = error instanceof ChildContextError
|
|
3404
|
+
? error
|
|
3405
|
+
: new ChildContextError(error instanceof Error ? error.message : String(error), error instanceof Error ? error : undefined);
|
|
3406
|
+
resultItems.push({
|
|
3407
|
+
error: err,
|
|
3408
|
+
index: item.index,
|
|
3409
|
+
status: exports.BatchItemStatus.FAILED,
|
|
3410
|
+
});
|
|
3411
|
+
completedCount++;
|
|
3412
|
+
stepCounter++;
|
|
3413
|
+
log("❌", `Replay failed for ${this.operationName} item:`, {
|
|
3414
|
+
index: item.index,
|
|
3415
|
+
itemId: item.id,
|
|
3416
|
+
error: err.message,
|
|
3417
|
+
completedCount,
|
|
3418
|
+
});
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
log("🎉", `${this.operationName} replay completed:`, {
|
|
3422
|
+
completedCount,
|
|
3423
|
+
totalCount: resultItems.length,
|
|
3424
|
+
});
|
|
3425
|
+
// Reconstruct the completion reason based on replay results
|
|
3426
|
+
const successCount = resultItems.filter((item) => item.status === exports.BatchItemStatus.SUCCEEDED).length;
|
|
3427
|
+
const getCompletionReason = () => {
|
|
3428
|
+
if (completedCount === items.length)
|
|
3429
|
+
return "ALL_COMPLETED";
|
|
3430
|
+
if (config.completionConfig?.minSuccessful !== undefined &&
|
|
3431
|
+
successCount >= config.completionConfig.minSuccessful)
|
|
3432
|
+
return "MIN_SUCCESSFUL_REACHED";
|
|
3433
|
+
return "FAILURE_TOLERANCE_EXCEEDED";
|
|
3434
|
+
};
|
|
3435
|
+
return new BatchResultImpl(resultItems, getCompletionReason());
|
|
3436
|
+
}
|
|
3437
|
+
async executeItemsConcurrently(items, executor, parentContext, config) {
|
|
3438
|
+
const maxConcurrency = config.maxConcurrency || Infinity;
|
|
3439
|
+
const resultItems = new Array(items.length);
|
|
3440
|
+
const startedItems = new Set();
|
|
3441
|
+
let activeCount = 0;
|
|
3442
|
+
let currentIndex = 0;
|
|
3443
|
+
let completedCount = 0;
|
|
3444
|
+
let successCount = 0;
|
|
3445
|
+
let failureCount = 0;
|
|
3446
|
+
log("🚀", `Starting ${this.operationName} with concurrency control:`, {
|
|
3447
|
+
itemCount: items.length,
|
|
3448
|
+
maxConcurrency,
|
|
3449
|
+
});
|
|
3450
|
+
return new Promise((resolve) => {
|
|
3451
|
+
const shouldContinue = () => {
|
|
3452
|
+
const completion = config.completionConfig;
|
|
3453
|
+
if (!completion)
|
|
3454
|
+
return failureCount === 0;
|
|
3455
|
+
// Default to fail-fast when no completion criteria are defined
|
|
3456
|
+
const hasAnyCompletionCriteria = Object.values(completion).some((value) => value !== undefined);
|
|
3457
|
+
if (!hasAnyCompletionCriteria) {
|
|
3458
|
+
return failureCount === 0;
|
|
3459
|
+
}
|
|
3460
|
+
if (completion.toleratedFailureCount !== undefined &&
|
|
3461
|
+
failureCount > completion.toleratedFailureCount)
|
|
3462
|
+
return false;
|
|
3463
|
+
if (completion.toleratedFailurePercentage !== undefined) {
|
|
3464
|
+
const failurePercentage = (failureCount / items.length) * 100;
|
|
3465
|
+
if (failurePercentage > completion.toleratedFailurePercentage)
|
|
3466
|
+
return false;
|
|
3467
|
+
}
|
|
3468
|
+
return true;
|
|
3469
|
+
};
|
|
3470
|
+
const isComplete = () => {
|
|
3471
|
+
// Always complete when all items are done (matches BatchResult inference)
|
|
3472
|
+
if (completedCount === items.length) {
|
|
3473
|
+
return true;
|
|
3474
|
+
}
|
|
3475
|
+
const completion = config.completionConfig;
|
|
3476
|
+
if (completion?.minSuccessful !== undefined &&
|
|
3477
|
+
successCount >= completion.minSuccessful) {
|
|
3478
|
+
return true;
|
|
3479
|
+
}
|
|
3480
|
+
return false;
|
|
3481
|
+
};
|
|
3482
|
+
const getCompletionReason = () => {
|
|
3483
|
+
if (completedCount === items.length)
|
|
3484
|
+
return "ALL_COMPLETED";
|
|
3485
|
+
if (config.completionConfig?.minSuccessful !== undefined &&
|
|
3486
|
+
successCount >= config.completionConfig.minSuccessful)
|
|
3487
|
+
return "MIN_SUCCESSFUL_REACHED";
|
|
3488
|
+
return "FAILURE_TOLERANCE_EXCEEDED";
|
|
3489
|
+
};
|
|
3490
|
+
const tryStartNext = () => {
|
|
3491
|
+
while (activeCount < maxConcurrency &&
|
|
3492
|
+
currentIndex < items.length &&
|
|
3493
|
+
shouldContinue()) {
|
|
3494
|
+
const index = currentIndex++;
|
|
3495
|
+
const item = items[index];
|
|
3496
|
+
startedItems.add(index);
|
|
3497
|
+
activeCount++;
|
|
3498
|
+
// Set STARTED status immediately in result array
|
|
3499
|
+
resultItems[index] = { index, status: exports.BatchItemStatus.STARTED };
|
|
3500
|
+
log("▶️", `Starting ${this.operationName} item:`, {
|
|
3501
|
+
index,
|
|
3502
|
+
itemId: item.id,
|
|
3503
|
+
itemName: item.name,
|
|
3504
|
+
});
|
|
3505
|
+
parentContext
|
|
3506
|
+
.runInChildContext(item.name || item.id, (childContext) => executor(item, childContext), { subType: config.iterationSubType, serdes: config.itemSerdes })
|
|
3507
|
+
.then((result) => {
|
|
3508
|
+
resultItems[index] = {
|
|
3509
|
+
result,
|
|
3510
|
+
index,
|
|
3511
|
+
status: exports.BatchItemStatus.SUCCEEDED,
|
|
3512
|
+
};
|
|
3513
|
+
successCount++;
|
|
3514
|
+
log("✅", `${this.operationName} item completed:`, {
|
|
3515
|
+
index,
|
|
3516
|
+
itemId: item.id,
|
|
3517
|
+
itemName: item.name,
|
|
3518
|
+
});
|
|
3519
|
+
onComplete();
|
|
3520
|
+
}, (error) => {
|
|
3521
|
+
const err = error instanceof ChildContextError
|
|
3522
|
+
? error
|
|
3523
|
+
: new ChildContextError(error instanceof Error ? error.message : String(error), error instanceof Error ? error : undefined);
|
|
3524
|
+
resultItems[index] = {
|
|
3525
|
+
error: err,
|
|
3526
|
+
index,
|
|
3527
|
+
status: exports.BatchItemStatus.FAILED,
|
|
3528
|
+
};
|
|
3529
|
+
failureCount++;
|
|
3530
|
+
log("❌", `${this.operationName} item failed:`, {
|
|
3531
|
+
index,
|
|
3532
|
+
itemId: item.id,
|
|
3533
|
+
itemName: item.name,
|
|
3534
|
+
error: err.message,
|
|
3535
|
+
});
|
|
3536
|
+
onComplete();
|
|
3537
|
+
});
|
|
3538
|
+
}
|
|
3539
|
+
};
|
|
3540
|
+
const onComplete = () => {
|
|
3541
|
+
activeCount--;
|
|
3542
|
+
completedCount++;
|
|
3543
|
+
if (isComplete() || !shouldContinue()) {
|
|
3544
|
+
// Convert sparse array to dense array - items are already in correct order by index
|
|
3545
|
+
// Include all items that were started (have a value in resultItems)
|
|
3546
|
+
// Create shallow copy to prevent mutations from affecting the returned result
|
|
3547
|
+
const finalBatchItems = [];
|
|
3548
|
+
for (let i = 0; i < resultItems.length; i++) {
|
|
3549
|
+
if (resultItems[i] !== undefined) {
|
|
3550
|
+
finalBatchItems.push({ ...resultItems[i] });
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
log("🎉", `${this.operationName} completed:`, {
|
|
3554
|
+
successCount,
|
|
3555
|
+
failureCount,
|
|
3556
|
+
startedCount: finalBatchItems.filter((item) => item.status === exports.BatchItemStatus.STARTED).length,
|
|
3557
|
+
totalCount: finalBatchItems.length,
|
|
3558
|
+
});
|
|
3559
|
+
const result = new BatchResultImpl(finalBatchItems, getCompletionReason());
|
|
3560
|
+
resolve(result);
|
|
3561
|
+
}
|
|
3562
|
+
else {
|
|
3563
|
+
tryStartNext();
|
|
3564
|
+
}
|
|
3565
|
+
};
|
|
3566
|
+
tryStartNext();
|
|
3567
|
+
});
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
const createConcurrentExecutionHandler = (context, runInChildContext, skipNextOperation) => {
|
|
3571
|
+
return (nameOrItems, itemsOrExecutor, executorOrConfig, maybeConfig) => {
|
|
3572
|
+
// Phase 1: Start execution immediately
|
|
3573
|
+
const phase1Promise = (async () => {
|
|
3574
|
+
let name;
|
|
3575
|
+
let items;
|
|
3576
|
+
let executor;
|
|
3577
|
+
let config;
|
|
3578
|
+
if (typeof nameOrItems === "string" || nameOrItems === undefined) {
|
|
3579
|
+
name = nameOrItems;
|
|
3580
|
+
items = itemsOrExecutor;
|
|
3581
|
+
executor = executorOrConfig;
|
|
3582
|
+
config = maybeConfig;
|
|
3583
|
+
}
|
|
3584
|
+
else {
|
|
3585
|
+
items = nameOrItems;
|
|
3586
|
+
executor = itemsOrExecutor;
|
|
3587
|
+
config = executorOrConfig;
|
|
3588
|
+
}
|
|
3589
|
+
log("🔄", "Starting concurrent execution:", {
|
|
3590
|
+
name,
|
|
3591
|
+
itemCount: items.length,
|
|
3592
|
+
maxConcurrency: config?.maxConcurrency,
|
|
3593
|
+
});
|
|
3594
|
+
if (!Array.isArray(items)) {
|
|
3595
|
+
throw new Error("Concurrent execution requires an array of items");
|
|
3596
|
+
}
|
|
3597
|
+
if (typeof executor !== "function") {
|
|
3598
|
+
throw new Error("Concurrent execution requires an executor function");
|
|
3599
|
+
}
|
|
3600
|
+
if (config?.maxConcurrency !== undefined &&
|
|
3601
|
+
config.maxConcurrency !== null &&
|
|
3602
|
+
config.maxConcurrency <= 0) {
|
|
3603
|
+
throw new Error(`Invalid maxConcurrency: ${config.maxConcurrency}. Must be a positive number or undefined for unlimited concurrency.`);
|
|
3604
|
+
}
|
|
3605
|
+
const executeOperation = async (executionContext) => {
|
|
3606
|
+
const concurrencyController = new ConcurrencyController("concurrent-execution", skipNextOperation);
|
|
3607
|
+
// Access durableExecutionMode from the context - it's set by runInChildContext
|
|
3608
|
+
// based on determineChildReplayMode logic
|
|
3609
|
+
const durableExecutionMode = executionContext.durableExecutionMode;
|
|
3610
|
+
// Get the entity ID (step prefix) from the child context
|
|
3611
|
+
const entityId = executionContext._stepPrefix;
|
|
3612
|
+
log("🔄", "Concurrent execution mode:", {
|
|
3613
|
+
mode: durableExecutionMode,
|
|
3614
|
+
itemCount: items.length,
|
|
3615
|
+
entityId,
|
|
3616
|
+
});
|
|
3617
|
+
return await concurrencyController.executeItems(items, executor, executionContext, config || {}, durableExecutionMode, entityId, context);
|
|
3618
|
+
};
|
|
3619
|
+
const result = await runInChildContext(name, executeOperation, {
|
|
3620
|
+
subType: config?.topLevelSubType,
|
|
3621
|
+
summaryGenerator: config?.summaryGenerator,
|
|
3622
|
+
serdes: config?.serdes,
|
|
3623
|
+
});
|
|
3624
|
+
// Restore BatchResult methods if the result came from deserialized data
|
|
3625
|
+
if (result &&
|
|
3626
|
+
typeof result === "object" &&
|
|
3627
|
+
"all" in result &&
|
|
3628
|
+
Array.isArray(result.all)) {
|
|
3629
|
+
return restoreBatchResult(result);
|
|
3630
|
+
}
|
|
3631
|
+
return result;
|
|
3632
|
+
})();
|
|
3633
|
+
// Attach catch handler to prevent unhandled promise rejections
|
|
3634
|
+
// The error will still be thrown when the DurablePromise is awaited
|
|
3635
|
+
phase1Promise.catch(() => { });
|
|
3636
|
+
// Phase 2: Return DurablePromise that returns Phase 1 result when awaited
|
|
3637
|
+
return new DurablePromise(async () => {
|
|
3638
|
+
return await phase1Promise;
|
|
3639
|
+
});
|
|
3640
|
+
};
|
|
3641
|
+
};
|
|
3642
|
+
|
|
3643
|
+
class ModeManagement {
|
|
3644
|
+
captureExecutionState;
|
|
3645
|
+
checkAndUpdateReplayMode;
|
|
3646
|
+
checkForNonResolvingPromise;
|
|
3647
|
+
getDurableExecutionMode;
|
|
3648
|
+
setDurableExecutionMode;
|
|
3649
|
+
constructor(captureExecutionState, checkAndUpdateReplayMode, checkForNonResolvingPromise, getDurableExecutionMode, setDurableExecutionMode) {
|
|
3650
|
+
this.captureExecutionState = captureExecutionState;
|
|
3651
|
+
this.checkAndUpdateReplayMode = checkAndUpdateReplayMode;
|
|
3652
|
+
this.checkForNonResolvingPromise = checkForNonResolvingPromise;
|
|
3653
|
+
this.getDurableExecutionMode = getDurableExecutionMode;
|
|
3654
|
+
this.setDurableExecutionMode = setDurableExecutionMode;
|
|
3655
|
+
}
|
|
3656
|
+
withModeManagement(operation) {
|
|
3657
|
+
const shouldSwitchToExecutionMode = this.captureExecutionState();
|
|
3658
|
+
this.checkAndUpdateReplayMode();
|
|
3659
|
+
const nonResolvingPromise = this.checkForNonResolvingPromise();
|
|
3660
|
+
if (nonResolvingPromise)
|
|
3661
|
+
return nonResolvingPromise;
|
|
3662
|
+
try {
|
|
3663
|
+
return operation();
|
|
3664
|
+
}
|
|
3665
|
+
finally {
|
|
3666
|
+
if (shouldSwitchToExecutionMode) {
|
|
3667
|
+
this.setDurableExecutionMode(DurableExecutionMode.ExecutionMode);
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
withDurableModeManagement(operation) {
|
|
3672
|
+
const shouldSwitchToExecutionMode = this.captureExecutionState();
|
|
3673
|
+
this.checkAndUpdateReplayMode();
|
|
3674
|
+
const nonResolvingPromise = this.checkForNonResolvingPromise();
|
|
3675
|
+
if (nonResolvingPromise) {
|
|
3676
|
+
return new DurablePromise(async () => {
|
|
3677
|
+
await nonResolvingPromise;
|
|
3678
|
+
// This will never be reached
|
|
3679
|
+
throw new Error("Unreachable code");
|
|
3680
|
+
});
|
|
3681
|
+
}
|
|
3682
|
+
try {
|
|
3683
|
+
return operation();
|
|
3684
|
+
}
|
|
3685
|
+
finally {
|
|
3686
|
+
if (shouldSwitchToExecutionMode) {
|
|
3687
|
+
this.setDurableExecutionMode(DurableExecutionMode.ExecutionMode);
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
class DurableContextImpl {
|
|
3694
|
+
executionContext;
|
|
3695
|
+
lambdaContext;
|
|
3696
|
+
_stepPrefix;
|
|
3697
|
+
_stepCounter = 0;
|
|
3698
|
+
durableLogger;
|
|
3699
|
+
modeAwareLoggingEnabled = true;
|
|
3700
|
+
runningOperations = new Set();
|
|
3701
|
+
operationsEmitter = new events.EventEmitter();
|
|
3702
|
+
checkpoint;
|
|
3703
|
+
durableExecutionMode;
|
|
3704
|
+
_parentId;
|
|
3705
|
+
modeManagement;
|
|
3706
|
+
durableExecution;
|
|
3707
|
+
logger;
|
|
3708
|
+
constructor(executionContext, lambdaContext, durableExecutionMode, inheritedLogger, stepPrefix, durableExecution, parentId) {
|
|
3709
|
+
this.executionContext = executionContext;
|
|
3710
|
+
this.lambdaContext = lambdaContext;
|
|
3711
|
+
this._stepPrefix = stepPrefix;
|
|
3712
|
+
this._parentId = parentId;
|
|
3713
|
+
this.durableExecution = durableExecution;
|
|
3714
|
+
this.durableLogger = inheritedLogger;
|
|
3715
|
+
this.durableLogger.configureDurableLoggingContext?.(this.getDurableLoggingContext());
|
|
3716
|
+
this.logger = this.createModeAwareLogger(inheritedLogger);
|
|
3717
|
+
this.durableExecutionMode = durableExecutionMode;
|
|
3718
|
+
this.checkpoint = durableExecution.checkpointManager;
|
|
3719
|
+
this.modeManagement = new ModeManagement(this.captureExecutionState.bind(this), this.checkAndUpdateReplayMode.bind(this), this.checkForNonResolvingPromise.bind(this), () => this.durableExecutionMode, (mode) => {
|
|
3720
|
+
this.durableExecutionMode = mode;
|
|
3721
|
+
});
|
|
3722
|
+
}
|
|
3723
|
+
getDurableLoggingContext() {
|
|
3724
|
+
return {
|
|
3725
|
+
getDurableLogData: () => {
|
|
3726
|
+
const activeContext = getActiveContext();
|
|
3727
|
+
const result = {
|
|
3728
|
+
executionArn: this.executionContext.durableExecutionArn,
|
|
3729
|
+
requestId: this.executionContext.requestId,
|
|
3730
|
+
tenantId: this.executionContext.tenantId,
|
|
3731
|
+
operationId: !activeContext || activeContext?.contextId === "root"
|
|
3732
|
+
? undefined
|
|
3733
|
+
: hashId(activeContext.contextId),
|
|
3734
|
+
};
|
|
3735
|
+
if (activeContext?.attempt !== undefined) {
|
|
3736
|
+
result.attempt = activeContext.attempt;
|
|
3737
|
+
}
|
|
3738
|
+
return result;
|
|
3739
|
+
},
|
|
3740
|
+
};
|
|
3741
|
+
}
|
|
3742
|
+
shouldLog() {
|
|
3743
|
+
const activeContext = getActiveContext();
|
|
3744
|
+
if (!this.modeAwareLoggingEnabled || !activeContext) {
|
|
3745
|
+
return true;
|
|
3746
|
+
}
|
|
3747
|
+
if (activeContext.contextId === "root") {
|
|
3748
|
+
return this.durableExecutionMode === DurableExecutionMode.ExecutionMode;
|
|
3749
|
+
}
|
|
3750
|
+
return (activeContext.durableExecutionMode === DurableExecutionMode.ExecutionMode);
|
|
3751
|
+
}
|
|
3752
|
+
createModeAwareLogger(logger) {
|
|
3753
|
+
const durableContextLogger = {
|
|
3754
|
+
warn: (...args) => {
|
|
3755
|
+
if (this.shouldLog()) {
|
|
3756
|
+
return logger.warn(...args);
|
|
3757
|
+
}
|
|
3758
|
+
},
|
|
3759
|
+
debug: (...args) => {
|
|
3760
|
+
if (this.shouldLog()) {
|
|
3761
|
+
return logger.debug(...args);
|
|
3762
|
+
}
|
|
3763
|
+
},
|
|
3764
|
+
info: (...args) => {
|
|
3765
|
+
if (this.shouldLog()) {
|
|
3766
|
+
return logger.info(...args);
|
|
3767
|
+
}
|
|
3768
|
+
},
|
|
3769
|
+
error: (...args) => {
|
|
3770
|
+
if (this.shouldLog()) {
|
|
3771
|
+
return logger.error(...args);
|
|
3772
|
+
}
|
|
3773
|
+
},
|
|
3774
|
+
};
|
|
3775
|
+
if ("log" in logger) {
|
|
3776
|
+
durableContextLogger.log = (level, ...args) => {
|
|
3777
|
+
if (this.shouldLog()) {
|
|
3778
|
+
return logger.log?.(level, ...args);
|
|
3779
|
+
}
|
|
3780
|
+
};
|
|
3781
|
+
}
|
|
3782
|
+
return durableContextLogger;
|
|
3783
|
+
}
|
|
3784
|
+
createStepId() {
|
|
3785
|
+
this._stepCounter++;
|
|
3786
|
+
return this._stepPrefix
|
|
3787
|
+
? `${this._stepPrefix}-${this._stepCounter}`
|
|
3788
|
+
: `${this._stepCounter}`;
|
|
3789
|
+
}
|
|
3790
|
+
getNextStepId() {
|
|
3791
|
+
const nextCounter = this._stepCounter + 1;
|
|
3792
|
+
return this._stepPrefix
|
|
3793
|
+
? `${this._stepPrefix}-${nextCounter}`
|
|
3794
|
+
: `${nextCounter}`;
|
|
3795
|
+
}
|
|
3796
|
+
/**
|
|
3797
|
+
* Skips the next operation by incrementing the step counter.
|
|
3798
|
+
* Used internally by concurrent execution handler during replay to skip incomplete items.
|
|
3799
|
+
* @internal
|
|
3800
|
+
*/
|
|
3801
|
+
skipNextOperation() {
|
|
3802
|
+
this._stepCounter++;
|
|
3803
|
+
}
|
|
3804
|
+
checkAndUpdateReplayMode() {
|
|
3805
|
+
if (this.durableExecutionMode === DurableExecutionMode.ReplayMode) {
|
|
3806
|
+
const nextStepId = this.getNextStepId();
|
|
3807
|
+
const nextStepData = this.executionContext.getStepData(nextStepId);
|
|
3808
|
+
if (!nextStepData) {
|
|
3809
|
+
this.durableExecutionMode = DurableExecutionMode.ExecutionMode;
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
captureExecutionState() {
|
|
3814
|
+
const wasInReplayMode = this.durableExecutionMode === DurableExecutionMode.ReplayMode;
|
|
3815
|
+
const nextStepId = this.getNextStepId();
|
|
3816
|
+
const stepData = this.executionContext.getStepData(nextStepId);
|
|
3817
|
+
const wasNotFinished = !!(stepData &&
|
|
3818
|
+
stepData.Status !== clientLambda.OperationStatus.SUCCEEDED &&
|
|
3819
|
+
stepData.Status !== clientLambda.OperationStatus.FAILED);
|
|
3820
|
+
return wasInReplayMode && wasNotFinished;
|
|
3821
|
+
}
|
|
3822
|
+
checkForNonResolvingPromise() {
|
|
3823
|
+
if (this.durableExecutionMode === DurableExecutionMode.ReplaySucceededContext) {
|
|
3824
|
+
const nextStepId = this.getNextStepId();
|
|
3825
|
+
const nextStepData = this.executionContext.getStepData(nextStepId);
|
|
3826
|
+
if (nextStepData &&
|
|
3827
|
+
nextStepData.Status !== clientLambda.OperationStatus.SUCCEEDED &&
|
|
3828
|
+
nextStepData.Status !== clientLambda.OperationStatus.FAILED) {
|
|
3829
|
+
return new Promise(() => { }); // Non-resolving promise
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
return null;
|
|
3833
|
+
}
|
|
3834
|
+
addRunningOperation(stepId) {
|
|
3835
|
+
this.runningOperations.add(stepId);
|
|
3836
|
+
}
|
|
3837
|
+
removeRunningOperation(stepId) {
|
|
3838
|
+
this.runningOperations.delete(stepId);
|
|
3839
|
+
if (this.runningOperations.size === 0) {
|
|
3840
|
+
this.operationsEmitter.emit(OPERATIONS_COMPLETE_EVENT);
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
hasRunningOperations() {
|
|
3844
|
+
return this.runningOperations.size > 0;
|
|
3845
|
+
}
|
|
3846
|
+
getOperationsEmitter() {
|
|
3847
|
+
return this.operationsEmitter;
|
|
3848
|
+
}
|
|
3849
|
+
withModeManagement(operation) {
|
|
3850
|
+
return this.modeManagement.withModeManagement(operation);
|
|
3851
|
+
}
|
|
3852
|
+
withDurableModeManagement(operation) {
|
|
3853
|
+
return this.modeManagement.withDurableModeManagement(operation);
|
|
3854
|
+
}
|
|
3855
|
+
step(nameOrFn, fnOrOptions, maybeOptions) {
|
|
3856
|
+
validateContextUsage(this._stepPrefix, "step", this.executionContext.terminationManager);
|
|
3857
|
+
return this.withDurableModeManagement(() => {
|
|
3858
|
+
const stepHandler = createStepHandler(this.executionContext, this.checkpoint, this.lambdaContext, this.createStepId.bind(this), this.durableLogger, this.addRunningOperation.bind(this), this.removeRunningOperation.bind(this), this.hasRunningOperations.bind(this), this.getOperationsEmitter.bind(this), this._parentId);
|
|
3859
|
+
return stepHandler(nameOrFn, fnOrOptions, maybeOptions);
|
|
3860
|
+
});
|
|
3861
|
+
}
|
|
3862
|
+
invoke(nameOrFuncId, funcIdOrInput, inputOrConfig, maybeConfig) {
|
|
3863
|
+
validateContextUsage(this._stepPrefix, "invoke", this.executionContext.terminationManager);
|
|
3864
|
+
return this.withDurableModeManagement(() => {
|
|
3865
|
+
const invokeHandler = createInvokeHandler(this.executionContext, this.checkpoint, this.createStepId.bind(this), this.hasRunningOperations.bind(this), this.getOperationsEmitter.bind(this), this._parentId, this.checkAndUpdateReplayMode.bind(this));
|
|
3866
|
+
return invokeHandler(...[
|
|
3867
|
+
nameOrFuncId,
|
|
3868
|
+
funcIdOrInput,
|
|
3869
|
+
inputOrConfig,
|
|
3870
|
+
maybeConfig,
|
|
3871
|
+
]);
|
|
3872
|
+
});
|
|
3873
|
+
}
|
|
3874
|
+
runInChildContext(nameOrFn, fnOrOptions, maybeOptions) {
|
|
3875
|
+
validateContextUsage(this._stepPrefix, "runInChildContext", this.executionContext.terminationManager);
|
|
3876
|
+
return this.withDurableModeManagement(() => {
|
|
3877
|
+
const blockHandler = createRunInChildContextHandler(this.executionContext, this.checkpoint, this.lambdaContext, this.createStepId.bind(this), () => this.durableLogger,
|
|
3878
|
+
// Adapter function to maintain compatibility
|
|
3879
|
+
(executionContext, parentContext, durableExecutionMode, inheritedLogger, stepPrefix, _checkpointToken, parentId) => createDurableContext(executionContext, parentContext, durableExecutionMode, inheritedLogger, stepPrefix, this.durableExecution, parentId), this._parentId);
|
|
3880
|
+
return blockHandler(nameOrFn, fnOrOptions, maybeOptions);
|
|
3881
|
+
});
|
|
3882
|
+
}
|
|
3883
|
+
wait(nameOrDuration, maybeDuration) {
|
|
3884
|
+
validateContextUsage(this._stepPrefix, "wait", this.executionContext.terminationManager);
|
|
3885
|
+
return this.withDurableModeManagement(() => {
|
|
3886
|
+
const waitHandler = createWaitHandler(this.executionContext, this.checkpoint, this.createStepId.bind(this), this.hasRunningOperations.bind(this), this.getOperationsEmitter.bind(this), this._parentId, this.checkAndUpdateReplayMode.bind(this));
|
|
3887
|
+
return typeof nameOrDuration === "string"
|
|
3888
|
+
? waitHandler(nameOrDuration, maybeDuration)
|
|
3889
|
+
: waitHandler(nameOrDuration);
|
|
3890
|
+
});
|
|
3891
|
+
}
|
|
3892
|
+
/**
|
|
3893
|
+
* Configure logger behavior for this context
|
|
3894
|
+
*
|
|
3895
|
+
* This method allows partial configuration - only the properties provided will be updated.
|
|
3896
|
+
* For example, calling configureLogger(\{ modeAware: false \}) will only change the modeAware
|
|
3897
|
+
* setting without affecting any previously configured custom logger.
|
|
3898
|
+
*
|
|
3899
|
+
* @param config - Logger configuration options including customLogger and modeAware settings (default: modeAware=true)
|
|
3900
|
+
* @example
|
|
3901
|
+
* // Set custom logger and enable mode-aware logging
|
|
3902
|
+
* context.configureLogger(\{ customLogger: myLogger, modeAware: true \});
|
|
3903
|
+
*
|
|
3904
|
+
* // Later, disable mode-aware logging without changing the custom logger
|
|
3905
|
+
* context.configureLogger(\{ modeAware: false \});
|
|
3906
|
+
*/
|
|
3907
|
+
configureLogger(config) {
|
|
3908
|
+
if (config.customLogger !== undefined) {
|
|
3909
|
+
this.durableLogger = config.customLogger;
|
|
3910
|
+
this.durableLogger.configureDurableLoggingContext?.(this.getDurableLoggingContext());
|
|
3911
|
+
this.logger = this.createModeAwareLogger(this.durableLogger);
|
|
3912
|
+
}
|
|
3913
|
+
if (config.modeAware !== undefined) {
|
|
3914
|
+
this.modeAwareLoggingEnabled = config.modeAware;
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
createCallback(nameOrConfig, maybeConfig) {
|
|
3918
|
+
validateContextUsage(this._stepPrefix, "createCallback", this.executionContext.terminationManager);
|
|
3919
|
+
return this.withDurableModeManagement(() => {
|
|
3920
|
+
const callbackFactory = createCallback(this.executionContext, this.checkpoint, this.createStepId.bind(this), this.hasRunningOperations.bind(this), this.getOperationsEmitter.bind(this), this.checkAndUpdateReplayMode.bind(this), this._parentId);
|
|
3921
|
+
return callbackFactory(nameOrConfig, maybeConfig);
|
|
3922
|
+
});
|
|
3923
|
+
}
|
|
3924
|
+
waitForCallback(nameOrSubmitter, submitterOrConfig, maybeConfig) {
|
|
3925
|
+
validateContextUsage(this._stepPrefix, "waitForCallback", this.executionContext.terminationManager);
|
|
3926
|
+
return this.withDurableModeManagement(() => {
|
|
3927
|
+
const waitForCallbackHandler = createWaitForCallbackHandler(this.executionContext, this.getNextStepId.bind(this), this.runInChildContext.bind(this));
|
|
3928
|
+
return waitForCallbackHandler(nameOrSubmitter, submitterOrConfig, maybeConfig);
|
|
3929
|
+
});
|
|
3930
|
+
}
|
|
3931
|
+
waitForCondition(nameOrCheckFunc, checkFuncOrConfig, maybeConfig) {
|
|
3932
|
+
validateContextUsage(this._stepPrefix, "waitForCondition", this.executionContext.terminationManager);
|
|
3933
|
+
return this.withDurableModeManagement(() => {
|
|
3934
|
+
const waitForConditionHandler = createWaitForConditionHandler(this.executionContext, this.checkpoint, this.createStepId.bind(this), this.durableLogger, this.addRunningOperation.bind(this), this.removeRunningOperation.bind(this), this.hasRunningOperations.bind(this), this.getOperationsEmitter.bind(this), this._parentId);
|
|
3935
|
+
return typeof nameOrCheckFunc === "string" ||
|
|
3936
|
+
nameOrCheckFunc === undefined
|
|
3937
|
+
? waitForConditionHandler(nameOrCheckFunc, checkFuncOrConfig, maybeConfig)
|
|
3938
|
+
: waitForConditionHandler(nameOrCheckFunc, checkFuncOrConfig);
|
|
3939
|
+
});
|
|
3940
|
+
}
|
|
3941
|
+
map(nameOrItems, itemsOrMapFunc, mapFuncOrConfig, maybeConfig) {
|
|
3942
|
+
validateContextUsage(this._stepPrefix, "map", this.executionContext.terminationManager);
|
|
3943
|
+
return this.withDurableModeManagement(() => {
|
|
3944
|
+
const mapHandler = createMapHandler(this.executionContext, this._executeConcurrently.bind(this));
|
|
3945
|
+
return mapHandler(nameOrItems, itemsOrMapFunc, mapFuncOrConfig, maybeConfig);
|
|
3946
|
+
});
|
|
3947
|
+
}
|
|
3948
|
+
parallel(nameOrBranches, branchesOrConfig, maybeConfig) {
|
|
3949
|
+
validateContextUsage(this._stepPrefix, "parallel", this.executionContext.terminationManager);
|
|
3950
|
+
return this.withDurableModeManagement(() => {
|
|
3951
|
+
const parallelHandler = createParallelHandler(this.executionContext, this._executeConcurrently.bind(this));
|
|
3952
|
+
return parallelHandler(nameOrBranches, branchesOrConfig, maybeConfig);
|
|
3953
|
+
});
|
|
3954
|
+
}
|
|
3955
|
+
_executeConcurrently(nameOrItems, itemsOrExecutor, executorOrConfig, maybeConfig) {
|
|
3956
|
+
validateContextUsage(this._stepPrefix, "_executeConcurrently", this.executionContext.terminationManager);
|
|
3957
|
+
return this.withDurableModeManagement(() => {
|
|
3958
|
+
const concurrentExecutionHandler = createConcurrentExecutionHandler(this.executionContext, this.runInChildContext.bind(this), this.skipNextOperation.bind(this));
|
|
3959
|
+
const promise = concurrentExecutionHandler(nameOrItems, itemsOrExecutor, executorOrConfig, maybeConfig);
|
|
3960
|
+
// Prevent unhandled promise rejections
|
|
3961
|
+
promise?.catch(() => { });
|
|
3962
|
+
return promise;
|
|
3963
|
+
});
|
|
3964
|
+
}
|
|
3965
|
+
get promise() {
|
|
3966
|
+
return createPromiseHandler(this.step.bind(this));
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
const createDurableContext = (executionContext, parentContext, durableExecutionMode, inheritedLogger, stepPrefix, durableExecution, parentId) => {
|
|
3970
|
+
return new DurableContextImpl(executionContext, parentContext, durableExecutionMode, inheritedLogger, stepPrefix, durableExecution, parentId);
|
|
3971
|
+
};
|
|
3972
|
+
|
|
3973
|
+
/*
|
|
3974
|
+
Second Approach (Promise-based):
|
|
3975
|
+
Pros:
|
|
3976
|
+
- Simpler implementation
|
|
3977
|
+
- Single promise instance for the entire lifecycle
|
|
3978
|
+
- More memory efficient as it doesn't create new promises on each getTerminationPromise() call
|
|
3979
|
+
- More predictable behavior since there's only one resolution path
|
|
3980
|
+
Cons:
|
|
3981
|
+
- Less flexible as it doesn't support multiple listeners
|
|
3982
|
+
- Once the promise is resolved, it stays resolved (can't be reset)
|
|
3983
|
+
- Doesn't leverage Node.js's event system
|
|
3984
|
+
*/
|
|
3985
|
+
class TerminationManager extends events.EventEmitter {
|
|
3986
|
+
isTerminated = false;
|
|
3987
|
+
terminationDetails;
|
|
3988
|
+
resolveTermination;
|
|
3989
|
+
terminationPromise;
|
|
3990
|
+
setCheckpointTerminating;
|
|
3991
|
+
constructor(setCheckpointTerminating) {
|
|
3992
|
+
super();
|
|
3993
|
+
this.setCheckpointTerminating = setCheckpointTerminating;
|
|
3994
|
+
// Create the promise immediately during construction
|
|
3995
|
+
this.terminationPromise = new Promise((resolve) => {
|
|
3996
|
+
this.resolveTermination = resolve;
|
|
3997
|
+
});
|
|
3998
|
+
}
|
|
3999
|
+
setCheckpointTerminatingCallback(callback) {
|
|
4000
|
+
this.setCheckpointTerminating = callback;
|
|
4001
|
+
}
|
|
4002
|
+
terminate(options = {}) {
|
|
4003
|
+
if (this.isTerminated)
|
|
4004
|
+
return;
|
|
4005
|
+
// Set checkpoint termination flag before any other termination logic
|
|
4006
|
+
this.setCheckpointTerminating?.();
|
|
4007
|
+
this.isTerminated = true;
|
|
4008
|
+
this.terminationDetails = {
|
|
4009
|
+
reason: options.reason ?? TerminationReason.OPERATION_TERMINATED,
|
|
4010
|
+
message: options.message ?? "Operation terminated",
|
|
4011
|
+
error: options.error,
|
|
4012
|
+
cleanup: options.cleanup,
|
|
4013
|
+
};
|
|
4014
|
+
// Instead of emitting an event, resolve the promise
|
|
4015
|
+
if (this.resolveTermination) {
|
|
4016
|
+
this.handleTermination(this.terminationDetails).then(this.resolveTermination);
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
4019
|
+
getTerminationPromise() {
|
|
4020
|
+
return this.terminationPromise;
|
|
4021
|
+
}
|
|
4022
|
+
async handleTermination(details) {
|
|
4023
|
+
if (details.cleanup) {
|
|
4024
|
+
try {
|
|
4025
|
+
await details.cleanup();
|
|
4026
|
+
}
|
|
4027
|
+
catch { }
|
|
4028
|
+
}
|
|
4029
|
+
return {
|
|
4030
|
+
reason: details.reason,
|
|
4031
|
+
message: details.message,
|
|
4032
|
+
error: details.error,
|
|
4033
|
+
};
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
// The logic from this file is based on the NodeJS RIC LogPatch functionality for parity with standard Lambda functions. We should always
|
|
4038
|
+
// align the default behaviour of how logs are emitted to match the RIC logging behaviour for consistency.
|
|
4039
|
+
// For custom logic, users can implement their own logger to log data differently.
|
|
4040
|
+
// See: https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/962ed28eefbc052389c4de4366b1c0c49ee08a13/src/LogPatch.js
|
|
4041
|
+
/**
|
|
4042
|
+
* JSON.stringify replacer function for Error objects.
|
|
4043
|
+
* Based on AWS Lambda Runtime Interface Client LogPatch functionality.
|
|
4044
|
+
* Transforms Error instances into serializable objects with structured error information,
|
|
4045
|
+
* emulating the default Node.js console behavior in Lambda environments.
|
|
4046
|
+
*
|
|
4047
|
+
* @param _key - The property key (unused in this replacer)
|
|
4048
|
+
* @param value - The value being stringified
|
|
4049
|
+
* @returns The original value, or a structured error object for Error instances
|
|
4050
|
+
*/
|
|
4051
|
+
function jsonErrorReplacer(_key, value) {
|
|
4052
|
+
if (value instanceof Error) {
|
|
4053
|
+
return Object.assign({
|
|
4054
|
+
errorType: value?.constructor?.name ?? "UnknownError",
|
|
4055
|
+
errorMessage: value.message,
|
|
4056
|
+
stackTrace: typeof value.stack === "string"
|
|
4057
|
+
? value.stack.split("\n")
|
|
4058
|
+
: value.stack,
|
|
4059
|
+
}, value);
|
|
4060
|
+
}
|
|
4061
|
+
return value;
|
|
4062
|
+
}
|
|
4063
|
+
/**
|
|
4064
|
+
* Formats durable log data into structured JSON string output.
|
|
4065
|
+
* Emulates AWS Lambda Runtime Interface Client's formatJsonMessage functionality
|
|
4066
|
+
* to provide consistent logging format with standard Lambda functions.
|
|
4067
|
+
*
|
|
4068
|
+
* The function handles two main scenarios:
|
|
4069
|
+
* 1. Single parameter: Attempts to stringify directly, falls back to util.format on error
|
|
4070
|
+
* 2. Multiple parameters: Uses util.format to create message, extracts error details if present
|
|
4071
|
+
*
|
|
4072
|
+
* This approach mirrors the RIC's behavior of:
|
|
4073
|
+
* - Using util.format for message formatting (same as console.log)
|
|
4074
|
+
* - Handling circular references gracefully with fallback formatting
|
|
4075
|
+
* - Extracting structured error information when Error objects are present
|
|
4076
|
+
* - Including optional tenantId when available
|
|
4077
|
+
*
|
|
4078
|
+
* @param level - The log level for this message
|
|
4079
|
+
* @param logData - Durable execution context data (requestId, executionArn, etc.)
|
|
4080
|
+
* @param messageParams - Variable number of message parameters to log
|
|
4081
|
+
* @returns JSON string representation of the structured log entry
|
|
4082
|
+
*/
|
|
4083
|
+
function formatDurableLogData(level, logData, ...messageParams) {
|
|
4084
|
+
const result = {
|
|
4085
|
+
requestId: logData.requestId,
|
|
4086
|
+
timestamp: new Date().toISOString(),
|
|
4087
|
+
level: level.toUpperCase(),
|
|
4088
|
+
executionArn: logData.executionArn,
|
|
4089
|
+
};
|
|
4090
|
+
const tenantId = logData.tenantId;
|
|
4091
|
+
if (tenantId != undefined && tenantId != null) {
|
|
4092
|
+
result.tenantId = tenantId;
|
|
4093
|
+
}
|
|
4094
|
+
if (logData.operationId !== undefined) {
|
|
4095
|
+
result.operationId = logData.operationId;
|
|
4096
|
+
}
|
|
4097
|
+
if (logData.attempt !== undefined) {
|
|
4098
|
+
result.attempt = logData.attempt;
|
|
4099
|
+
}
|
|
4100
|
+
if (messageParams.length === 1) {
|
|
4101
|
+
result.message = messageParams[0];
|
|
4102
|
+
try {
|
|
4103
|
+
return JSON.stringify(result, jsonErrorReplacer);
|
|
4104
|
+
}
|
|
4105
|
+
catch (_) {
|
|
4106
|
+
result.message = util.format(result.message);
|
|
4107
|
+
return JSON.stringify(result);
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
result.message = util.format(...messageParams);
|
|
4111
|
+
for (const param of messageParams) {
|
|
4112
|
+
if (param instanceof Error) {
|
|
4113
|
+
result.errorType = param?.constructor?.name ?? "UnknownError";
|
|
4114
|
+
result.errorMessage = param.message;
|
|
4115
|
+
result.stackTrace =
|
|
4116
|
+
typeof param.stack === "string" ? param.stack.split("\n") : [];
|
|
4117
|
+
break;
|
|
4118
|
+
}
|
|
4119
|
+
}
|
|
4120
|
+
return JSON.stringify(result);
|
|
4121
|
+
}
|
|
4122
|
+
/**
|
|
4123
|
+
* Default logger class that outputs structured logs to console.
|
|
4124
|
+
*
|
|
4125
|
+
* This logger emulates the AWS Lambda Runtime Interface Client (RIC) console patching
|
|
4126
|
+
* behavior to maintain parity with standard Lambda function logging while providing
|
|
4127
|
+
* structured output suitable for durable execution contexts.
|
|
4128
|
+
*
|
|
4129
|
+
* Key RIC behavior emulation:
|
|
4130
|
+
* - Respects AWS_LAMBDA_LOG_LEVEL environment variable for log filtering
|
|
4131
|
+
* - Uses priority-based level filtering (DEBUG=2, INFO=3, WARN=4, ERROR=5)
|
|
4132
|
+
* - Outputs structured JSON with timestamp, requestId, executionArn, and other metadata
|
|
4133
|
+
* - Handles Error objects with structured error information extraction
|
|
4134
|
+
* - Uses Node.js Console instance for proper stdout/stderr routing
|
|
4135
|
+
* - Applies util.format for message formatting (same as console.log behavior)
|
|
4136
|
+
*
|
|
4137
|
+
* Individual logger methods (info, error, warn, debug) are dynamically enabled/disabled
|
|
4138
|
+
* based on the configured log level, defaulting to no-op functions when disabled.
|
|
4139
|
+
* This mirrors how RIC patches console methods conditionally.
|
|
4140
|
+
*/
|
|
4141
|
+
class DefaultLogger {
|
|
4142
|
+
consoleLogger;
|
|
4143
|
+
durableLoggingContext = undefined;
|
|
4144
|
+
executionContext;
|
|
4145
|
+
noOpLog = () => { };
|
|
4146
|
+
constructor(executionContext) {
|
|
4147
|
+
this.executionContext = executionContext;
|
|
4148
|
+
// Override the RIC logger to provide custom attributes on the structured log output
|
|
4149
|
+
this.consoleLogger = new node_console.Console({
|
|
4150
|
+
stdout: process.stdout,
|
|
4151
|
+
stderr: process.stderr,
|
|
4152
|
+
});
|
|
4153
|
+
// Initialize methods with no-op and then configure based on log level
|
|
4154
|
+
this.info = this.noOpLog;
|
|
4155
|
+
this.error = this.noOpLog;
|
|
4156
|
+
this.warn = this.noOpLog;
|
|
4157
|
+
this.debug = this.noOpLog;
|
|
4158
|
+
this.configureLogLevel();
|
|
4159
|
+
}
|
|
4160
|
+
configureLogLevel() {
|
|
4161
|
+
const levels = {
|
|
4162
|
+
DEBUG: { name: "DEBUG", priority: 2 },
|
|
4163
|
+
INFO: { name: "INFO", priority: 3 },
|
|
4164
|
+
WARN: { name: "WARN", priority: 4 },
|
|
4165
|
+
ERROR: { name: "ERROR", priority: 5 },
|
|
4166
|
+
// Not implemented yet. Can be implemented later
|
|
4167
|
+
// TRACE: { name: "TRACE", priority: 1 },
|
|
4168
|
+
// FATAL: { name: "FATAL", priority: 6 },
|
|
4169
|
+
};
|
|
4170
|
+
const logLevelEnvVariable = process.env["AWS_LAMBDA_LOG_LEVEL"]?.toUpperCase();
|
|
4171
|
+
// Default to DEBUG level when env var is invalid/missing
|
|
4172
|
+
const lambdaLogLevel = logLevelEnvVariable && logLevelEnvVariable in levels
|
|
4173
|
+
? levels[logLevelEnvVariable]
|
|
4174
|
+
: levels.DEBUG;
|
|
4175
|
+
// Enable methods based on priority: higher priority = more restrictive
|
|
4176
|
+
// e.g., if WARN is set (priority 4), only WARN and ERROR methods are enabled
|
|
4177
|
+
if (levels.DEBUG.priority >= lambdaLogLevel.priority) {
|
|
4178
|
+
this.debug = (message, ...optionalParams) => {
|
|
4179
|
+
const loggingContext = this.ensureDurableLoggingContext();
|
|
4180
|
+
const params = message !== undefined ? [message, ...optionalParams] : optionalParams;
|
|
4181
|
+
this.consoleLogger.debug(formatDurableLogData(DurableLogLevel.DEBUG, loggingContext.getDurableLogData(), ...params));
|
|
4182
|
+
};
|
|
4183
|
+
}
|
|
4184
|
+
if (levels.INFO.priority >= lambdaLogLevel.priority) {
|
|
4185
|
+
this.info = (message, ...optionalParams) => {
|
|
4186
|
+
const loggingContext = this.ensureDurableLoggingContext();
|
|
4187
|
+
const params = message !== undefined ? [message, ...optionalParams] : optionalParams;
|
|
4188
|
+
this.consoleLogger.info(formatDurableLogData(DurableLogLevel.INFO, loggingContext.getDurableLogData(), ...params));
|
|
4189
|
+
};
|
|
4190
|
+
}
|
|
4191
|
+
if (levels.WARN.priority >= lambdaLogLevel.priority) {
|
|
4192
|
+
this.warn = (message, ...optionalParams) => {
|
|
4193
|
+
const loggingContext = this.ensureDurableLoggingContext();
|
|
4194
|
+
const params = message !== undefined ? [message, ...optionalParams] : optionalParams;
|
|
4195
|
+
this.consoleLogger.warn(formatDurableLogData(DurableLogLevel.WARN, loggingContext.getDurableLogData(), ...params));
|
|
4196
|
+
};
|
|
4197
|
+
}
|
|
4198
|
+
if (levels.ERROR.priority >= lambdaLogLevel.priority) {
|
|
4199
|
+
this.error = (message, ...optionalParams) => {
|
|
4200
|
+
const loggingContext = this.ensureDurableLoggingContext();
|
|
4201
|
+
const params = message !== undefined ? [message, ...optionalParams] : optionalParams;
|
|
4202
|
+
this.consoleLogger.error(formatDurableLogData(DurableLogLevel.ERROR, loggingContext.getDurableLogData(), ...params));
|
|
4203
|
+
};
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
4206
|
+
ensureDurableLoggingContext() {
|
|
4207
|
+
const context = this.executionContext;
|
|
4208
|
+
if (!this.durableLoggingContext && !context) {
|
|
4209
|
+
throw new Error("DurableLoggingContext is not configured. Please call configureDurableLoggingContext before logging.");
|
|
4210
|
+
}
|
|
4211
|
+
if (this.durableLoggingContext) {
|
|
4212
|
+
return this.durableLoggingContext;
|
|
4213
|
+
}
|
|
4214
|
+
if (!context) {
|
|
4215
|
+
throw new Error("Execution context is not provided.");
|
|
4216
|
+
}
|
|
4217
|
+
return {
|
|
4218
|
+
getDurableLogData: () => {
|
|
4219
|
+
return {
|
|
4220
|
+
requestId: context.requestId,
|
|
4221
|
+
executionArn: context.durableExecutionArn,
|
|
4222
|
+
tenantId: context.tenantId,
|
|
4223
|
+
};
|
|
4224
|
+
},
|
|
4225
|
+
};
|
|
4226
|
+
}
|
|
4227
|
+
log(level, message, ...optionalParams) {
|
|
4228
|
+
switch (level) {
|
|
4229
|
+
case DurableLogLevel.DEBUG:
|
|
4230
|
+
this.debug(message, ...optionalParams);
|
|
4231
|
+
break;
|
|
4232
|
+
case DurableLogLevel.INFO:
|
|
4233
|
+
this.info(message, ...optionalParams);
|
|
4234
|
+
break;
|
|
4235
|
+
case DurableLogLevel.WARN:
|
|
4236
|
+
this.warn(message, ...optionalParams);
|
|
4237
|
+
break;
|
|
4238
|
+
case DurableLogLevel.ERROR:
|
|
4239
|
+
this.error(message, ...optionalParams);
|
|
4240
|
+
break;
|
|
4241
|
+
default:
|
|
4242
|
+
this.info(message, ...optionalParams);
|
|
4243
|
+
break;
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
// These method signatures will be set dynamically in configureLogLevel()
|
|
4247
|
+
info;
|
|
4248
|
+
error;
|
|
4249
|
+
warn;
|
|
4250
|
+
debug;
|
|
4251
|
+
configureDurableLoggingContext(durableLoggingContext) {
|
|
4252
|
+
this.durableLoggingContext = durableLoggingContext;
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
/**
|
|
4256
|
+
* Creates a default logger that outputs structured logs to console.
|
|
4257
|
+
*
|
|
4258
|
+
* @param executionContext - Optional execution context for logging
|
|
4259
|
+
* @returns DefaultLogger instance
|
|
4260
|
+
*/
|
|
4261
|
+
const createDefaultLogger = (executionContext) => {
|
|
4262
|
+
return new DefaultLogger(executionContext);
|
|
4263
|
+
};
|
|
4264
|
+
|
|
4265
|
+
/**
|
|
4266
|
+
* Tracks active async operations to prevent premature termination
|
|
4267
|
+
*/
|
|
4268
|
+
class ActiveOperationsTracker {
|
|
4269
|
+
activeCount = 0;
|
|
4270
|
+
/**
|
|
4271
|
+
* Increment the counter when starting an async operation
|
|
4272
|
+
*/
|
|
4273
|
+
increment() {
|
|
4274
|
+
this.activeCount++;
|
|
4275
|
+
}
|
|
4276
|
+
/**
|
|
4277
|
+
* Decrement the counter when an async operation completes
|
|
4278
|
+
*/
|
|
4279
|
+
decrement() {
|
|
4280
|
+
this.activeCount = Math.max(0, this.activeCount - 1);
|
|
4281
|
+
}
|
|
4282
|
+
/**
|
|
4283
|
+
* Check if there are any active operations
|
|
4284
|
+
*/
|
|
4285
|
+
hasActive() {
|
|
4286
|
+
return this.activeCount > 0;
|
|
4287
|
+
}
|
|
4288
|
+
/**
|
|
4289
|
+
* Get the current count of active operations
|
|
4290
|
+
*/
|
|
4291
|
+
getCount() {
|
|
4292
|
+
return this.activeCount;
|
|
4293
|
+
}
|
|
4294
|
+
/**
|
|
4295
|
+
* Reset the counter (useful for testing)
|
|
4296
|
+
*/
|
|
4297
|
+
reset() {
|
|
4298
|
+
this.activeCount = 0;
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
|
|
4302
|
+
let defaultLambdaClient;
|
|
4303
|
+
/**
|
|
4304
|
+
* Durable execution client which uses an API-based LambdaClient
|
|
4305
|
+
* with built-in error logging. By default, the Lambda client will
|
|
4306
|
+
* have custom timeouts set.
|
|
4307
|
+
*
|
|
4308
|
+
* @public
|
|
4309
|
+
*/
|
|
4310
|
+
class DurableExecutionApiClient {
|
|
4311
|
+
client;
|
|
4312
|
+
constructor(client) {
|
|
4313
|
+
if (!client) {
|
|
4314
|
+
if (!defaultLambdaClient) {
|
|
4315
|
+
defaultLambdaClient = new clientLambda.LambdaClient({
|
|
4316
|
+
requestHandler: {
|
|
4317
|
+
connectionTimeout: 5000,
|
|
4318
|
+
socketTimeout: 50000,
|
|
4319
|
+
requestTimeout: 55000,
|
|
4320
|
+
throwOnRequestTimeout: true,
|
|
4321
|
+
},
|
|
4322
|
+
});
|
|
4323
|
+
}
|
|
4324
|
+
this.client = defaultLambdaClient;
|
|
4325
|
+
}
|
|
4326
|
+
else {
|
|
4327
|
+
this.client = client;
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
/**
|
|
4331
|
+
* Gets operation state data from the durable execution
|
|
4332
|
+
* @param params - The GetDurableExecutionState request
|
|
4333
|
+
* @param logger - Optional developer logger for error reporting
|
|
4334
|
+
* @returns Response with operations data
|
|
4335
|
+
*/
|
|
4336
|
+
async getExecutionState(params, logger) {
|
|
4337
|
+
try {
|
|
4338
|
+
const response = await this.client.send(new clientLambda.GetDurableExecutionStateCommand({
|
|
4339
|
+
DurableExecutionArn: params.DurableExecutionArn,
|
|
4340
|
+
CheckpointToken: params.CheckpointToken,
|
|
4341
|
+
Marker: params.Marker,
|
|
4342
|
+
MaxItems: params.MaxItems,
|
|
4343
|
+
}));
|
|
4344
|
+
return response;
|
|
4345
|
+
}
|
|
4346
|
+
catch (error) {
|
|
4347
|
+
// Internal debug logging
|
|
4348
|
+
log("❌", "GetDurableExecutionState failed", {
|
|
4349
|
+
error,
|
|
4350
|
+
requestId: error?.$metadata
|
|
4351
|
+
?.requestId,
|
|
4352
|
+
DurableExecutionArn: params.DurableExecutionArn,
|
|
4353
|
+
CheckpointToken: params.CheckpointToken,
|
|
4354
|
+
Marker: params.Marker,
|
|
4355
|
+
});
|
|
4356
|
+
// Developer logging if logger provided
|
|
4357
|
+
if (logger) {
|
|
4358
|
+
logger.error("Failed to get durable execution state", error, {
|
|
4359
|
+
requestId: error
|
|
4360
|
+
?.$metadata?.requestId,
|
|
4361
|
+
});
|
|
4362
|
+
}
|
|
4363
|
+
throw error;
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
/**
|
|
4367
|
+
* Checkpoints the durable execution with operation updates
|
|
4368
|
+
* @param params - The checkpoint request
|
|
4369
|
+
* @param logger - Optional developer logger for error reporting
|
|
4370
|
+
* @returns Checkpoint response
|
|
4371
|
+
*/
|
|
4372
|
+
async checkpoint(params, logger) {
|
|
4373
|
+
try {
|
|
4374
|
+
const response = await this.client.send(new clientLambda.CheckpointDurableExecutionCommand({
|
|
4375
|
+
DurableExecutionArn: params.DurableExecutionArn,
|
|
4376
|
+
CheckpointToken: params.CheckpointToken,
|
|
4377
|
+
ClientToken: params.ClientToken,
|
|
4378
|
+
Updates: params.Updates,
|
|
4379
|
+
}));
|
|
4380
|
+
return response;
|
|
4381
|
+
}
|
|
4382
|
+
catch (error) {
|
|
4383
|
+
// Internal debug logging
|
|
4384
|
+
log("❌", "CheckpointDurableExecution failed", {
|
|
4385
|
+
error,
|
|
4386
|
+
requestId: error?.$metadata
|
|
4387
|
+
?.requestId,
|
|
4388
|
+
DurableExecutionArn: params.DurableExecutionArn,
|
|
4389
|
+
CheckpointToken: params.CheckpointToken,
|
|
4390
|
+
ClientToken: params.ClientToken,
|
|
4391
|
+
});
|
|
4392
|
+
// Developer logging if logger provided
|
|
4393
|
+
if (logger) {
|
|
4394
|
+
logger.error("Failed to checkpoint durable execution", error, {
|
|
4395
|
+
requestId: error
|
|
4396
|
+
?.$metadata?.requestId,
|
|
4397
|
+
});
|
|
4398
|
+
}
|
|
4399
|
+
throw error;
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
|
|
4404
|
+
/**
|
|
4405
|
+
* Custom DurableExecutionInvocationInput which uses a custom durable
|
|
4406
|
+
* execution client instead of the API-based LambdaClient.
|
|
4407
|
+
*
|
|
4408
|
+
* @internal
|
|
4409
|
+
*/
|
|
4410
|
+
class DurableExecutionInvocationInputWithClient {
|
|
4411
|
+
durableExecutionClient;
|
|
4412
|
+
InitialExecutionState;
|
|
4413
|
+
DurableExecutionArn;
|
|
4414
|
+
CheckpointToken;
|
|
4415
|
+
constructor(params, durableExecutionClient) {
|
|
4416
|
+
this.durableExecutionClient = durableExecutionClient;
|
|
4417
|
+
this.InitialExecutionState = params.InitialExecutionState;
|
|
4418
|
+
this.DurableExecutionArn = params.DurableExecutionArn;
|
|
4419
|
+
this.CheckpointToken = params.CheckpointToken;
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
|
|
4423
|
+
const initializeExecutionContext = async (event, context, lambdaClient) => {
|
|
4424
|
+
log("🔵", "Initializing durable function with event:", event);
|
|
4425
|
+
log("📍", "Function Input:", event);
|
|
4426
|
+
const checkpointToken = event.CheckpointToken;
|
|
4427
|
+
const durableExecutionArn = event.DurableExecutionArn;
|
|
4428
|
+
const durableExecutionClient =
|
|
4429
|
+
// Allow passing arbitrary durable clients if the input is a custom class
|
|
4430
|
+
event instanceof DurableExecutionInvocationInputWithClient
|
|
4431
|
+
? event.durableExecutionClient
|
|
4432
|
+
: new DurableExecutionApiClient(lambdaClient);
|
|
4433
|
+
// Create logger for initialization errors using existing logger factory
|
|
4434
|
+
const initLogger = createDefaultLogger({
|
|
4435
|
+
durableExecutionArn,
|
|
4436
|
+
requestId: context.awsRequestId,
|
|
4437
|
+
tenantId: context.tenantId,
|
|
4438
|
+
});
|
|
4439
|
+
const operationsArray = [...(event.InitialExecutionState.Operations || [])];
|
|
4440
|
+
let nextMarker = event.InitialExecutionState.NextMarker;
|
|
4441
|
+
while (nextMarker) {
|
|
4442
|
+
const response = await durableExecutionClient.getExecutionState({
|
|
4443
|
+
CheckpointToken: checkpointToken,
|
|
4444
|
+
Marker: nextMarker,
|
|
4445
|
+
DurableExecutionArn: durableExecutionArn,
|
|
4446
|
+
MaxItems: 1000,
|
|
4447
|
+
}, initLogger);
|
|
4448
|
+
operationsArray.push(...(response.Operations || []));
|
|
4449
|
+
nextMarker = response.NextMarker || "";
|
|
4450
|
+
}
|
|
4451
|
+
// Determine replay mode based on operations array length
|
|
4452
|
+
const durableExecutionMode = operationsArray.length > 1
|
|
4453
|
+
? DurableExecutionMode.ReplayMode
|
|
4454
|
+
: DurableExecutionMode.ExecutionMode;
|
|
4455
|
+
log("📝", "Operations:", operationsArray);
|
|
4456
|
+
const stepData = operationsArray.reduce((acc, operation) => {
|
|
4457
|
+
if (operation.Id) {
|
|
4458
|
+
// The stepData received from backend has Id and ParentId as hash, so no need to hash it again
|
|
4459
|
+
acc[operation.Id] = operation;
|
|
4460
|
+
}
|
|
4461
|
+
return acc;
|
|
4462
|
+
}, {});
|
|
4463
|
+
log("📝", "Loaded step data:", stepData);
|
|
4464
|
+
return {
|
|
4465
|
+
executionContext: {
|
|
4466
|
+
durableExecutionClient,
|
|
4467
|
+
_stepData: stepData,
|
|
4468
|
+
terminationManager: new TerminationManager(),
|
|
4469
|
+
activeOperationsTracker: new ActiveOperationsTracker(),
|
|
4470
|
+
durableExecutionArn,
|
|
4471
|
+
pendingCompletions: new Set(),
|
|
4472
|
+
getStepData(stepId) {
|
|
4473
|
+
return getStepData(stepData, stepId);
|
|
4474
|
+
},
|
|
4475
|
+
tenantId: context.tenantId,
|
|
4476
|
+
requestId: context.awsRequestId,
|
|
4477
|
+
},
|
|
4478
|
+
durableExecutionMode,
|
|
4479
|
+
checkpointToken,
|
|
4480
|
+
};
|
|
4481
|
+
};
|
|
4482
|
+
|
|
4483
|
+
// Lambda response size limit is 6MB
|
|
4484
|
+
const LAMBDA_RESPONSE_SIZE_LIMIT = 6 * 1024 * 1024 - 50; // 6MB in bytes, minus 50 bytes for envelope
|
|
4485
|
+
async function runHandler(event, context, executionContext, durableExecutionMode, checkpointToken, handler) {
|
|
4486
|
+
// Create checkpoint manager and step data emitter
|
|
4487
|
+
const stepDataEmitter = new events.EventEmitter();
|
|
4488
|
+
const checkpointManager = new CheckpointManager(executionContext.durableExecutionArn, executionContext._stepData, executionContext.durableExecutionClient, executionContext.terminationManager, executionContext.activeOperationsTracker, checkpointToken, stepDataEmitter, createDefaultLogger(executionContext), executionContext.pendingCompletions);
|
|
4489
|
+
// Set the checkpoint terminating callback on the termination manager
|
|
4490
|
+
executionContext.terminationManager.setCheckpointTerminatingCallback(() => {
|
|
4491
|
+
checkpointManager.setTerminating();
|
|
4492
|
+
});
|
|
4493
|
+
const durableExecution = {
|
|
4494
|
+
checkpointManager,
|
|
4495
|
+
stepDataEmitter,
|
|
4496
|
+
setTerminating: () => checkpointManager.setTerminating(),
|
|
4497
|
+
};
|
|
4498
|
+
const durableContext = createDurableContext(executionContext, context, durableExecutionMode,
|
|
4499
|
+
// Default logger may not have the same type as Logger, but we should always provide a default logger even if the user overrides it
|
|
4500
|
+
createDefaultLogger(), undefined, durableExecution);
|
|
4501
|
+
// Extract customerHandlerEvent from the original event
|
|
4502
|
+
const initialExecutionEvent = event.InitialExecutionState.Operations?.[0];
|
|
4503
|
+
const customerHandlerEvent = JSON.parse(initialExecutionEvent?.ExecutionDetails?.InputPayload ?? "{}");
|
|
4504
|
+
try {
|
|
4505
|
+
log("🎯", `Starting handler execution, handler event: ${customerHandlerEvent}`);
|
|
4506
|
+
let handlerPromiseResolved = false;
|
|
4507
|
+
let terminationPromiseResolved = false;
|
|
4508
|
+
const handlerPromise = runWithContext("root", undefined, () => handler(customerHandlerEvent, durableContext)).then((result) => {
|
|
4509
|
+
handlerPromiseResolved = true;
|
|
4510
|
+
log("🏆", "Handler promise resolved first!");
|
|
4511
|
+
return ["handler", result];
|
|
4512
|
+
});
|
|
4513
|
+
const terminationPromise = executionContext.terminationManager
|
|
4514
|
+
.getTerminationPromise()
|
|
4515
|
+
.then((result) => {
|
|
4516
|
+
terminationPromiseResolved = true;
|
|
4517
|
+
log("💥", "Termination promise resolved first!");
|
|
4518
|
+
// Set checkpoint manager as terminating when termination starts
|
|
4519
|
+
durableExecution.setTerminating();
|
|
4520
|
+
return ["termination", result];
|
|
4521
|
+
});
|
|
4522
|
+
// Set up a timeout to log the state of promises after a short delay
|
|
4523
|
+
setTimeout(() => {
|
|
4524
|
+
log("⏱️", "Promise race status check:", {
|
|
4525
|
+
handlerResolved: handlerPromiseResolved,
|
|
4526
|
+
terminationResolved: terminationPromiseResolved,
|
|
4527
|
+
});
|
|
4528
|
+
}, 500);
|
|
4529
|
+
const [resultType, result] = await Promise.race([
|
|
4530
|
+
handlerPromise,
|
|
4531
|
+
terminationPromise,
|
|
4532
|
+
]);
|
|
4533
|
+
log("🏁", "Promise race completed with:", {
|
|
4534
|
+
resultType,
|
|
4535
|
+
});
|
|
4536
|
+
// Wait for all pending checkpoints to complete
|
|
4537
|
+
try {
|
|
4538
|
+
await durableExecution.checkpointManager.waitForQueueCompletion();
|
|
4539
|
+
log("✅", "All pending checkpoints completed");
|
|
4540
|
+
}
|
|
4541
|
+
catch (error) {
|
|
4542
|
+
log("⚠️", "Error waiting for checkpoint completion:", error);
|
|
4543
|
+
}
|
|
4544
|
+
// If termination was due to checkpoint failure, throw the appropriate error
|
|
4545
|
+
if (resultType === "termination" &&
|
|
4546
|
+
result.reason === TerminationReason.CHECKPOINT_FAILED) {
|
|
4547
|
+
log("🛑", "Checkpoint failed - handling termination");
|
|
4548
|
+
// checkpoint.ts always provides classified error
|
|
4549
|
+
throw result.error;
|
|
4550
|
+
}
|
|
4551
|
+
// If termination was due to serdes failure, throw an error to terminate the Lambda
|
|
4552
|
+
if (resultType === "termination" &&
|
|
4553
|
+
result.reason === TerminationReason.SERDES_FAILED) {
|
|
4554
|
+
log("🛑", "Serdes failed - terminating Lambda execution");
|
|
4555
|
+
throw new SerdesFailedError(result.message);
|
|
4556
|
+
}
|
|
4557
|
+
// If termination was due to context validation error, return FAILED
|
|
4558
|
+
if (resultType === "termination" &&
|
|
4559
|
+
result.reason === TerminationReason.CONTEXT_VALIDATION_ERROR) {
|
|
4560
|
+
log("🛑", "Context validation error - returning FAILED status");
|
|
4561
|
+
return {
|
|
4562
|
+
Status: exports.InvocationStatus.FAILED,
|
|
4563
|
+
Error: createErrorObjectFromError(result.error || new Error(result.message)),
|
|
4564
|
+
};
|
|
4565
|
+
}
|
|
4566
|
+
if (resultType === "termination") {
|
|
4567
|
+
log("🛑", "Returning termination response");
|
|
4568
|
+
return {
|
|
4569
|
+
Status: exports.InvocationStatus.PENDING,
|
|
4570
|
+
};
|
|
4571
|
+
}
|
|
4572
|
+
log("✅", "Returning normal completion response");
|
|
4573
|
+
// Stringify the result once to avoid multiple JSON.stringify calls
|
|
4574
|
+
const serializedResult = JSON.stringify(result);
|
|
4575
|
+
const serializedSize = new TextEncoder().encode(serializedResult).length;
|
|
4576
|
+
// Check if the response size exceeds the Lambda limit
|
|
4577
|
+
// Note: JSON.stringify(undefined) returns undefined, so we need to handle that case
|
|
4578
|
+
if (serializedResult && serializedSize > LAMBDA_RESPONSE_SIZE_LIMIT) {
|
|
4579
|
+
log("📦", `Response size (${serializedSize} bytes) exceeds Lambda limit (${LAMBDA_RESPONSE_SIZE_LIMIT} bytes). Checkpointing result.`);
|
|
4580
|
+
// Create a checkpoint to save the large result
|
|
4581
|
+
const stepId = `execution-result-${Date.now()}`;
|
|
4582
|
+
try {
|
|
4583
|
+
await durableExecution.checkpointManager.checkpoint(stepId, {
|
|
4584
|
+
Id: stepId,
|
|
4585
|
+
Action: "SUCCEED",
|
|
4586
|
+
Type: clientLambda.OperationType.EXECUTION,
|
|
4587
|
+
Payload: serializedResult, // Reuse the already serialized result
|
|
4588
|
+
});
|
|
4589
|
+
log("✅", "Large result successfully checkpointed");
|
|
4590
|
+
// Return a response indicating the result was checkpointed
|
|
4591
|
+
return {
|
|
4592
|
+
Status: exports.InvocationStatus.SUCCEEDED,
|
|
4593
|
+
Result: "",
|
|
4594
|
+
};
|
|
4595
|
+
}
|
|
4596
|
+
catch (checkpointError) {
|
|
4597
|
+
log("❌", "Failed to checkpoint large result:", checkpointError);
|
|
4598
|
+
// Re-throw - checkpoint.ts always classifies errors before terminating
|
|
4599
|
+
throw checkpointError;
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4602
|
+
// If response size is acceptable, return the response
|
|
4603
|
+
return {
|
|
4604
|
+
Status: exports.InvocationStatus.SUCCEEDED,
|
|
4605
|
+
Result: serializedResult,
|
|
4606
|
+
};
|
|
4607
|
+
}
|
|
4608
|
+
catch (error) {
|
|
4609
|
+
log("❌", "Handler threw an error:", error);
|
|
4610
|
+
// Check if this is an unrecoverable invocation error (includes checkpoint invocation failures)
|
|
4611
|
+
if (isUnrecoverableInvocationError(error)) {
|
|
4612
|
+
log("🛑", "Unrecoverable invocation error - terminating Lambda execution");
|
|
4613
|
+
throw error; // Re-throw the error to terminate Lambda execution
|
|
4614
|
+
}
|
|
4615
|
+
return {
|
|
4616
|
+
Status: exports.InvocationStatus.FAILED,
|
|
4617
|
+
Error: createErrorObjectFromError(error),
|
|
4618
|
+
};
|
|
4619
|
+
}
|
|
4620
|
+
}
|
|
4621
|
+
/**
|
|
4622
|
+
* Validates that the event is a proper durable execution input
|
|
4623
|
+
*/
|
|
4624
|
+
function validateDurableExecutionEvent(event) {
|
|
4625
|
+
try {
|
|
4626
|
+
const eventObj = event;
|
|
4627
|
+
if (!eventObj?.DurableExecutionArn || !eventObj?.CheckpointToken) {
|
|
4628
|
+
throw new Error("Missing required durable execution fields");
|
|
4629
|
+
}
|
|
4630
|
+
}
|
|
4631
|
+
catch {
|
|
4632
|
+
const msg = `Unexpected payload provided to start the durable execution.
|
|
4633
|
+
Check your resource configurations to confirm the durability is set.`;
|
|
4634
|
+
throw new Error(msg);
|
|
4635
|
+
}
|
|
4636
|
+
}
|
|
4637
|
+
/**
|
|
4638
|
+
* Wraps a durable handler function to create a handler with automatic state persistence,
|
|
4639
|
+
* retry logic, and workflow orchestration capabilities.
|
|
4640
|
+
*
|
|
4641
|
+
* This function transforms your durable handler into a function that integrates
|
|
4642
|
+
* with the AWS Durable Execution service. The wrapped handler automatically manages execution state
|
|
4643
|
+
* and checkpointing.
|
|
4644
|
+
*
|
|
4645
|
+
* @typeParam TEvent - The type of the input event your handler expects (defaults to any)
|
|
4646
|
+
* @typeParam TResult - The type of the result your handler returns (defaults to any)
|
|
4647
|
+
* @typeParam TLogger - The type of custom logger implementation (defaults to DurableLogger)
|
|
4648
|
+
*
|
|
4649
|
+
* @param handler - Your durable handler function that uses the DurableContext for operations
|
|
4650
|
+
* @param config - Optional configuration for custom advanced settings
|
|
4651
|
+
*
|
|
4652
|
+
* @returns A handler function that automatically manages durability
|
|
4653
|
+
*
|
|
4654
|
+
* @example
|
|
4655
|
+
* **Basic Usage:**
|
|
4656
|
+
* ```typescript
|
|
4657
|
+
* import { withDurableExecution, DurableExecutionHandler } from '@aws/durable-execution-sdk-js';
|
|
4658
|
+
*
|
|
4659
|
+
* const durableHandler: DurableExecutionHandler<MyEvent, MyResult> = async (event, context) => {
|
|
4660
|
+
* // Execute durable operations with automatic retry and checkpointing
|
|
4661
|
+
* const userData = await context.step("fetch-user", async () =>
|
|
4662
|
+
* fetchUserFromDB(event.userId)
|
|
4663
|
+
* );
|
|
4664
|
+
*
|
|
4665
|
+
* // Wait for external approval
|
|
4666
|
+
* const approval = await context.waitForCallback("user-approval", async (callbackId) => {
|
|
4667
|
+
* await sendApprovalEmail(callbackId, userData);
|
|
4668
|
+
* });
|
|
4669
|
+
*
|
|
4670
|
+
* // Process in parallel
|
|
4671
|
+
* const results = await context.parallel("process-data", [
|
|
4672
|
+
* async (ctx) => ctx.step("validate", () => validateData(userData)),
|
|
4673
|
+
* async (ctx) => ctx.step("transform", () => transformData(userData))
|
|
4674
|
+
* ]);
|
|
4675
|
+
*
|
|
4676
|
+
* return { success: true, results };
|
|
4677
|
+
* };
|
|
4678
|
+
*
|
|
4679
|
+
* export const handler = withDurableExecution(durableHandler);
|
|
4680
|
+
* ```
|
|
4681
|
+
*
|
|
4682
|
+
* @example
|
|
4683
|
+
* **With Custom Configuration:**
|
|
4684
|
+
* ```typescript
|
|
4685
|
+
* import { LambdaClient } from '@aws-sdk/client-lambda';
|
|
4686
|
+
*
|
|
4687
|
+
* const customClient = new LambdaClient({
|
|
4688
|
+
* region: 'us-west-2',
|
|
4689
|
+
* maxAttempts: 5
|
|
4690
|
+
* });
|
|
4691
|
+
*
|
|
4692
|
+
* export const handler = withDurableExecution(durableHandler, {
|
|
4693
|
+
* client: customClient
|
|
4694
|
+
* });
|
|
4695
|
+
* ```
|
|
4696
|
+
*
|
|
4697
|
+
* @example
|
|
4698
|
+
* **Passed Directly to the Handler:**
|
|
4699
|
+
* ```typescript
|
|
4700
|
+
* export const handler = withDurableExecution(async (event, context) => {
|
|
4701
|
+
* const result = await context.step(async () => processEvent(event));
|
|
4702
|
+
* return result;
|
|
4703
|
+
* });
|
|
4704
|
+
* ```
|
|
4705
|
+
*
|
|
4706
|
+
* @public
|
|
4707
|
+
*/
|
|
4708
|
+
const withDurableExecution = (handler, config) => {
|
|
4709
|
+
return async (event, context) => {
|
|
4710
|
+
validateDurableExecutionEvent(event);
|
|
4711
|
+
const { executionContext, durableExecutionMode, checkpointToken } = await initializeExecutionContext(event, context, config?.client);
|
|
4712
|
+
let response = null;
|
|
4713
|
+
try {
|
|
4714
|
+
response = await runHandler(event, context, executionContext, durableExecutionMode, checkpointToken, handler);
|
|
4715
|
+
return response;
|
|
4716
|
+
}
|
|
4717
|
+
catch (err) {
|
|
4718
|
+
throw err;
|
|
4719
|
+
}
|
|
4720
|
+
};
|
|
4721
|
+
};
|
|
4722
|
+
|
|
4723
|
+
const DEFAULT_CONFIG = {
|
|
4724
|
+
maxAttempts: 60,
|
|
4725
|
+
initialDelay: { seconds: 5 },
|
|
4726
|
+
maxDelay: { seconds: 300 }, // 5 minutes
|
|
4727
|
+
backoffRate: 1.5,
|
|
4728
|
+
jitter: exports.JitterStrategy.FULL,
|
|
4729
|
+
timeoutSeconds: undefined, // No timeout by default
|
|
4730
|
+
};
|
|
4731
|
+
const applyJitter = (delay, strategy) => {
|
|
4732
|
+
switch (strategy) {
|
|
4733
|
+
case exports.JitterStrategy.NONE:
|
|
4734
|
+
return delay;
|
|
4735
|
+
case exports.JitterStrategy.FULL:
|
|
4736
|
+
// Random between 0 and delay
|
|
4737
|
+
return Math.random() * delay;
|
|
4738
|
+
case exports.JitterStrategy.HALF:
|
|
4739
|
+
// Random between delay/2 and delay
|
|
4740
|
+
return delay / 2 + Math.random() * (delay / 2);
|
|
4741
|
+
}
|
|
4742
|
+
};
|
|
4743
|
+
/**
|
|
4744
|
+
* @public
|
|
4745
|
+
*/
|
|
4746
|
+
const createWaitStrategy = (config) => {
|
|
4747
|
+
const finalConfig = {
|
|
4748
|
+
...DEFAULT_CONFIG,
|
|
4749
|
+
...config,
|
|
4750
|
+
};
|
|
4751
|
+
return (result, attemptsMade) => {
|
|
4752
|
+
// Check if condition is met
|
|
4753
|
+
if (!finalConfig.shouldContinuePolling(result)) {
|
|
4754
|
+
return { shouldContinue: false };
|
|
4755
|
+
}
|
|
4756
|
+
// Check if we've exceeded max attempts
|
|
4757
|
+
if (attemptsMade >= finalConfig.maxAttempts) {
|
|
4758
|
+
throw new Error(`waitForCondition exceeded maximum attempts (${finalConfig.maxAttempts})`);
|
|
4759
|
+
}
|
|
4760
|
+
// Calculate delay with exponential backoff
|
|
4761
|
+
const initialDelaySeconds = durationToSeconds(finalConfig.initialDelay);
|
|
4762
|
+
const maxDelaySeconds = durationToSeconds(finalConfig.maxDelay);
|
|
4763
|
+
const baseDelay = Math.min(initialDelaySeconds * Math.pow(finalConfig.backoffRate, attemptsMade - 1), maxDelaySeconds);
|
|
4764
|
+
// Apply jitter
|
|
4765
|
+
const delayWithJitter = applyJitter(baseDelay, finalConfig.jitter);
|
|
4766
|
+
// Ensure delay is an integer >= 1
|
|
4767
|
+
const finalDelay = Math.max(1, Math.round(delayWithJitter));
|
|
4768
|
+
return { shouldContinue: true, delay: { seconds: finalDelay } };
|
|
4769
|
+
};
|
|
4770
|
+
};
|
|
4771
|
+
|
|
4772
|
+
exports.CallbackError = CallbackError;
|
|
4773
|
+
exports.ChildContextError = ChildContextError;
|
|
4774
|
+
exports.DurableExecutionApiClient = DurableExecutionApiClient;
|
|
4775
|
+
exports.DurableExecutionInvocationInputWithClient = DurableExecutionInvocationInputWithClient;
|
|
4776
|
+
exports.DurableOperationError = DurableOperationError;
|
|
4777
|
+
exports.DurablePromise = DurablePromise;
|
|
4778
|
+
exports.InvokeError = InvokeError;
|
|
4779
|
+
exports.StepError = StepError;
|
|
4780
|
+
exports.StepInterruptedError = StepInterruptedError;
|
|
4781
|
+
exports.WaitForConditionError = WaitForConditionError;
|
|
4782
|
+
exports.createClassSerdes = createClassSerdes;
|
|
4783
|
+
exports.createClassSerdesWithDates = createClassSerdesWithDates;
|
|
4784
|
+
exports.createRetryStrategy = createRetryStrategy;
|
|
4785
|
+
exports.createWaitStrategy = createWaitStrategy;
|
|
4786
|
+
exports.defaultSerdes = defaultSerdes;
|
|
4787
|
+
exports.retryPresets = retryPresets;
|
|
4788
|
+
exports.withDurableExecution = withDurableExecution;
|
|
4789
|
+
//# sourceMappingURL=index.js.map
|