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