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

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/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
  /**
@@ -2796,7 +2841,13 @@ class ConcurrencyController {
2796
2841
  tryStartNext();
2797
2842
  }
2798
2843
  };
2799
- tryStartNext();
2844
+ if (items.length === 0) {
2845
+ log("🎉", `${this.operationName} completed with no items`);
2846
+ resolve(new BatchResultImpl([], getCompletionReason(0)));
2847
+ }
2848
+ else {
2849
+ tryStartNext();
2850
+ }
2800
2851
  });
2801
2852
  }
2802
2853
  }
@@ -3234,6 +3285,13 @@ class CheckpointUnrecoverableExecutionError extends UnrecoverableExecutionError
3234
3285
  }
3235
3286
 
3236
3287
  const STEP_DATA_UPDATED_EVENT = "stepDataUpdated";
3288
+ const TERMINAL_STATUSES = [
3289
+ clientLambda.OperationStatus.SUCCEEDED,
3290
+ clientLambda.OperationStatus.CANCELLED,
3291
+ clientLambda.OperationStatus.FAILED,
3292
+ clientLambda.OperationStatus.STOPPED,
3293
+ clientLambda.OperationStatus.TIMED_OUT,
3294
+ ];
3237
3295
  class CheckpointManager {
3238
3296
  durableExecutionArn;
3239
3297
  stepData;
@@ -3248,6 +3306,7 @@ class CheckpointManager {
3248
3306
  forceCheckpointPromises = [];
3249
3307
  queueCompletionResolver = null;
3250
3308
  MAX_PAYLOAD_SIZE = 750 * 1024; // 750KB in bytes
3309
+ MAX_ITEMS_IN_BATCH = 250;
3251
3310
  isTerminating = false;
3252
3311
  static textEncoder = new TextEncoder();
3253
3312
  // Operation lifecycle tracking
@@ -3409,7 +3468,9 @@ class CheckpointManager {
3409
3468
  while (this.queue.length > 0) {
3410
3469
  const nextItem = this.queue[0];
3411
3470
  const itemSize = CheckpointManager.textEncoder.encode(JSON.stringify(nextItem)).length;
3412
- if (currentSize + itemSize > this.MAX_PAYLOAD_SIZE && batch.length > 0) {
3471
+ if ((currentSize + itemSize > this.MAX_PAYLOAD_SIZE ||
3472
+ batch.length >= this.MAX_ITEMS_IN_BATCH) &&
3473
+ batch.length > 0) {
3413
3474
  break;
3414
3475
  }
3415
3476
  this.queue.shift();
@@ -3591,6 +3652,11 @@ class CheckpointManager {
3591
3652
  if (op.state !== OperationLifecycleState.RETRY_WAITING) {
3592
3653
  throw new Error(`Operation ${stepId} must be in RETRY_WAITING state, got ${op.state}`);
3593
3654
  }
3655
+ // Resolve immediately if the step was completed already
3656
+ const stepData = this.stepData[hashId(stepId)];
3657
+ if (stepData?.Status && TERMINAL_STATUSES.includes(stepData.Status)) {
3658
+ return Promise.resolve();
3659
+ }
3594
3660
  // Start timer with polling
3595
3661
  this.startTimerWithPolling(stepId, op.endTimestamp);
3596
3662
  // Return promise that resolves when status changes
@@ -3606,6 +3672,11 @@ class CheckpointManager {
3606
3672
  if (op.state !== OperationLifecycleState.IDLE_AWAITED) {
3607
3673
  throw new Error(`Operation ${stepId} must be in IDLE_AWAITED state, got ${op.state}`);
3608
3674
  }
3675
+ // Resolve immediately if the step was completed already
3676
+ const stepData = this.stepData[hashId(stepId)];
3677
+ if (stepData?.Status && TERMINAL_STATUSES.includes(stepData.Status)) {
3678
+ return Promise.resolve();
3679
+ }
3609
3680
  // Start timer with polling
3610
3681
  this.startTimerWithPolling(stepId, op.endTimestamp);
3611
3682
  // Return promise that resolves when status changes
@@ -3655,28 +3726,28 @@ class CheckpointManager {
3655
3726
  op.resolver = undefined;
3656
3727
  }
3657
3728
  }
3658
- checkAndTerminate() {
3729
+ /**
3730
+ * Determines if the function should terminate.
3731
+ * @returns TerminationReason if the function should terminate, or undefined if the function should not terminate
3732
+ */
3733
+ shouldTerminate() {
3659
3734
  // Rule 1: Can't terminate if checkpoint queue is not empty
3660
3735
  if (this.queue.length > 0) {
3661
- this.abortTermination();
3662
- return;
3736
+ return undefined;
3663
3737
  }
3664
3738
  // Rule 2: Can't terminate if checkpoint is currently processing
3665
3739
  if (this.isProcessing) {
3666
- this.abortTermination();
3667
- return;
3740
+ return undefined;
3668
3741
  }
3669
3742
  // Rule 3: Can't terminate if there are pending force checkpoint promises
3670
3743
  if (this.forceCheckpointPromises.length > 0) {
3671
- this.abortTermination();
3672
- return;
3744
+ return undefined;
3673
3745
  }
3674
3746
  const allOps = Array.from(this.operations.values());
3675
3747
  // Rule 4: Can't terminate if any operation is EXECUTING
3676
3748
  const hasExecuting = allOps.some((op) => op.state === OperationLifecycleState.EXECUTING);
3677
3749
  if (hasExecuting) {
3678
- this.abortTermination();
3679
- return;
3750
+ return undefined;
3680
3751
  }
3681
3752
  // Rule 5: Clean up operations whose ancestors are complete or pending completion
3682
3753
  for (const op of allOps) {
@@ -3699,12 +3770,17 @@ class CheckpointManager {
3699
3770
  op.state === OperationLifecycleState.IDLE_NOT_AWAITED ||
3700
3771
  op.state === OperationLifecycleState.IDLE_AWAITED);
3701
3772
  if (hasWaiting) {
3702
- const reason = this.determineTerminationReason(remainingOps);
3703
- this.scheduleTermination(reason);
3773
+ return this.determineTerminationReason(remainingOps);
3704
3774
  }
3705
- else {
3706
- this.abortTermination();
3775
+ return undefined;
3776
+ }
3777
+ checkAndTerminate() {
3778
+ const terminationReason = this.shouldTerminate();
3779
+ if (terminationReason) {
3780
+ this.scheduleTermination(terminationReason);
3781
+ return;
3707
3782
  }
3783
+ this.abortTermination();
3708
3784
  }
3709
3785
  abortTermination() {
3710
3786
  if (this.terminationTimer) {
@@ -3728,6 +3804,11 @@ class CheckpointManager {
3728
3804
  cooldownMs: this.TERMINATION_COOLDOWN_MS,
3729
3805
  });
3730
3806
  this.terminationTimer = setTimeout(() => {
3807
+ if (!this.shouldTerminate()) {
3808
+ log("🔄", "Termination conditions no longer valid after cooldown, aborting termination");
3809
+ this.abortTermination();
3810
+ return;
3811
+ }
3731
3812
  this.executeTermination(reason);
3732
3813
  }, this.TERMINATION_COOLDOWN_MS);
3733
3814
  }
@@ -4479,16 +4560,10 @@ async function runHandler(event, context, executionContext, durableExecutionMode
4479
4560
  * Validates that the event is a proper durable execution input
4480
4561
  */
4481
4562
  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);
4563
+ const eventObj = event;
4564
+ if (!eventObj?.DurableExecutionArn || !eventObj?.CheckpointToken) {
4565
+ throw new Error("Unexpected payload provided to start the durable execution.\n" +
4566
+ "Check your resource configurations to confirm the durability is set.");
4492
4567
  }
4493
4568
  }
4494
4569
  /**
@@ -4566,14 +4641,7 @@ const withDurableExecution = (handler, config) => {
4566
4641
  return async (event, context) => {
4567
4642
  validateDurableExecutionEvent(event);
4568
4643
  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
- }
4644
+ return runHandler(event, context, executionContext, durableExecutionMode, checkpointToken, handler);
4577
4645
  };
4578
4646
  };
4579
4647