@aws/durable-execution-sdk-js 1.0.1 → 1.0.3

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.
Files changed (30) hide show
  1. package/README.md +25 -5
  2. package/dist/index.mjs +192 -70
  3. package/dist/index.mjs.map +1 -1
  4. package/dist-cjs/index.js +192 -70
  5. package/dist-cjs/index.js.map +1 -1
  6. package/dist-types/context/durable-context/durable-context.d.ts +5 -2
  7. package/dist-types/context/durable-context/durable-context.d.ts.map +1 -1
  8. package/dist-types/context/durable-context/durable-context.test.d.ts +2 -0
  9. package/dist-types/context/durable-context/durable-context.test.d.ts.map +1 -0
  10. package/dist-types/durable-execution-api-client/durable-execution-api-client.d.ts.map +1 -1
  11. package/dist-types/handlers/concurrent-execution-handler/batch-result.d.ts.map +1 -1
  12. package/dist-types/handlers/concurrent-execution-handler/concurrent-execution-handler.d.ts.map +1 -1
  13. package/dist-types/handlers/invoke-handler/invoke-handler.d.ts.map +1 -1
  14. package/dist-types/index.d.ts +1 -1
  15. package/dist-types/index.d.ts.map +1 -1
  16. package/dist-types/types/durable-context.d.ts +9 -0
  17. package/dist-types/types/durable-context.d.ts.map +1 -1
  18. package/dist-types/types/invoke.d.ts +2 -0
  19. package/dist-types/types/invoke.d.ts.map +1 -1
  20. package/dist-types/types/step.d.ts +45 -0
  21. package/dist-types/types/step.d.ts.map +1 -1
  22. package/dist-types/utils/checkpoint/checkpoint-manager.d.ts +6 -1
  23. package/dist-types/utils/checkpoint/checkpoint-manager.d.ts.map +1 -1
  24. package/dist-types/utils/constants/constants.d.ts +12 -0
  25. package/dist-types/utils/constants/constants.d.ts.map +1 -1
  26. package/dist-types/utils/constants/version.d.ts +14 -0
  27. package/dist-types/utils/constants/version.d.ts.map +1 -0
  28. package/dist-types/utils/logger/default-logger.d.ts.map +1 -1
  29. package/dist-types/with-durable-execution.d.ts.map +1 -1
  30. package/package.json +1 -2
package/dist-cjs/index.js CHANGED
@@ -167,11 +167,56 @@ var DurableLogLevel;
167
167
  })(DurableLogLevel || (DurableLogLevel = {}));
168
168
 
169
169
  /**
170
+ * Execution semantics for step operations.
171
+ *
172
+ * @remarks
173
+ * These semantics control how step execution is checkpointed and replayed. **Important**: The guarantees apply *per
174
+ * retry attempt*, not per overall workflow execution.
175
+ *
176
+ * With retries enabled (the default), a step could execute multiple times across different retry attempts even when
177
+ * using `AtMostOncePerRetry`. To achieve step-level at-most-once execution, combine `AtMostOncePerRetry` with a retry
178
+ * strategy that disables retries (`shouldRetry: false`).
179
+ *
180
+ * @example
181
+ * ```typescript
182
+ * // At-least-once per retry (default) - safe for idempotent operations
183
+ * await context.step("send-notification", async () => sendEmail(), {
184
+ * semantics: StepSemantics.AtLeastOncePerRetry,
185
+ * });
186
+ *
187
+ * // At-most-once per retry - for non-idempotent operations
188
+ * await context.step("charge-payment", async () => processPayment(), {
189
+ * semantics: StepSemantics.AtMostOncePerRetry,
190
+ * retryStrategy: () => ({ shouldRetry: false }),
191
+ * });
192
+ * ```
193
+ *
170
194
  * @public
171
195
  */
172
196
  exports.StepSemantics = void 0;
173
197
  (function (StepSemantics) {
198
+ /**
199
+ * At-most-once execution per retry attempt.
200
+ *
201
+ * @remarks
202
+ * A checkpoint is created before step execution. If a failure occurs after the checkpoint
203
+ * but before step completion, the previous step retry attempt is skipped on replay.
204
+ *
205
+ * **Note**: This is "at-most-once *per retry*". With multiple retry attempts, the step
206
+ * could still execute multiple times across different retries. To guarantee the step
207
+ * executes at most once, disable retries by returning
208
+ * `{ shouldRetry: false }` from your retry strategy.
209
+ */
174
210
  StepSemantics["AtMostOncePerRetry"] = "AT_MOST_ONCE_PER_RETRY";
211
+ /**
212
+ * At-least-once execution per retry attempt (default).
213
+ *
214
+ * @remarks
215
+ * The step will execute at least once on each retry attempt. If the step succeeds
216
+ * but the checkpoint fails (e.g., due to a sandbox crash), the step will re-execute
217
+ * on replay. This is the safer default for operations that are idempotent or can
218
+ * tolerate duplicate execution.
219
+ */
175
220
  StepSemantics["AtLeastOncePerRetry"] = "AT_LEAST_ONCE_PER_RETRY";
176
221
  })(exports.StepSemantics || (exports.StepSemantics = {}));
177
222
  /**
@@ -569,6 +614,26 @@ class StepInterruptedError extends Error {
569
614
  }
570
615
  }
571
616
 
617
+ /**
618
+ * Shared constants to avoid circular dependencies
619
+ */
620
+ /**
621
+ * Controls whether stack traces are stored in error objects
622
+ * TODO: Accept this as configuration parameter in the future
623
+ */
624
+ /**
625
+ * Checkpoint manager termination cooldown in milliseconds
626
+ * After the last operation completes, the checkpoint manager waits this duration
627
+ * before terminating to allow for any final checkpoint operations
628
+ */
629
+ const CHECKPOINT_TERMINATION_COOLDOWN_MS = 20;
630
+ /**
631
+ * Maximum polling duration in milliseconds (15 minutes)
632
+ * Used to cap setTimeout delays to prevent 32-bit signed integer overflow
633
+ * and limit polling duration for long-running operations
634
+ */
635
+ const MAX_POLL_DURATION_MS = 15 * 60 * 1000;
636
+
572
637
  /**
573
638
  * Base class for all durable operation errors
574
639
  * @public
@@ -1357,6 +1422,7 @@ const createInvokeHandler = (context, checkpoint, createStepId, parentId, checkA
1357
1422
  Payload: serializedPayload,
1358
1423
  ChainedInvokeOptions: {
1359
1424
  FunctionName: funcId,
1425
+ ...(config?.tenantId && { TenantId: config.tenantId }),
1360
1426
  },
1361
1427
  });
1362
1428
  }
@@ -2487,6 +2553,10 @@ class BatchResultImpl {
2487
2553
  * Restores methods to deserialized BatchResult data
2488
2554
  */
2489
2555
  function restoreBatchResult(data) {
2556
+ // If data is already a BatchResultImpl instance, return it as-is
2557
+ if (data instanceof BatchResultImpl) {
2558
+ return data;
2559
+ }
2490
2560
  if (data &&
2491
2561
  typeof data === "object" &&
2492
2562
  "all" in data &&
@@ -2796,7 +2866,13 @@ class ConcurrencyController {
2796
2866
  tryStartNext();
2797
2867
  }
2798
2868
  };
2799
- tryStartNext();
2869
+ if (items.length === 0) {
2870
+ log("🎉", `${this.operationName} completed with no items`);
2871
+ resolve(new BatchResultImpl([], getCompletionReason(0)));
2872
+ }
2873
+ else {
2874
+ tryStartNext();
2875
+ }
2800
2876
  });
2801
2877
  }
2802
2878
  }
@@ -2948,7 +3024,7 @@ const getStepData = (stepData, stepId) => {
2948
3024
  };
2949
3025
 
2950
3026
  class DurableContextImpl {
2951
- executionContext;
3027
+ _executionContext;
2952
3028
  lambdaContext;
2953
3029
  _stepPrefix;
2954
3030
  _stepCounter = 0;
@@ -2960,8 +3036,9 @@ class DurableContextImpl {
2960
3036
  modeManagement;
2961
3037
  durableExecution;
2962
3038
  logger;
2963
- constructor(executionContext, lambdaContext, durableExecutionMode, inheritedLogger, stepPrefix, durableExecution, parentId) {
2964
- this.executionContext = executionContext;
3039
+ executionContext;
3040
+ constructor(_executionContext, lambdaContext, durableExecutionMode, inheritedLogger, stepPrefix, durableExecution, parentId) {
3041
+ this._executionContext = _executionContext;
2965
3042
  this.lambdaContext = lambdaContext;
2966
3043
  this._stepPrefix = stepPrefix;
2967
3044
  this._parentId = parentId;
@@ -2969,6 +3046,9 @@ class DurableContextImpl {
2969
3046
  this.durableLogger = inheritedLogger;
2970
3047
  this.durableLogger.configureDurableLoggingContext?.(this.getDurableLoggingContext());
2971
3048
  this.logger = this.createModeAwareLogger(inheritedLogger);
3049
+ this.executionContext = {
3050
+ durableExecutionArn: _executionContext.durableExecutionArn,
3051
+ };
2972
3052
  this.durableExecutionMode = durableExecutionMode;
2973
3053
  this.checkpoint = durableExecution.checkpointManager;
2974
3054
  this.modeManagement = new ModeManagement(this.captureExecutionState.bind(this), this.checkAndUpdateReplayMode.bind(this), this.checkForNonResolvingPromise.bind(this), () => this.durableExecutionMode, (mode) => {
@@ -2980,9 +3060,9 @@ class DurableContextImpl {
2980
3060
  getDurableLogData: () => {
2981
3061
  const activeContext = getActiveContext();
2982
3062
  const result = {
2983
- executionArn: this.executionContext.durableExecutionArn,
2984
- requestId: this.executionContext.requestId,
2985
- tenantId: this.executionContext.tenantId,
3063
+ executionArn: this._executionContext.durableExecutionArn,
3064
+ requestId: this._executionContext.requestId,
3065
+ tenantId: this._executionContext.tenantId,
2986
3066
  operationId: !activeContext || activeContext?.contextId === "root"
2987
3067
  ? undefined
2988
3068
  : hashId(activeContext.contextId),
@@ -3059,7 +3139,7 @@ class DurableContextImpl {
3059
3139
  checkAndUpdateReplayMode() {
3060
3140
  if (this.durableExecutionMode === DurableExecutionMode.ReplayMode) {
3061
3141
  const nextStepId = this.getNextStepId();
3062
- const nextStepData = this.executionContext.getStepData(nextStepId);
3142
+ const nextStepData = this._executionContext.getStepData(nextStepId);
3063
3143
  if (!nextStepData) {
3064
3144
  this.durableExecutionMode = DurableExecutionMode.ExecutionMode;
3065
3145
  }
@@ -3068,7 +3148,7 @@ class DurableContextImpl {
3068
3148
  captureExecutionState() {
3069
3149
  const wasInReplayMode = this.durableExecutionMode === DurableExecutionMode.ReplayMode;
3070
3150
  const nextStepId = this.getNextStepId();
3071
- const stepData = this.executionContext.getStepData(nextStepId);
3151
+ const stepData = this._executionContext.getStepData(nextStepId);
3072
3152
  const wasNotFinished = !!(stepData &&
3073
3153
  stepData.Status !== clientLambda.OperationStatus.SUCCEEDED &&
3074
3154
  stepData.Status !== clientLambda.OperationStatus.FAILED);
@@ -3077,7 +3157,7 @@ class DurableContextImpl {
3077
3157
  checkForNonResolvingPromise() {
3078
3158
  if (this.durableExecutionMode === DurableExecutionMode.ReplaySucceededContext) {
3079
3159
  const nextStepId = this.getNextStepId();
3080
- const nextStepData = this.executionContext.getStepData(nextStepId);
3160
+ const nextStepData = this._executionContext.getStepData(nextStepId);
3081
3161
  if (nextStepData &&
3082
3162
  nextStepData.Status !== clientLambda.OperationStatus.SUCCEEDED &&
3083
3163
  nextStepData.Status !== clientLambda.OperationStatus.FAILED) {
@@ -3093,16 +3173,16 @@ class DurableContextImpl {
3093
3173
  return this.modeManagement.withDurableModeManagement(operation);
3094
3174
  }
3095
3175
  step(nameOrFn, fnOrOptions, maybeOptions) {
3096
- validateContextUsage(this._stepPrefix, "step", this.executionContext.terminationManager);
3176
+ validateContextUsage(this._stepPrefix, "step", this._executionContext.terminationManager);
3097
3177
  return this.withDurableModeManagement(() => {
3098
- const stepHandler = createStepHandler(this.executionContext, this.checkpoint, this.lambdaContext, this.createStepId.bind(this), this.durableLogger, this._parentId);
3178
+ const stepHandler = createStepHandler(this._executionContext, this.checkpoint, this.lambdaContext, this.createStepId.bind(this), this.durableLogger, this._parentId);
3099
3179
  return stepHandler(nameOrFn, fnOrOptions, maybeOptions);
3100
3180
  });
3101
3181
  }
3102
3182
  invoke(nameOrFuncId, funcIdOrInput, inputOrConfig, maybeConfig) {
3103
- validateContextUsage(this._stepPrefix, "invoke", this.executionContext.terminationManager);
3183
+ validateContextUsage(this._stepPrefix, "invoke", this._executionContext.terminationManager);
3104
3184
  return this.withDurableModeManagement(() => {
3105
- const invokeHandler = createInvokeHandler(this.executionContext, this.checkpoint, this.createStepId.bind(this), this._parentId, this.checkAndUpdateReplayMode.bind(this));
3185
+ const invokeHandler = createInvokeHandler(this._executionContext, this.checkpoint, this.createStepId.bind(this), this._parentId, this.checkAndUpdateReplayMode.bind(this));
3106
3186
  return invokeHandler(...[
3107
3187
  nameOrFuncId,
3108
3188
  funcIdOrInput,
@@ -3112,18 +3192,18 @@ class DurableContextImpl {
3112
3192
  });
3113
3193
  }
3114
3194
  runInChildContext(nameOrFn, fnOrOptions, maybeOptions) {
3115
- validateContextUsage(this._stepPrefix, "runInChildContext", this.executionContext.terminationManager);
3195
+ validateContextUsage(this._stepPrefix, "runInChildContext", this._executionContext.terminationManager);
3116
3196
  return this.withDurableModeManagement(() => {
3117
- const blockHandler = createRunInChildContextHandler(this.executionContext, this.checkpoint, this.lambdaContext, this.createStepId.bind(this), () => this.durableLogger,
3197
+ const blockHandler = createRunInChildContextHandler(this._executionContext, this.checkpoint, this.lambdaContext, this.createStepId.bind(this), () => this.durableLogger,
3118
3198
  // Adapter function to maintain compatibility
3119
3199
  (executionContext, parentContext, durableExecutionMode, inheritedLogger, stepPrefix, _checkpointToken, parentId) => createDurableContext(executionContext, parentContext, durableExecutionMode, inheritedLogger, stepPrefix, this.durableExecution, parentId), this._parentId);
3120
3200
  return blockHandler(nameOrFn, fnOrOptions, maybeOptions);
3121
3201
  });
3122
3202
  }
3123
3203
  wait(nameOrDuration, maybeDuration) {
3124
- validateContextUsage(this._stepPrefix, "wait", this.executionContext.terminationManager);
3204
+ validateContextUsage(this._stepPrefix, "wait", this._executionContext.terminationManager);
3125
3205
  return this.withDurableModeManagement(() => {
3126
- const waitHandler = createWaitHandler(this.executionContext, this.checkpoint, this.createStepId.bind(this), this._parentId, this.checkAndUpdateReplayMode.bind(this));
3206
+ const waitHandler = createWaitHandler(this._executionContext, this.checkpoint, this.createStepId.bind(this), this._parentId, this.checkAndUpdateReplayMode.bind(this));
3127
3207
  return typeof nameOrDuration === "string"
3128
3208
  ? waitHandler(nameOrDuration, maybeDuration)
3129
3209
  : waitHandler(nameOrDuration);
@@ -3155,23 +3235,23 @@ class DurableContextImpl {
3155
3235
  }
3156
3236
  }
3157
3237
  createCallback(nameOrConfig, maybeConfig) {
3158
- validateContextUsage(this._stepPrefix, "createCallback", this.executionContext.terminationManager);
3238
+ validateContextUsage(this._stepPrefix, "createCallback", this._executionContext.terminationManager);
3159
3239
  return this.withDurableModeManagement(() => {
3160
- const callbackFactory = createCallback(this.executionContext, this.checkpoint, this.createStepId.bind(this), this.checkAndUpdateReplayMode.bind(this), this._parentId);
3240
+ const callbackFactory = createCallback(this._executionContext, this.checkpoint, this.createStepId.bind(this), this.checkAndUpdateReplayMode.bind(this), this._parentId);
3161
3241
  return callbackFactory(nameOrConfig, maybeConfig);
3162
3242
  });
3163
3243
  }
3164
3244
  waitForCallback(nameOrSubmitter, submitterOrConfig, maybeConfig) {
3165
- validateContextUsage(this._stepPrefix, "waitForCallback", this.executionContext.terminationManager);
3245
+ validateContextUsage(this._stepPrefix, "waitForCallback", this._executionContext.terminationManager);
3166
3246
  return this.withDurableModeManagement(() => {
3167
- const waitForCallbackHandler = createWaitForCallbackHandler(this.executionContext, this.getNextStepId.bind(this), this.runInChildContext.bind(this));
3247
+ const waitForCallbackHandler = createWaitForCallbackHandler(this._executionContext, this.getNextStepId.bind(this), this.runInChildContext.bind(this));
3168
3248
  return waitForCallbackHandler(nameOrSubmitter, submitterOrConfig, maybeConfig);
3169
3249
  });
3170
3250
  }
3171
3251
  waitForCondition(nameOrCheckFunc, checkFuncOrConfig, maybeConfig) {
3172
- validateContextUsage(this._stepPrefix, "waitForCondition", this.executionContext.terminationManager);
3252
+ validateContextUsage(this._stepPrefix, "waitForCondition", this._executionContext.terminationManager);
3173
3253
  return this.withDurableModeManagement(() => {
3174
- const waitForConditionHandler = createWaitForConditionHandler(this.executionContext, this.checkpoint, this.createStepId.bind(this), this.durableLogger, this._parentId);
3254
+ const waitForConditionHandler = createWaitForConditionHandler(this._executionContext, this.checkpoint, this.createStepId.bind(this), this.durableLogger, this._parentId);
3175
3255
  return typeof nameOrCheckFunc === "string" ||
3176
3256
  nameOrCheckFunc === undefined
3177
3257
  ? waitForConditionHandler(nameOrCheckFunc, checkFuncOrConfig, maybeConfig)
@@ -3179,23 +3259,23 @@ class DurableContextImpl {
3179
3259
  });
3180
3260
  }
3181
3261
  map(nameOrItems, itemsOrMapFunc, mapFuncOrConfig, maybeConfig) {
3182
- validateContextUsage(this._stepPrefix, "map", this.executionContext.terminationManager);
3262
+ validateContextUsage(this._stepPrefix, "map", this._executionContext.terminationManager);
3183
3263
  return this.withDurableModeManagement(() => {
3184
- const mapHandler = createMapHandler(this.executionContext, this._executeConcurrently.bind(this));
3264
+ const mapHandler = createMapHandler(this._executionContext, this._executeConcurrently.bind(this));
3185
3265
  return mapHandler(nameOrItems, itemsOrMapFunc, mapFuncOrConfig, maybeConfig);
3186
3266
  });
3187
3267
  }
3188
3268
  parallel(nameOrBranches, branchesOrConfig, maybeConfig) {
3189
- validateContextUsage(this._stepPrefix, "parallel", this.executionContext.terminationManager);
3269
+ validateContextUsage(this._stepPrefix, "parallel", this._executionContext.terminationManager);
3190
3270
  return this.withDurableModeManagement(() => {
3191
- const parallelHandler = createParallelHandler(this.executionContext, this._executeConcurrently.bind(this));
3271
+ const parallelHandler = createParallelHandler(this._executionContext, this._executeConcurrently.bind(this));
3192
3272
  return parallelHandler(nameOrBranches, branchesOrConfig, maybeConfig);
3193
3273
  });
3194
3274
  }
3195
3275
  _executeConcurrently(nameOrItems, itemsOrExecutor, executorOrConfig, maybeConfig) {
3196
- validateContextUsage(this._stepPrefix, "_executeConcurrently", this.executionContext.terminationManager);
3276
+ validateContextUsage(this._stepPrefix, "_executeConcurrently", this._executionContext.terminationManager);
3197
3277
  return this.withDurableModeManagement(() => {
3198
- const concurrentExecutionHandler = createConcurrentExecutionHandler(this.executionContext, this.runInChildContext.bind(this), this.skipNextOperation.bind(this));
3278
+ const concurrentExecutionHandler = createConcurrentExecutionHandler(this._executionContext, this.runInChildContext.bind(this), this.skipNextOperation.bind(this));
3199
3279
  const promise = concurrentExecutionHandler(nameOrItems, itemsOrExecutor, executorOrConfig, maybeConfig);
3200
3280
  // Prevent unhandled promise rejections
3201
3281
  promise?.catch(() => { });
@@ -3234,6 +3314,13 @@ class CheckpointUnrecoverableExecutionError extends UnrecoverableExecutionError
3234
3314
  }
3235
3315
 
3236
3316
  const STEP_DATA_UPDATED_EVENT = "stepDataUpdated";
3317
+ const TERMINAL_STATUSES = [
3318
+ clientLambda.OperationStatus.SUCCEEDED,
3319
+ clientLambda.OperationStatus.CANCELLED,
3320
+ clientLambda.OperationStatus.FAILED,
3321
+ clientLambda.OperationStatus.STOPPED,
3322
+ clientLambda.OperationStatus.TIMED_OUT,
3323
+ ];
3237
3324
  class CheckpointManager {
3238
3325
  durableExecutionArn;
3239
3326
  stepData;
@@ -3248,6 +3335,7 @@ class CheckpointManager {
3248
3335
  forceCheckpointPromises = [];
3249
3336
  queueCompletionResolver = null;
3250
3337
  MAX_PAYLOAD_SIZE = 750 * 1024; // 750KB in bytes
3338
+ MAX_ITEMS_IN_BATCH = 250;
3251
3339
  isTerminating = false;
3252
3340
  static textEncoder = new TextEncoder();
3253
3341
  // Operation lifecycle tracking
@@ -3255,7 +3343,6 @@ class CheckpointManager {
3255
3343
  // Termination cooldown
3256
3344
  terminationTimer = null;
3257
3345
  terminationReason = null;
3258
- TERMINATION_COOLDOWN_MS = 50;
3259
3346
  constructor(durableExecutionArn, stepData, storage, terminationManager, initialTaskToken, stepDataEmitter, logger, finishedAncestors) {
3260
3347
  this.durableExecutionArn = durableExecutionArn;
3261
3348
  this.stepData = stepData;
@@ -3409,7 +3496,9 @@ class CheckpointManager {
3409
3496
  while (this.queue.length > 0) {
3410
3497
  const nextItem = this.queue[0];
3411
3498
  const itemSize = CheckpointManager.textEncoder.encode(JSON.stringify(nextItem)).length;
3412
- if (currentSize + itemSize > this.MAX_PAYLOAD_SIZE && batch.length > 0) {
3499
+ if ((currentSize + itemSize > this.MAX_PAYLOAD_SIZE ||
3500
+ batch.length >= this.MAX_ITEMS_IN_BATCH) &&
3501
+ batch.length > 0) {
3413
3502
  break;
3414
3503
  }
3415
3504
  this.queue.shift();
@@ -3591,6 +3680,11 @@ class CheckpointManager {
3591
3680
  if (op.state !== OperationLifecycleState.RETRY_WAITING) {
3592
3681
  throw new Error(`Operation ${stepId} must be in RETRY_WAITING state, got ${op.state}`);
3593
3682
  }
3683
+ // Resolve immediately if the step was completed already
3684
+ const stepData = this.stepData[hashId(stepId)];
3685
+ if (stepData?.Status && TERMINAL_STATUSES.includes(stepData.Status)) {
3686
+ return Promise.resolve();
3687
+ }
3594
3688
  // Start timer with polling
3595
3689
  this.startTimerWithPolling(stepId, op.endTimestamp);
3596
3690
  // Return promise that resolves when status changes
@@ -3606,6 +3700,11 @@ class CheckpointManager {
3606
3700
  if (op.state !== OperationLifecycleState.IDLE_AWAITED) {
3607
3701
  throw new Error(`Operation ${stepId} must be in IDLE_AWAITED state, got ${op.state}`);
3608
3702
  }
3703
+ // Resolve immediately if the step was completed already
3704
+ const stepData = this.stepData[hashId(stepId)];
3705
+ if (stepData?.Status && TERMINAL_STATUSES.includes(stepData.Status)) {
3706
+ return Promise.resolve();
3707
+ }
3609
3708
  // Start timer with polling
3610
3709
  this.startTimerWithPolling(stepId, op.endTimestamp);
3611
3710
  // Return promise that resolves when status changes
@@ -3655,28 +3754,28 @@ class CheckpointManager {
3655
3754
  op.resolver = undefined;
3656
3755
  }
3657
3756
  }
3658
- checkAndTerminate() {
3757
+ /**
3758
+ * Determines if the function should terminate.
3759
+ * @returns TerminationReason if the function should terminate, or undefined if the function should not terminate
3760
+ */
3761
+ shouldTerminate() {
3659
3762
  // Rule 1: Can't terminate if checkpoint queue is not empty
3660
3763
  if (this.queue.length > 0) {
3661
- this.abortTermination();
3662
- return;
3764
+ return undefined;
3663
3765
  }
3664
3766
  // Rule 2: Can't terminate if checkpoint is currently processing
3665
3767
  if (this.isProcessing) {
3666
- this.abortTermination();
3667
- return;
3768
+ return undefined;
3668
3769
  }
3669
3770
  // Rule 3: Can't terminate if there are pending force checkpoint promises
3670
3771
  if (this.forceCheckpointPromises.length > 0) {
3671
- this.abortTermination();
3672
- return;
3772
+ return undefined;
3673
3773
  }
3674
3774
  const allOps = Array.from(this.operations.values());
3675
3775
  // Rule 4: Can't terminate if any operation is EXECUTING
3676
3776
  const hasExecuting = allOps.some((op) => op.state === OperationLifecycleState.EXECUTING);
3677
3777
  if (hasExecuting) {
3678
- this.abortTermination();
3679
- return;
3778
+ return undefined;
3680
3779
  }
3681
3780
  // Rule 5: Clean up operations whose ancestors are complete or pending completion
3682
3781
  for (const op of allOps) {
@@ -3699,12 +3798,17 @@ class CheckpointManager {
3699
3798
  op.state === OperationLifecycleState.IDLE_NOT_AWAITED ||
3700
3799
  op.state === OperationLifecycleState.IDLE_AWAITED);
3701
3800
  if (hasWaiting) {
3702
- const reason = this.determineTerminationReason(remainingOps);
3703
- this.scheduleTermination(reason);
3801
+ return this.determineTerminationReason(remainingOps);
3704
3802
  }
3705
- else {
3706
- this.abortTermination();
3803
+ return undefined;
3804
+ }
3805
+ checkAndTerminate() {
3806
+ const terminationReason = this.shouldTerminate();
3807
+ if (terminationReason) {
3808
+ this.scheduleTermination(terminationReason);
3809
+ return;
3707
3810
  }
3811
+ this.abortTermination();
3708
3812
  }
3709
3813
  abortTermination() {
3710
3814
  if (this.terminationTimer) {
@@ -3725,11 +3829,16 @@ class CheckpointManager {
3725
3829
  this.terminationReason = reason;
3726
3830
  log("⏱️", "Scheduling termination", {
3727
3831
  reason,
3728
- cooldownMs: this.TERMINATION_COOLDOWN_MS,
3832
+ cooldownMs: CHECKPOINT_TERMINATION_COOLDOWN_MS,
3729
3833
  });
3730
3834
  this.terminationTimer = setTimeout(() => {
3835
+ if (!this.shouldTerminate()) {
3836
+ log("🔄", "Termination conditions no longer valid after cooldown, aborting termination");
3837
+ this.abortTermination();
3838
+ return;
3839
+ }
3731
3840
  this.executeTermination(reason);
3732
- }, this.TERMINATION_COOLDOWN_MS);
3841
+ }, CHECKPOINT_TERMINATION_COOLDOWN_MS);
3733
3842
  }
3734
3843
  executeTermination(reason) {
3735
3844
  log("🛑", "Executing termination after cooldown", { reason });
@@ -3764,6 +3873,10 @@ class CheckpointManager {
3764
3873
  const timestamp = endTimestamp instanceof Date ? endTimestamp : new Date(endTimestamp);
3765
3874
  // Wait until endTimestamp
3766
3875
  delay = Math.max(0, timestamp.getTime() - Date.now());
3876
+ // Skip setTimeout if delay exceeds MAX_POLL_DURATION_MS (Lambda will timeout before it fires)
3877
+ if (delay > MAX_POLL_DURATION_MS) {
3878
+ return;
3879
+ }
3767
3880
  }
3768
3881
  else {
3769
3882
  // No timestamp, start polling immediately (1 second delay)
@@ -3783,7 +3896,6 @@ class CheckpointManager {
3783
3896
  if (!op)
3784
3897
  return;
3785
3898
  // Check if we've exceeded max polling duration (15 minutes)
3786
- const MAX_POLL_DURATION_MS = 15 * 60 * 1000; // 15 minutes
3787
3899
  if (op.pollStartTime &&
3788
3900
  Date.now() - op.pollStartTime > MAX_POLL_DURATION_MS) {
3789
3901
  // Stop polling after 15 minutes to prevent indefinite resource consumption.
@@ -3895,6 +4007,13 @@ class TerminationManager extends events.EventEmitter {
3895
4007
  // align the default behaviour of how logs are emitted to match the RIC logging behaviour for consistency.
3896
4008
  // For custom logic, users can implement their own logger to log data differently.
3897
4009
  // See: https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/962ed28eefbc052389c4de4366b1c0c49ee08a13/src/LogPatch.js
4010
+ /**
4011
+ * Format options for util.formatWithOptions.
4012
+ * Using breakLength: Infinity prevents util.inspect from inserting newlines
4013
+ * when formatting objects, regardless of object size (fixes issue #322).
4014
+ * Defined at module level to avoid creating a new object on every function call.
4015
+ */
4016
+ const FORMAT_OPTIONS = { breakLength: Infinity };
3898
4017
  /**
3899
4018
  * JSON.stringify replacer function for Error objects.
3900
4019
  * Based on AWS Lambda Runtime Interface Client LogPatch functionality.
@@ -3960,11 +4079,11 @@ function formatDurableLogData(level, logData, ...messageParams) {
3960
4079
  return JSON.stringify(result, jsonErrorReplacer);
3961
4080
  }
3962
4081
  catch (_) {
3963
- result.message = util.format(result.message);
4082
+ result.message = util.formatWithOptions(FORMAT_OPTIONS, result.message);
3964
4083
  return JSON.stringify(result);
3965
4084
  }
3966
4085
  }
3967
- result.message = util.format(...messageParams);
4086
+ result.message = util.formatWithOptions(FORMAT_OPTIONS, ...messageParams);
3968
4087
  for (const param of messageParams) {
3969
4088
  if (param instanceof Error) {
3970
4089
  result.errorType = param?.constructor?.name ?? "UnknownError";
@@ -4119,6 +4238,20 @@ const createDefaultLogger = (executionContext) => {
4119
4238
  return new DefaultLogger(executionContext);
4120
4239
  };
4121
4240
 
4241
+ /**
4242
+ * SDK metadata injected by Rollup at build time from package.json.
4243
+ *
4244
+ * At build time, Rollup replaces "@aws/durable-execution-sdk-js" and
4245
+ * "1.0.3" with actual values from package.json.
4246
+ *
4247
+ * Defaults are provided for test environments where Rollup doesn't run
4248
+ * and process.env values are undefined.
4249
+ *
4250
+ * @internal
4251
+ */
4252
+ const SDK_NAME = "@aws/durable-execution-sdk-js";
4253
+ const SDK_VERSION = "1.0.3";
4254
+
4122
4255
  let defaultLambdaClient;
4123
4256
  /**
4124
4257
  * Durable execution client which uses an API-based LambdaClient
@@ -4133,6 +4266,7 @@ class DurableExecutionApiClient {
4133
4266
  if (!client) {
4134
4267
  if (!defaultLambdaClient) {
4135
4268
  defaultLambdaClient = new clientLambda.LambdaClient({
4269
+ customUserAgent: [[SDK_NAME, SDK_VERSION]],
4136
4270
  requestHandler: {
4137
4271
  connectionTimeout: 5000,
4138
4272
  socketTimeout: 50000,
@@ -4331,8 +4465,9 @@ async function runHandler(event, context, executionContext, durableExecutionMode
4331
4465
  const durableContext = createDurableContext(executionContext, context, durableExecutionMode,
4332
4466
  // Default logger may not have the same type as Logger, but we should always provide a default logger even if the user overrides it
4333
4467
  createDefaultLogger(), undefined, durableExecution);
4334
- // Extract customerHandlerEvent from the original event
4335
- const initialExecutionEvent = event.InitialExecutionState.Operations?.[0];
4468
+ // Extract customerHandlerEvent from the complete operations array (after pagination)
4469
+ // This ensures we get the full payload even for large payloads that are paginated
4470
+ const initialExecutionEvent = executionContext._stepData[Object.keys(executionContext._stepData)[0]];
4336
4471
  const customerHandlerEvent = JSON.parse(initialExecutionEvent?.ExecutionDetails?.InputPayload ?? "{}");
4337
4472
  try {
4338
4473
  log("🎯", `Starting handler execution, handler event: ${customerHandlerEvent}`);
@@ -4479,16 +4614,10 @@ async function runHandler(event, context, executionContext, durableExecutionMode
4479
4614
  * Validates that the event is a proper durable execution input
4480
4615
  */
4481
4616
  function validateDurableExecutionEvent(event) {
4482
- try {
4483
- const eventObj = event;
4484
- if (!eventObj?.DurableExecutionArn || !eventObj?.CheckpointToken) {
4485
- throw new Error("Missing required durable execution fields");
4486
- }
4487
- }
4488
- catch {
4489
- const msg = `Unexpected payload provided to start the durable execution.
4490
- Check your resource configurations to confirm the durability is set.`;
4491
- throw new Error(msg);
4617
+ const eventObj = event;
4618
+ if (!eventObj?.DurableExecutionArn || !eventObj?.CheckpointToken) {
4619
+ throw new Error("Unexpected payload provided to start the durable execution.\n" +
4620
+ "Check your resource configurations to confirm the durability is set.");
4492
4621
  }
4493
4622
  }
4494
4623
  /**
@@ -4566,14 +4695,7 @@ const withDurableExecution = (handler, config) => {
4566
4695
  return async (event, context) => {
4567
4696
  validateDurableExecutionEvent(event);
4568
4697
  const { executionContext, durableExecutionMode, checkpointToken } = await initializeExecutionContext(event, context, config?.client);
4569
- let response = null;
4570
- try {
4571
- response = await runHandler(event, context, executionContext, durableExecutionMode, checkpointToken, handler);
4572
- return response;
4573
- }
4574
- catch (err) {
4575
- throw err;
4576
- }
4698
+ return runHandler(event, context, executionContext, durableExecutionMode, checkpointToken, handler);
4577
4699
  };
4578
4700
  };
4579
4701