@aws/durable-execution-sdk-js 1.1.2 → 2.0.0-alpha.1

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 (63) hide show
  1. package/dist/index.mjs +669 -103
  2. package/dist/index.mjs.map +1 -1
  3. package/dist-cjs/index.js +666 -102
  4. package/dist-cjs/index.js.map +1 -1
  5. package/dist-types/context/durable-context/durable-context.d.ts +5 -0
  6. package/dist-types/context/durable-context/durable-context.d.ts.map +1 -1
  7. package/dist-types/errors/durable-error/durable-error.d.ts +24 -0
  8. package/dist-types/errors/durable-error/durable-error.d.ts.map +1 -1
  9. package/dist-types/errors/non-retryable-errors.d.ts +5 -0
  10. package/dist-types/errors/non-retryable-errors.d.ts.map +1 -0
  11. package/dist-types/handlers/callback-handler/callback.d.ts +2 -2
  12. package/dist-types/handlers/callback-handler/callback.d.ts.map +1 -1
  13. package/dist-types/handlers/concurrent-execution-handler/concurrent-execution-handler.d.ts +4 -2
  14. package/dist-types/handlers/concurrent-execution-handler/concurrent-execution-handler.d.ts.map +1 -1
  15. package/dist-types/handlers/invoke-handler/invoke-handler.d.ts +2 -1
  16. package/dist-types/handlers/invoke-handler/invoke-handler.d.ts.map +1 -1
  17. package/dist-types/handlers/map-handler/map-handler.d.ts.map +1 -1
  18. package/dist-types/handlers/parallel-handler/parallel-handler.d.ts.map +1 -1
  19. package/dist-types/handlers/promise-handler/promise-handler.d.ts +1 -1
  20. package/dist-types/handlers/promise-handler/promise-handler.d.ts.map +1 -1
  21. package/dist-types/handlers/run-in-child-context-handler/run-in-child-context-handler.d.ts +4 -3
  22. package/dist-types/handlers/run-in-child-context-handler/run-in-child-context-handler.d.ts.map +1 -1
  23. package/dist-types/handlers/step-handler/step-handler.d.ts +2 -1
  24. package/dist-types/handlers/step-handler/step-handler.d.ts.map +1 -1
  25. package/dist-types/handlers/wait-for-callback-handler/wait-for-callback-handler.d.ts +2 -1
  26. package/dist-types/handlers/wait-for-callback-handler/wait-for-callback-handler.d.ts.map +1 -1
  27. package/dist-types/handlers/wait-for-condition-handler/wait-for-condition-handler.d.ts +2 -1
  28. package/dist-types/handlers/wait-for-condition-handler/wait-for-condition-handler.d.ts.map +1 -1
  29. package/dist-types/index.d.ts +7 -3
  30. package/dist-types/index.d.ts.map +1 -1
  31. package/dist-types/testing/test-constants.d.ts +6 -0
  32. package/dist-types/testing/test-constants.d.ts.map +1 -1
  33. package/dist-types/types/batch.d.ts +50 -0
  34. package/dist-types/types/batch.d.ts.map +1 -1
  35. package/dist-types/types/child-context.d.ts +5 -0
  36. package/dist-types/types/child-context.d.ts.map +1 -1
  37. package/dist-types/types/durable-context.d.ts +23 -0
  38. package/dist-types/types/durable-context.d.ts.map +1 -1
  39. package/dist-types/utils/checkpoint/checkpoint-manager.d.ts.map +1 -1
  40. package/dist-types/utils/constants/constants.d.ts +6 -0
  41. package/dist-types/utils/constants/constants.d.ts.map +1 -1
  42. package/dist-types/utils/constants/version.d.ts +1 -12
  43. package/dist-types/utils/constants/version.d.ts.map +1 -1
  44. package/dist-types/utils/retry/linear-retry-strategy/linear-retry-strategy.d.ts +10 -0
  45. package/dist-types/utils/retry/linear-retry-strategy/linear-retry-strategy.d.ts.map +1 -0
  46. package/dist-types/utils/retry/linear-retry-strategy/linear-retry-strategy.test.d.ts +2 -0
  47. package/dist-types/utils/retry/linear-retry-strategy/linear-retry-strategy.test.d.ts.map +1 -0
  48. package/dist-types/utils/retry/retry-presets/retry-presets.d.ts +13 -1
  49. package/dist-types/utils/retry/retry-presets/retry-presets.d.ts.map +1 -1
  50. package/dist-types/utils/serdes/filesystem-serdes.d.ts +119 -0
  51. package/dist-types/utils/serdes/filesystem-serdes.d.ts.map +1 -0
  52. package/dist-types/utils/serdes/filesystem-serdes.test.d.ts +2 -0
  53. package/dist-types/utils/serdes/filesystem-serdes.test.d.ts.map +1 -0
  54. package/dist-types/utils/serdes/preview.d.ts +96 -0
  55. package/dist-types/utils/serdes/preview.d.ts.map +1 -0
  56. package/dist-types/utils/serdes/serdes.d.ts +29 -0
  57. package/dist-types/utils/serdes/serdes.d.ts.map +1 -1
  58. package/dist-types/utils/with-retry/index.d.ts +188 -0
  59. package/dist-types/utils/with-retry/index.d.ts.map +1 -0
  60. package/dist-types/utils/with-retry/index.test.d.ts +2 -0
  61. package/dist-types/utils/with-retry/index.test.d.ts.map +1 -0
  62. package/dist-types/with-durable-execution.d.ts.map +1 -1
  63. package/package.json +3 -3
package/dist/index.mjs CHANGED
@@ -4,7 +4,17 @@ import { AsyncLocalStorage } from 'async_hooks';
4
4
  import { createHash } from 'crypto';
5
5
  import { Console } from 'node:console';
6
6
  import util from 'node:util';
7
+ import { readFile, mkdir, writeFile } from 'node:fs/promises';
8
+ import { join } from 'node:path';
7
9
 
10
+
11
+ // -- Shims --
12
+ import cjsUrl from 'node:url';
13
+ import cjsPath from 'node:path';
14
+ import cjsModule from 'node:module';
15
+ const __filename = cjsUrl.fileURLToPath(import.meta.url);
16
+ const __dirname = cjsPath.dirname(__filename);
17
+ const require = cjsModule.createRequire(import.meta.url);
8
18
  /**
9
19
  * @internal
10
20
  */
@@ -233,6 +243,35 @@ var JitterStrategy;
233
243
  JitterStrategy["HALF"] = "HALF";
234
244
  })(JitterStrategy || (JitterStrategy = {}));
235
245
 
246
+ /**
247
+ * Nesting type for batch operations (map and parallel)
248
+ *
249
+ * Controls how child contexts are created for each branch/iteration, affecting
250
+ * observability, cost, and scale limits.
251
+ *
252
+ * @public
253
+ */
254
+ var NestingType;
255
+ (function (NestingType) {
256
+ /**
257
+ * Create CONTEXT operations for each branch/iteration with full checkpointing.
258
+ * Operations within each branch/iteration are wrapped in their own context.
259
+ *
260
+ * - **Observability**: High - each branch/iteration appears as separate operation in execution history
261
+ * - **Cost**: Higher - consumes more operations due to CONTEXT creation overhead
262
+ * - **Scale**: Lower maximum iterations due to operation limits
263
+ */
264
+ NestingType["NESTED"] = "NESTED";
265
+ /**
266
+ * Skip CONTEXT operations for branches/iterations using virtual contexts.
267
+ * Operations execute directly without individual context wrapping.
268
+ *
269
+ * - **Observability**: Lower - branches/iterations don't appear as separate operations
270
+ * - **Cost**: ~30% lower - reduces operation consumption by skipping CONTEXT overhead
271
+ * - **Scale**: Higher maximum iterations possible within operation limits
272
+ */
273
+ NestingType["FLAT"] = "FLAT";
274
+ })(NestingType || (NestingType = {}));
236
275
  /**
237
276
  * The status of a batch item
238
277
  * @public
@@ -557,15 +596,39 @@ const createRetryStrategy = (config = {}) => {
557
596
  };
558
597
  };
559
598
 
599
+ /**
600
+ * Creates a linear backoff retry strategy
601
+ * @param maxAttempts - Maximum number of attempts (default: 6)
602
+ * @param initialDelay - Initial delay in seconds (default: 1)
603
+ * @param increment - Linear increment per attempt in seconds (default: 1)
604
+ * @returns Retry strategy with linear backoff
605
+ */
606
+ const createLinearRetryStrategy = (maxAttempts = 6, initialDelay = 1, increment = 1) => {
607
+ return (error, attemptsMade) => {
608
+ if (attemptsMade >= maxAttempts) {
609
+ return { shouldRetry: false };
610
+ }
611
+ return {
612
+ shouldRetry: true,
613
+ delay: { seconds: initialDelay + increment * (attemptsMade - 1) },
614
+ };
615
+ };
616
+ };
617
+
560
618
  /**
561
619
  * Pre-configured retry strategies for common use cases
562
620
  * @example
563
621
  * ```typescript
564
- * // Use default retry preset (3 attempts with exponential backoff)
622
+ * // Use default retry preset (6 attempts with exponential backoff)
565
623
  * await context.step('my-step', async () => {
566
624
  * return await someOperation();
567
625
  * }, { retryStrategy: retryPresets.default });
568
626
  *
627
+ * // Use linear retry preset (1s, 2s, 3s, 4s, 5s delays)
628
+ * await context.step('linear-step', async () => {
629
+ * return await someOperation();
630
+ * }, { retryStrategy: retryPresets.linear });
631
+ *
569
632
  * // Use no-retry preset (fail immediately on error)
570
633
  * await context.step('critical-step', async () => {
571
634
  * return await criticalOperation();
@@ -591,6 +654,13 @@ const retryPresets = {
591
654
  backoffRate: 2,
592
655
  jitter: JitterStrategy.FULL,
593
656
  }),
657
+ /**
658
+ * Linear retry strategy with fixed increment
659
+ * - 6 total attempts (1 initial + 5 retries)
660
+ * - Delays: 1s, 2s, 3s, 4s, 5s
661
+ * - Total max wait time: 15 seconds
662
+ */
663
+ linear: createLinearRetryStrategy(6, 1, 1),
594
664
  /**
595
665
  * No retry strategy - fails immediately on first error
596
666
  * - 1 total attempt (no retries)
@@ -631,6 +701,12 @@ const CHECKPOINT_TERMINATION_COOLDOWN_MS = 20;
631
701
  * and limit polling duration for long-running operations
632
702
  */
633
703
  const MAX_POLL_DURATION_MS = 15 * 60 * 1000;
704
+ /**
705
+ * Maximum checkpoint payload size in bytes (256KB).
706
+ * Payloads exceeding this limit trigger ReplayChildren mode in child contexts,
707
+ * and overflow-to-file behavior in FileSystemSerdes.
708
+ */
709
+ const CHECKPOINT_SIZE_LIMIT_BYTES = 256 * 1024;
634
710
 
635
711
  /**
636
712
  * Base class for all durable operation errors
@@ -659,6 +735,10 @@ class DurableOperationError extends Error {
659
735
  return new StepError(errorObject.ErrorMessage || "Step failed", cause, errorObject.ErrorData);
660
736
  case "CallbackError":
661
737
  return new CallbackError(errorObject.ErrorMessage || "Callback failed", cause, errorObject.ErrorData);
738
+ case "CallbackTimeoutError":
739
+ return new CallbackTimeoutError(errorObject.ErrorMessage || "Callback timed out", cause, errorObject.ErrorData);
740
+ case "CallbackSubmitterError":
741
+ return new CallbackSubmitterError(errorObject.ErrorMessage || "Callback submitter failed", cause, errorObject.ErrorData);
662
742
  case "InvokeError":
663
743
  return new InvokeError(errorObject.ErrorMessage || "Invoke failed", cause, errorObject.ErrorData);
664
744
  case "ChildContextError":
@@ -701,6 +781,26 @@ class CallbackError extends DurableOperationError {
701
781
  super(message || "Callback failed", cause, errorData);
702
782
  }
703
783
  }
784
+ /**
785
+ * Error thrown when a callback operation times out
786
+ * @public
787
+ */
788
+ class CallbackTimeoutError extends DurableOperationError {
789
+ errorType = "CallbackTimeoutError";
790
+ constructor(message, cause, errorData) {
791
+ super(message || "Callback timed out", cause, errorData);
792
+ }
793
+ }
794
+ /**
795
+ * Error thrown when a callback submitter fails
796
+ * @public
797
+ */
798
+ class CallbackSubmitterError extends DurableOperationError {
799
+ errorType = "CallbackSubmitterError";
800
+ constructor(message, cause, errorData) {
801
+ super(message || "Callback submitter failed", cause, errorData);
802
+ }
803
+ }
704
804
  /**
705
805
  * Error thrown when an invoke operation fails
706
806
  * @public
@@ -721,6 +821,16 @@ class ChildContextError extends DurableOperationError {
721
821
  super(message || "Child context failed", cause, errorData);
722
822
  }
723
823
  }
824
+ /**
825
+ * Error thrown when a promise combinator operation fails
826
+ * @public
827
+ */
828
+ class PromiseCombinatorError extends DurableOperationError {
829
+ errorType = "PromiseCombinatorError";
830
+ constructor(message, cause, errorData) {
831
+ super(message || "Promise combinator failed", cause, errorData);
832
+ }
833
+ }
724
834
  /**
725
835
  * Error thrown when a wait for condition operation fails
726
836
  * @public
@@ -1120,7 +1230,7 @@ const validateReplayConsistency = (stepId, currentOperation, checkpointData, con
1120
1230
  }
1121
1231
  };
1122
1232
 
1123
- const createStepHandler = (context, checkpoint, parentContext, createStepId, logger, parentId) => {
1233
+ const createStepHandler = (context, checkpoint, parentContext, createStepId, logger, parentId, getDefaultSerdes) => {
1124
1234
  return (nameOrFn, fnOrOptions, maybeOptions) => {
1125
1235
  let name;
1126
1236
  let fn;
@@ -1136,7 +1246,8 @@ const createStepHandler = (context, checkpoint, parentContext, createStepId, log
1136
1246
  }
1137
1247
  const stepId = createStepId();
1138
1248
  const semantics = options?.semantics || StepSemantics.AtLeastOncePerRetry;
1139
- const serdes = options?.serdes || defaultSerdes;
1249
+ const serdes = options?.serdes ||
1250
+ (getDefaultSerdes ? getDefaultSerdes() : defaultSerdes);
1140
1251
  // Phase 1: Execute step
1141
1252
  const phase1Promise = (async () => {
1142
1253
  let stepData = context.getStepData(stepId);
@@ -1351,7 +1462,7 @@ const createStepHandler = (context, checkpoint, parentContext, createStepId, log
1351
1462
  };
1352
1463
  };
1353
1464
 
1354
- const createInvokeHandler = (context, checkpoint, createStepId, parentId, checkAndUpdateReplayMode) => {
1465
+ const createInvokeHandler = (context, checkpoint, createStepId, parentId, checkAndUpdateReplayMode, getDefaultSerdes) => {
1355
1466
  function invokeHandler(nameOrFuncId, funcIdOrInput, inputOrConfig, maybeConfig) {
1356
1467
  const isNameFirst = typeof funcIdOrInput === "string";
1357
1468
  const name = isNameFirst ? nameOrFuncId : undefined;
@@ -1409,7 +1520,8 @@ const createInvokeHandler = (context, checkpoint, createStepId, parentId, checkA
1409
1520
  }
1410
1521
  // Start invoke if not already started
1411
1522
  if (!stepData) {
1412
- const serializedPayload = await safeSerialize(config?.payloadSerdes || defaultSerdes, input, stepId, name, context.terminationManager, context.durableExecutionArn);
1523
+ const serializedPayload = await safeSerialize(config?.payloadSerdes ||
1524
+ (getDefaultSerdes ? getDefaultSerdes() : defaultSerdes), input, stepId, name, context.terminationManager, context.durableExecutionArn);
1413
1525
  await checkpoint.checkpoint(stepId, {
1414
1526
  Id: stepId,
1415
1527
  ParentId: parentId,
@@ -1444,7 +1556,8 @@ const createInvokeHandler = (context, checkpoint, createStepId, parentId, checkA
1444
1556
  const stepData = context.getStepData(stepId);
1445
1557
  if (stepData?.Status === OperationStatus.SUCCEEDED) {
1446
1558
  const invokeDetails = stepData.ChainedInvokeDetails;
1447
- return await safeDeserialize(config?.resultSerdes || defaultSerdes, invokeDetails?.Result, stepId, name, context.terminationManager, context.durableExecutionArn);
1559
+ return await safeDeserialize(config?.resultSerdes ||
1560
+ (getDefaultSerdes ? getDefaultSerdes() : defaultSerdes), invokeDetails?.Result, stepId, name, context.terminationManager, context.durableExecutionArn);
1448
1561
  }
1449
1562
  // Handle failure
1450
1563
  const invokeDetails = stepData?.ChainedInvokeDetails;
@@ -1466,7 +1579,8 @@ const createInvokeHandler = (context, checkpoint, createStepId, parentId, checkA
1466
1579
  checkAndUpdateReplayMode?.();
1467
1580
  checkpoint.markOperationState(stepId, OperationLifecycleState.COMPLETED);
1468
1581
  const invokeDetails = stepData.ChainedInvokeDetails;
1469
- return await safeDeserialize(config?.resultSerdes || defaultSerdes, invokeDetails?.Result, stepId, name, context.terminationManager, context.durableExecutionArn);
1582
+ return await safeDeserialize(config?.resultSerdes ||
1583
+ (getDefaultSerdes ? getDefaultSerdes() : defaultSerdes), invokeDetails?.Result, stepId, name, context.terminationManager, context.durableExecutionArn);
1470
1584
  }
1471
1585
  // Handle failure
1472
1586
  log("❌", "Invoke failed:", { stepId, status: stepData?.Status });
@@ -1485,8 +1599,6 @@ const createInvokeHandler = (context, checkpoint, createStepId, parentId, checkA
1485
1599
  return invokeHandler;
1486
1600
  };
1487
1601
 
1488
- // Checkpoint size limit in bytes (256KB)
1489
- const CHECKPOINT_SIZE_LIMIT = 256 * 1024;
1490
1602
  const determineChildReplayMode = (context, stepId) => {
1491
1603
  const stepData = context.getStepData(stepId);
1492
1604
  if (!stepData) {
@@ -1502,7 +1614,7 @@ const determineChildReplayMode = (context, stepId) => {
1502
1614
  }
1503
1615
  return DurableExecutionMode.ExecutionMode;
1504
1616
  };
1505
- const createRunInChildContextHandler = (context, checkpoint, parentContext, createStepId, getParentLogger, createChildContext, parentId) => {
1617
+ const createRunInChildContextHandler = (context, checkpoint, parentContext, createStepId, getParentLogger, createChildContext, parentId, getDefaultSerdes) => {
1506
1618
  return (nameOrFn, fnOrOptions, maybeOptions) => {
1507
1619
  let name;
1508
1620
  let fn;
@@ -1540,10 +1652,10 @@ const createRunInChildContextHandler = (context, checkpoint, parentContext, crea
1540
1652
  currentStepData?.Status === OperationStatus.FAILED) {
1541
1653
  // Mark this run-in-child-context as finished to prevent descendant operations
1542
1654
  checkpoint.markAncestorFinished(entityId);
1543
- return handleCompletedChildContext(context, parentContext, entityId, name, fn, options, getParentLogger, createChildContext);
1655
+ return handleCompletedChildContext(context, parentContext, entityId, name, fn, options, getParentLogger, createChildContext, getDefaultSerdes);
1544
1656
  }
1545
1657
  // Execute if not completed
1546
- return executeChildContext(context, checkpoint, parentContext, entityId, name, fn, options, getParentLogger, createChildContext, parentId);
1658
+ return executeChildContext(context, checkpoint, parentContext, entityId, name, fn, options, getParentLogger, createChildContext, parentId, getDefaultSerdes);
1547
1659
  })()
1548
1660
  .then((result) => {
1549
1661
  phase1Result = result;
@@ -1561,14 +1673,19 @@ const createRunInChildContextHandler = (context, checkpoint, parentContext, crea
1561
1673
  });
1562
1674
  };
1563
1675
  };
1564
- const handleCompletedChildContext = async (context, parentContext, entityId, stepName, fn, options, getParentLogger, createChildContext) => {
1565
- const serdes = options?.serdes || defaultSerdes;
1676
+ const handleCompletedChildContext = async (context, parentContext, entityId, stepName, fn, options, getParentLogger, createChildContext, getDefaultSerdes) => {
1677
+ const serdes = options?.serdes || (getDefaultSerdes ? getDefaultSerdes() : defaultSerdes);
1678
+ const errorMapper = options?.errorMapper;
1566
1679
  const stepData = context.getStepData(entityId);
1567
1680
  const result = stepData?.ContextDetails?.Result;
1568
1681
  // Handle failed child context
1569
1682
  if (stepData?.Status === OperationStatus.FAILED) {
1570
1683
  if (stepData.ContextDetails?.Error) {
1571
1684
  const originalError = DurableOperationError.fromErrorObject(stepData.ContextDetails.Error);
1685
+ // Use errorMapper if provided, otherwise wrap in ChildContextError
1686
+ if (errorMapper) {
1687
+ throw errorMapper(originalError);
1688
+ }
1572
1689
  throw new ChildContextError(originalError.message, originalError);
1573
1690
  }
1574
1691
  else {
@@ -1587,10 +1704,12 @@ const handleCompletedChildContext = async (context, parentContext, entityId, ste
1587
1704
  });
1588
1705
  return await safeDeserialize(serdes, result, entityId, stepName, context.terminationManager, context.durableExecutionArn);
1589
1706
  };
1590
- const executeChildContext = async (context, checkpoint, parentContext, entityId, name, fn, options, getParentLogger, createChildContext, parentId) => {
1591
- const serdes = options?.serdes || defaultSerdes;
1592
- // Checkpoint at start if not already started (fire-and-forget for performance)
1593
- if (context.getStepData(entityId) === undefined) {
1707
+ const executeChildContext = async (context, checkpoint, parentContext, entityId, name, fn, options, getParentLogger, createChildContext, parentId, getDefaultSerdes) => {
1708
+ const serdes = options?.serdes || (getDefaultSerdes ? getDefaultSerdes() : defaultSerdes);
1709
+ const errorMapper = options?.errorMapper;
1710
+ const isVirtual = options?.virtualContext === true;
1711
+ // Checkpoint at start if not already started and not virtual (fire-and-forget for performance)
1712
+ if (!isVirtual && context.getStepData(entityId) === undefined) {
1594
1713
  const subType = options?.subType || OperationSubType.RUN_IN_CHILD_CONTEXT;
1595
1714
  checkpoint.checkpoint(entityId, {
1596
1715
  Id: entityId,
@@ -1602,8 +1721,14 @@ const executeChildContext = async (context, checkpoint, parentContext, entityId,
1602
1721
  });
1603
1722
  }
1604
1723
  const childReplayMode = determineChildReplayMode(context, entityId);
1605
- // Create a child context with the entity ID as prefix
1606
- const durableChildContext = createChildContext(context, parentContext, childReplayMode, getParentLogger(), entityId, undefined, entityId);
1724
+ // Create a child context with appropriate parentId and stepPrefix
1725
+ const durableChildContext = createChildContext(context, parentContext, childReplayMode, getParentLogger(), entityId, // stepPrefix: use entityId for unique step IDs
1726
+ undefined,
1727
+ // parentId: this parameter is used for checkpointing, and should point to
1728
+ // valid parentId tthat is already checkpointed.
1729
+ // If this runInChildContext is a virtual, then we will use the parentId (the ancestor)
1730
+ // But if this runInChildContext is a virtual, then it's entityId can be used
1731
+ isVirtual ? parentId : entityId);
1607
1732
  try {
1608
1733
  // Execute the child context function with context tracking
1609
1734
  const result = await runWithContext(entityId, parentId, () => fn(durableChildContext), undefined, childReplayMode);
@@ -1613,7 +1738,7 @@ const executeChildContext = async (context, checkpoint, parentContext, entityId,
1613
1738
  let payloadToCheckpoint = serializedResult;
1614
1739
  let replayChildren = false;
1615
1740
  if (serializedResult &&
1616
- Buffer.byteLength(serializedResult, "utf8") > CHECKPOINT_SIZE_LIMIT) {
1741
+ Buffer.byteLength(serializedResult, "utf8") > CHECKPOINT_SIZE_LIMIT_BYTES) {
1617
1742
  replayChildren = true;
1618
1743
  // Use summary generator if provided, otherwise use empty string
1619
1744
  if (options?.summaryGenerator) {
@@ -1626,50 +1751,63 @@ const executeChildContext = async (context, checkpoint, parentContext, entityId,
1626
1751
  entityId,
1627
1752
  name,
1628
1753
  payloadSize: Buffer.byteLength(serializedResult, "utf8"),
1629
- limit: CHECKPOINT_SIZE_LIMIT,
1754
+ limit: CHECKPOINT_SIZE_LIMIT_BYTES,
1755
+ });
1756
+ }
1757
+ // Mark this run-in-child-context as finished to prevent descendant operations (only for non-virtual)
1758
+ if (!isVirtual) {
1759
+ checkpoint.markAncestorFinished(entityId);
1760
+ const subType = options?.subType || OperationSubType.RUN_IN_CHILD_CONTEXT;
1761
+ checkpoint.checkpoint(entityId, {
1762
+ Id: entityId,
1763
+ ParentId: parentId,
1764
+ Action: OperationAction.SUCCEED,
1765
+ SubType: subType,
1766
+ Type: OperationType.CONTEXT,
1767
+ Payload: payloadToCheckpoint,
1768
+ ContextOptions: replayChildren ? { ReplayChildren: true } : undefined,
1769
+ Name: name,
1770
+ });
1771
+ log("✅", "Child context completed successfully:", {
1772
+ entityId,
1773
+ name,
1774
+ });
1775
+ }
1776
+ else {
1777
+ log("✅", "Virtual child context completed successfully:", {
1778
+ entityId,
1779
+ name,
1630
1780
  });
1631
1781
  }
1632
- // Mark this run-in-child-context as finished to prevent descendant operations
1633
- checkpoint.markAncestorFinished(entityId);
1634
- const subType = options?.subType || OperationSubType.RUN_IN_CHILD_CONTEXT;
1635
- checkpoint.checkpoint(entityId, {
1636
- Id: entityId,
1637
- ParentId: parentId,
1638
- Action: OperationAction.SUCCEED,
1639
- SubType: subType,
1640
- Type: OperationType.CONTEXT,
1641
- Payload: payloadToCheckpoint,
1642
- ContextOptions: replayChildren ? { ReplayChildren: true } : undefined,
1643
- Name: name,
1644
- });
1645
- log("✅", "Child context completed successfully:", {
1646
- entityId,
1647
- name,
1648
- });
1649
1782
  return result;
1650
1783
  }
1651
1784
  catch (error) {
1652
- log("❌", "Child context failed:", {
1785
+ log("❌", isVirtual ? "Virtual child context failed:" : "Child context failed:", {
1653
1786
  entityId,
1654
1787
  name,
1655
1788
  error,
1656
1789
  });
1657
- // Mark this run-in-child-context as finished to prevent descendant operations
1658
- checkpoint.markAncestorFinished(entityId);
1659
- // Always checkpoint failures
1660
- const subType = options?.subType || OperationSubType.RUN_IN_CHILD_CONTEXT;
1661
- checkpoint.checkpoint(entityId, {
1662
- Id: entityId,
1663
- ParentId: parentId,
1664
- Action: OperationAction.FAIL,
1665
- SubType: subType,
1666
- Type: OperationType.CONTEXT,
1667
- Error: createErrorObjectFromError(error),
1668
- Name: name,
1669
- });
1670
- // Reconstruct error from ErrorObject for deterministic behavior
1790
+ // Mark this run-in-child-context as finished and checkpoint failure (only for non-virtual)
1791
+ if (!isVirtual) {
1792
+ checkpoint.markAncestorFinished(entityId);
1793
+ const subType = options?.subType || OperationSubType.RUN_IN_CHILD_CONTEXT;
1794
+ checkpoint.checkpoint(entityId, {
1795
+ Id: entityId,
1796
+ ParentId: parentId,
1797
+ Action: OperationAction.FAIL,
1798
+ SubType: subType,
1799
+ Type: OperationType.CONTEXT,
1800
+ Error: createErrorObjectFromError(error),
1801
+ Name: name,
1802
+ });
1803
+ }
1804
+ // Always wrap in ChildContextError for consistent error handling
1671
1805
  const errorObject = createErrorObjectFromError(error);
1672
1806
  const reconstructedError = DurableOperationError.fromErrorObject(errorObject);
1807
+ // Use errorMapper if provided, otherwise wrap in ChildContextError
1808
+ if (errorMapper) {
1809
+ throw errorMapper(reconstructedError);
1810
+ }
1673
1811
  throw new ChildContextError(reconstructedError.message, reconstructedError);
1674
1812
  }
1675
1813
  };
@@ -1776,7 +1914,7 @@ const createWaitHandler = (context, checkpoint, createStepId, parentId, checkAnd
1776
1914
  return waitHandler;
1777
1915
  };
1778
1916
 
1779
- const createWaitForConditionHandler = (context, checkpoint, createStepId, logger, parentId) => {
1917
+ const createWaitForConditionHandler = (context, checkpoint, createStepId, logger, parentId, getDefaultSerdes) => {
1780
1918
  return (nameOrCheck, checkOrConfig, maybeConfig) => {
1781
1919
  let name;
1782
1920
  let check;
@@ -1794,7 +1932,7 @@ const createWaitForConditionHandler = (context, checkpoint, createStepId, logger
1794
1932
  throw new Error("waitForCondition requires config with waitStrategy and initialState");
1795
1933
  }
1796
1934
  const stepId = createStepId();
1797
- const serdes = config.serdes || defaultSerdes;
1935
+ const serdes = config.serdes || (getDefaultSerdes ? getDefaultSerdes() : defaultSerdes);
1798
1936
  const phase1Promise = (async () => {
1799
1937
  let stepData = context.getStepData(stepId);
1800
1938
  // Check if already completed
@@ -1999,7 +2137,7 @@ const createPassThroughSerdes = () => ({
1999
2137
  serialize: async (value) => value,
2000
2138
  deserialize: async (data) => data,
2001
2139
  });
2002
- const createCallback = (context, checkpoint, createStepId, checkAndUpdateReplayMode, parentId) => {
2140
+ const createCallback = (context, checkpoint, createStepId, checkAndUpdateReplayMode, parentId, getDefaultCallbackDeserializer) => {
2003
2141
  return (nameOrConfig, maybeConfig) => {
2004
2142
  let name;
2005
2143
  let config;
@@ -2011,7 +2149,10 @@ const createCallback = (context, checkpoint, createStepId, checkAndUpdateReplayM
2011
2149
  config = nameOrConfig;
2012
2150
  }
2013
2151
  const stepId = createStepId();
2014
- const serdes = config?.serdes || createPassThroughSerdes();
2152
+ const serdes = config?.serdes ||
2153
+ (getDefaultCallbackDeserializer
2154
+ ? getDefaultCallbackDeserializer()
2155
+ : createPassThroughSerdes());
2015
2156
  // Phase 1: Setup and checkpoint
2016
2157
  let isCompleted = false;
2017
2158
  const phase1Promise = (async () => {
@@ -2103,16 +2244,22 @@ const createCallback = (context, checkpoint, createStepId, checkAndUpdateReplayM
2103
2244
  const resolvedPromise = new DurablePromise(async () => deserializedResult);
2104
2245
  return [resolvedPromise, callbackData.CallbackId];
2105
2246
  }
2106
- // Handle failure
2247
+ // Handle failure or timeout
2107
2248
  const error = stepData?.CallbackDetails?.Error;
2249
+ const isTimeout = stepData?.Status === OperationStatus.TIMED_OUT;
2108
2250
  const callbackError = error
2109
2251
  ? (() => {
2110
2252
  const cause = new Error(error.ErrorMessage);
2111
2253
  cause.name = error.ErrorType || "Error";
2112
2254
  cause.stack = error.StackTrace?.join("\n");
2255
+ if (isTimeout) {
2256
+ return new CallbackTimeoutError(error.ErrorMessage || "Callback timed out", cause, error.ErrorData);
2257
+ }
2113
2258
  return new CallbackError(error.ErrorMessage || "Callback failed", cause, error.ErrorData);
2114
2259
  })()
2115
- : new CallbackError("Callback failed");
2260
+ : isTimeout
2261
+ ? new CallbackTimeoutError("Callback timed out")
2262
+ : new CallbackError("Callback failed");
2116
2263
  const rejectedPromise = new DurablePromise(async () => {
2117
2264
  throw callbackError;
2118
2265
  });
@@ -2132,7 +2279,7 @@ const createCallback = (context, checkpoint, createStepId, checkAndUpdateReplayM
2132
2279
  };
2133
2280
  };
2134
2281
 
2135
- const createWaitForCallbackHandler = (context, getNextStepId, runInChildContext) => {
2282
+ const createWaitForCallbackHandler = (context, getNextStepId, runInChildContext, getDefaultCallbackDeserializer) => {
2136
2283
  return (nameOrSubmitter, submitterOrConfig, maybeConfig) => {
2137
2284
  let name;
2138
2285
  let submitter;
@@ -2167,11 +2314,16 @@ const createWaitForCallbackHandler = (context, getNextStepId, runInChildContext)
2167
2314
  });
2168
2315
  // Use runInChildContext to ensure proper ID generation and isolation
2169
2316
  const childFunction = async (childCtx) => {
2170
- // Convert WaitForCallbackConfig to CreateCallbackConfig
2171
- const createCallbackConfig = config
2317
+ // Convert WaitForCallbackConfig to CreateCallbackConfig.
2318
+ // When a defaultCallbackDeserializer is configured, force passthrough serdes
2319
+ // on the inner createCallback so the raw string is preserved for phase 2.
2320
+ const createCallbackConfig = config || getDefaultCallbackDeserializer
2172
2321
  ? {
2173
- timeout: config.timeout,
2174
- heartbeatTimeout: config.heartbeatTimeout,
2322
+ timeout: config?.timeout,
2323
+ heartbeatTimeout: config?.heartbeatTimeout,
2324
+ ...(getDefaultCallbackDeserializer && {
2325
+ serdes: createPassThroughSerdes(),
2326
+ }),
2175
2327
  }
2176
2328
  : undefined;
2177
2329
  // Create callback and get the promise + callbackId
@@ -2209,6 +2361,26 @@ const createWaitForCallbackHandler = (context, getNextStepId, runInChildContext)
2209
2361
  return {
2210
2362
  result: await runInChildContext(name, childFunction, {
2211
2363
  subType: OperationSubType.WAIT_FOR_CALLBACK,
2364
+ // When a defaultCallbackDeserializer is configured, use passthrough serdes
2365
+ // so the raw callback string is preserved through the runInChildContext
2366
+ // round-trip and phase 2 can apply the deserializer exactly once.
2367
+ // Without this, defaultSerdes (JSON) would add an extra encode/decode layer.
2368
+ ...(getDefaultCallbackDeserializer && {
2369
+ serdes: createPassThroughSerdes(),
2370
+ }),
2371
+ errorMapper: (originalError) => {
2372
+ // Pass through callback errors directly (both timeout and failure)
2373
+ if (originalError.errorType === "CallbackTimeoutError" ||
2374
+ originalError.errorType === "CallbackError") {
2375
+ return originalError;
2376
+ }
2377
+ // Map step errors to CallbackSubmitterError
2378
+ if (originalError.errorType === "StepError") {
2379
+ return new CallbackSubmitterError(originalError.message, originalError);
2380
+ }
2381
+ // Wrap other errors in ChildContextError
2382
+ return new ChildContextError(originalError.message, originalError);
2383
+ },
2212
2384
  }),
2213
2385
  stepId,
2214
2386
  };
@@ -2220,7 +2392,10 @@ const createWaitForCallbackHandler = (context, getNextStepId, runInChildContext)
2220
2392
  return new DurablePromise(async () => {
2221
2393
  const { result, stepId } = await phase1Promise;
2222
2394
  // Always deserialize the result since it's a string
2223
- return (await safeDeserialize(config?.serdes ?? createPassThroughSerdes(), result, stepId, name, context.terminationManager, context.durableExecutionArn));
2395
+ return (await safeDeserialize(config?.serdes ??
2396
+ (getDefaultCallbackDeserializer
2397
+ ? getDefaultCallbackDeserializer()
2398
+ : createPassThroughSerdes()), result, stepId, name, context.terminationManager, context.durableExecutionArn));
2224
2399
  });
2225
2400
  };
2226
2401
  };
@@ -2304,6 +2479,7 @@ const createMapHandler = (context, executeConcurrently) => {
2304
2479
  completionConfig: config?.completionConfig,
2305
2480
  serdes: config?.serdes,
2306
2481
  itemSerdes: config?.itemSerdes,
2482
+ nesting: config?.nesting,
2307
2483
  });
2308
2484
  log("🗺️", "Map operation completed successfully:", {
2309
2485
  resultCount: result.totalCount,
@@ -2384,6 +2560,7 @@ const createParallelHandler = (context, executeConcurrently) => {
2384
2560
  completionConfig: config?.completionConfig,
2385
2561
  serdes: config?.serdes,
2386
2562
  itemSerdes: config?.itemSerdes,
2563
+ nesting: config?.nesting,
2387
2564
  });
2388
2565
  log("🔀", "Parallel operation completed successfully:", {
2389
2566
  resultCount: result.totalCount,
@@ -2445,13 +2622,7 @@ function createErrorAwareSerdes() {
2445
2622
  : undefined,
2446
2623
  };
2447
2624
  }
2448
- // No-retry strategy for promise combinators
2449
- const stepConfig = {
2450
- retryStrategy: () => ({
2451
- shouldRetry: false,
2452
- }),
2453
- };
2454
- const createPromiseHandler = (step) => {
2625
+ const createPromiseHandler = (runInChildContext) => {
2455
2626
  const parseParams = (nameOrPromises, maybePromises) => {
2456
2627
  if (typeof nameOrPromises === "string" || nameOrPromises === undefined) {
2457
2628
  return { name: nameOrPromises, promises: maybePromises };
@@ -2461,32 +2632,38 @@ const createPromiseHandler = (step) => {
2461
2632
  const all = (nameOrPromises, maybePromises) => {
2462
2633
  return new DurablePromise(async () => {
2463
2634
  const { name, promises } = parseParams(nameOrPromises, maybePromises);
2464
- // Wrap Promise.all execution in a step for persistence
2465
- return await step(name, () => Promise.all(promises), stepConfig);
2635
+ // Wrap Promise.all execution in a child context for persistence
2636
+ return await runInChildContext(name, () => Promise.all(promises), {
2637
+ errorMapper: (error) => new PromiseCombinatorError(error.message, error),
2638
+ });
2466
2639
  });
2467
2640
  };
2468
2641
  const allSettled = (nameOrPromises, maybePromises) => {
2469
2642
  return new DurablePromise(async () => {
2470
2643
  const { name, promises } = parseParams(nameOrPromises, maybePromises);
2471
- // Wrap Promise.allSettled execution in a step for persistence
2472
- return await step(name, () => Promise.allSettled(promises), {
2473
- ...stepConfig,
2644
+ // Wrap Promise.allSettled execution in a child context for persistence
2645
+ return await runInChildContext(name, () => Promise.allSettled(promises), {
2474
2646
  serdes: createErrorAwareSerdes(),
2647
+ errorMapper: (error) => new PromiseCombinatorError(error.message, error),
2475
2648
  });
2476
2649
  });
2477
2650
  };
2478
2651
  const any = (nameOrPromises, maybePromises) => {
2479
2652
  return new DurablePromise(async () => {
2480
2653
  const { name, promises } = parseParams(nameOrPromises, maybePromises);
2481
- // Wrap Promise.any execution in a step for persistence
2482
- return await step(name, () => Promise.any(promises), stepConfig);
2654
+ // Wrap Promise.any execution in a child context for persistence
2655
+ return await runInChildContext(name, () => Promise.any(promises), {
2656
+ errorMapper: (error) => new PromiseCombinatorError(error.message, error),
2657
+ });
2483
2658
  });
2484
2659
  };
2485
2660
  const race = (nameOrPromises, maybePromises) => {
2486
2661
  return new DurablePromise(async () => {
2487
2662
  const { name, promises } = parseParams(nameOrPromises, maybePromises);
2488
- // Wrap Promise.race execution in a step for persistence
2489
- return await step(name, () => Promise.race(promises), stepConfig);
2663
+ // Wrap Promise.race execution in a child context for persistence
2664
+ return await runInChildContext(name, () => Promise.race(promises), {
2665
+ errorMapper: (error) => new PromiseCombinatorError(error.message, error),
2666
+ });
2490
2667
  });
2491
2668
  };
2492
2669
  return {
@@ -2576,9 +2753,11 @@ function restoreBatchResult(data) {
2576
2753
  class ConcurrencyController {
2577
2754
  operationName;
2578
2755
  skipNextOperation;
2579
- constructor(operationName, skipNextOperation) {
2756
+ getDefaultSerdes;
2757
+ constructor(operationName, skipNextOperation, getDefaultSerdes) {
2580
2758
  this.operationName = operationName;
2581
2759
  this.skipNextOperation = skipNextOperation;
2760
+ this.getDefaultSerdes = getDefaultSerdes;
2582
2761
  }
2583
2762
  isChildEntityCompleted(executionContext, parentEntityId, completedCount) {
2584
2763
  const childEntityId = `${parentEntityId}-${completedCount + 1}`;
@@ -2636,7 +2815,8 @@ class ConcurrencyController {
2636
2815
  const summaryPayload = stepData?.ContextDetails?.Result;
2637
2816
  if (summaryPayload) {
2638
2817
  try {
2639
- const serdes = config.serdes || defaultSerdes;
2818
+ const serdes = config.serdes ||
2819
+ (this.getDefaultSerdes ? this.getDefaultSerdes() : defaultSerdes);
2640
2820
  const parsedSummary = await serdes.deserialize(summaryPayload, {
2641
2821
  entityId: entityId,
2642
2822
  durableExecutionArn: executionContext.durableExecutionArn,
@@ -2699,7 +2879,11 @@ class ConcurrencyController {
2699
2879
  continue;
2700
2880
  }
2701
2881
  try {
2702
- const result = await parentContext.runInChildContext(item.name || item.id, (childContext) => executor(item, childContext), { subType: config.iterationSubType, serdes: config.itemSerdes });
2882
+ const result = await parentContext.runInChildContext(item.name || item.id, (childContext) => executor(item, childContext), {
2883
+ subType: config.iterationSubType,
2884
+ serdes: config.itemSerdes,
2885
+ virtualContext: config.nesting === NestingType.FLAT,
2886
+ });
2703
2887
  resultItems.push({
2704
2888
  result,
2705
2889
  index: item.index,
@@ -2804,7 +2988,11 @@ class ConcurrencyController {
2804
2988
  itemName: item.name,
2805
2989
  });
2806
2990
  parentContext
2807
- .runInChildContext(item.name || item.id, (childContext) => executor(item, childContext), { subType: config.iterationSubType, serdes: config.itemSerdes })
2991
+ .runInChildContext(item.name || item.id, (childContext) => executor(item, childContext), {
2992
+ subType: config.iterationSubType,
2993
+ serdes: config.itemSerdes,
2994
+ virtualContext: config.nesting === NestingType.FLAT,
2995
+ })
2808
2996
  .then((result) => {
2809
2997
  resultItems[index] = {
2810
2998
  result,
@@ -2874,7 +3062,7 @@ class ConcurrencyController {
2874
3062
  });
2875
3063
  }
2876
3064
  }
2877
- const createConcurrentExecutionHandler = (context, runInChildContext, skipNextOperation) => {
3065
+ const createConcurrentExecutionHandler = (context, runInChildContext, skipNextOperation, getDefaultSerdes) => {
2878
3066
  return (nameOrItems, itemsOrExecutor, executorOrConfig, maybeConfig) => {
2879
3067
  // Phase 1: Start execution immediately
2880
3068
  const phase1Promise = (async () => {
@@ -2910,7 +3098,7 @@ const createConcurrentExecutionHandler = (context, runInChildContext, skipNextOp
2910
3098
  throw new Error(`Invalid maxConcurrency: ${config.maxConcurrency}. Must be a positive number or undefined for unlimited concurrency.`);
2911
3099
  }
2912
3100
  const executeOperation = async (executionContext) => {
2913
- const concurrencyController = new ConcurrencyController("concurrent-execution", skipNextOperation);
3101
+ const concurrencyController = new ConcurrencyController("concurrent-execution", skipNextOperation, getDefaultSerdes);
2914
3102
  // Access durableExecutionMode from the context - it's set by runInChildContext
2915
3103
  // based on determineChildReplayMode logic
2916
3104
  const durableExecutionMode = executionContext.durableExecutionMode;
@@ -3033,6 +3221,9 @@ class DurableContextImpl {
3033
3221
  _parentId;
3034
3222
  modeManagement;
3035
3223
  durableExecution;
3224
+ _defaultSerdes = defaultSerdes;
3225
+ _defaultCallbackDeserializer = createPassThroughSerdes();
3226
+ _customCallbackDeserializerSet = false;
3036
3227
  logger;
3037
3228
  executionContext;
3038
3229
  constructor(_executionContext, lambdaContext, durableExecutionMode, inheritedLogger, stepPrefix, durableExecution, parentId) {
@@ -3173,14 +3364,14 @@ class DurableContextImpl {
3173
3364
  step(nameOrFn, fnOrOptions, maybeOptions) {
3174
3365
  validateContextUsage(this._stepPrefix, "step", this._executionContext.terminationManager);
3175
3366
  return this.withDurableModeManagement(() => {
3176
- const stepHandler = createStepHandler(this._executionContext, this.checkpoint, this.lambdaContext, this.createStepId.bind(this), this.durableLogger, this._parentId);
3367
+ const stepHandler = createStepHandler(this._executionContext, this.checkpoint, this.lambdaContext, this.createStepId.bind(this), this.durableLogger, this._parentId, () => this._defaultSerdes);
3177
3368
  return stepHandler(nameOrFn, fnOrOptions, maybeOptions);
3178
3369
  });
3179
3370
  }
3180
3371
  invoke(nameOrFuncId, funcIdOrInput, inputOrConfig, maybeConfig) {
3181
3372
  validateContextUsage(this._stepPrefix, "invoke", this._executionContext.terminationManager);
3182
3373
  return this.withDurableModeManagement(() => {
3183
- const invokeHandler = createInvokeHandler(this._executionContext, this.checkpoint, this.createStepId.bind(this), this._parentId, this.checkAndUpdateReplayMode.bind(this));
3374
+ const invokeHandler = createInvokeHandler(this._executionContext, this.checkpoint, this.createStepId.bind(this), this._parentId, this.checkAndUpdateReplayMode.bind(this), () => this._defaultSerdes);
3184
3375
  return invokeHandler(...[
3185
3376
  nameOrFuncId,
3186
3377
  funcIdOrInput,
@@ -3194,7 +3385,17 @@ class DurableContextImpl {
3194
3385
  return this.withDurableModeManagement(() => {
3195
3386
  const blockHandler = createRunInChildContextHandler(this._executionContext, this.checkpoint, this.lambdaContext, this.createStepId.bind(this), () => this.durableLogger,
3196
3387
  // Adapter function to maintain compatibility
3197
- (executionContext, parentContext, durableExecutionMode, inheritedLogger, stepPrefix, _checkpointToken, parentId) => createDurableContext(executionContext, parentContext, durableExecutionMode, inheritedLogger, stepPrefix, this.durableExecution, parentId), this._parentId);
3388
+ (executionContext, parentContext, durableExecutionMode, inheritedLogger, stepPrefix, _checkpointToken, parentId) => {
3389
+ const childCtx = createDurableContext(executionContext, parentContext, durableExecutionMode, inheritedLogger, stepPrefix, this.durableExecution, parentId);
3390
+ // Propagate serdes config to child context
3391
+ childCtx.configureSerdes({
3392
+ defaultSerdes: this._defaultSerdes,
3393
+ ...(this._customCallbackDeserializerSet && {
3394
+ defaultCallbackDeserializer: this._defaultCallbackDeserializer,
3395
+ }),
3396
+ });
3397
+ return childCtx;
3398
+ }, this._parentId, () => this._defaultSerdes);
3198
3399
  return blockHandler(nameOrFn, fnOrOptions, maybeOptions);
3199
3400
  });
3200
3401
  }
@@ -3232,24 +3433,39 @@ class DurableContextImpl {
3232
3433
  this.modeAwareLoggingEnabled = config.modeAware;
3233
3434
  }
3234
3435
  }
3436
+ configureSerdes(config) {
3437
+ if (config.defaultSerdes !== undefined) {
3438
+ this._defaultSerdes = config.defaultSerdes;
3439
+ }
3440
+ if (config.defaultCallbackDeserializer !== undefined) {
3441
+ this._defaultCallbackDeserializer = config.defaultCallbackDeserializer;
3442
+ this._customCallbackDeserializerSet = true;
3443
+ }
3444
+ }
3235
3445
  createCallback(nameOrConfig, maybeConfig) {
3236
3446
  validateContextUsage(this._stepPrefix, "createCallback", this._executionContext.terminationManager);
3237
3447
  return this.withDurableModeManagement(() => {
3238
- const callbackFactory = createCallback(this._executionContext, this.checkpoint, this.createStepId.bind(this), this.checkAndUpdateReplayMode.bind(this), this._parentId);
3448
+ const callbackFactory = createCallback(this._executionContext, this.checkpoint, this.createStepId.bind(this), this.checkAndUpdateReplayMode.bind(this), this._parentId, () => this._defaultCallbackDeserializer);
3239
3449
  return callbackFactory(nameOrConfig, maybeConfig);
3240
3450
  });
3241
3451
  }
3242
3452
  waitForCallback(nameOrSubmitter, submitterOrConfig, maybeConfig) {
3243
3453
  validateContextUsage(this._stepPrefix, "waitForCallback", this._executionContext.terminationManager);
3244
3454
  return this.withDurableModeManagement(() => {
3245
- const waitForCallbackHandler = createWaitForCallbackHandler(this._executionContext, this.getNextStepId.bind(this), this.runInChildContext.bind(this));
3455
+ const waitForCallbackHandler = createWaitForCallbackHandler(this._executionContext, this.getNextStepId.bind(this), this.runInChildContext.bind(this),
3456
+ // Only pass the getter when the user has explicitly configured a custom
3457
+ // deserializer. The default is createPassThroughSerdes() which matches
3458
+ // the original behavior, so no injection is needed in that case.
3459
+ this._customCallbackDeserializerSet
3460
+ ? () => this._defaultCallbackDeserializer
3461
+ : undefined);
3246
3462
  return waitForCallbackHandler(nameOrSubmitter, submitterOrConfig, maybeConfig);
3247
3463
  });
3248
3464
  }
3249
3465
  waitForCondition(nameOrCheckFunc, checkFuncOrConfig, maybeConfig) {
3250
3466
  validateContextUsage(this._stepPrefix, "waitForCondition", this._executionContext.terminationManager);
3251
3467
  return this.withDurableModeManagement(() => {
3252
- const waitForConditionHandler = createWaitForConditionHandler(this._executionContext, this.checkpoint, this.createStepId.bind(this), this.durableLogger, this._parentId);
3468
+ const waitForConditionHandler = createWaitForConditionHandler(this._executionContext, this.checkpoint, this.createStepId.bind(this), this.durableLogger, this._parentId, () => this._defaultSerdes);
3253
3469
  return typeof nameOrCheckFunc === "string" ||
3254
3470
  nameOrCheckFunc === undefined
3255
3471
  ? waitForConditionHandler(nameOrCheckFunc, checkFuncOrConfig, maybeConfig)
@@ -3273,7 +3489,7 @@ class DurableContextImpl {
3273
3489
  _executeConcurrently(nameOrItems, itemsOrExecutor, executorOrConfig, maybeConfig) {
3274
3490
  validateContextUsage(this._stepPrefix, "_executeConcurrently", this._executionContext.terminationManager);
3275
3491
  return this.withDurableModeManagement(() => {
3276
- const concurrentExecutionHandler = createConcurrentExecutionHandler(this._executionContext, this.runInChildContext.bind(this), this.skipNextOperation.bind(this));
3492
+ const concurrentExecutionHandler = createConcurrentExecutionHandler(this._executionContext, this.runInChildContext.bind(this), this.skipNextOperation.bind(this), () => this._defaultSerdes);
3277
3493
  const promise = concurrentExecutionHandler(nameOrItems, itemsOrExecutor, executorOrConfig, maybeConfig);
3278
3494
  // Prevent unhandled promise rejections
3279
3495
  promise?.catch(() => { });
@@ -3281,7 +3497,7 @@ class DurableContextImpl {
3281
3497
  });
3282
3498
  }
3283
3499
  get promise() {
3284
- return createPromiseHandler(this.step.bind(this));
3500
+ return createPromiseHandler(this.runInChildContext.bind(this));
3285
3501
  }
3286
3502
  }
3287
3503
  const createDurableContext = (executionContext, parentContext, durableExecutionMode, inheritedLogger, stepPrefix, durableExecution, parentId) => {
@@ -3311,6 +3527,22 @@ class CheckpointUnrecoverableExecutionError extends UnrecoverableExecutionError
3311
3527
  }
3312
3528
  }
3313
3529
 
3530
+ // KMS errors from Lambda that indicate customer-caused key misconfiguration.
3531
+ // These arrive as 502 errors but are non-retryable.
3532
+ const NON_RETRYABLE_CUSTOMER_ERRORS = new Set([
3533
+ "KMSAccessDeniedException",
3534
+ "KMSDisabledException",
3535
+ "KMSInvalidStateException",
3536
+ "KMSNotFoundException",
3537
+ ]);
3538
+ /**
3539
+ * Returns true if the error is a non-retryable customer error (e.g., KMS key misconfiguration).
3540
+ */
3541
+ function isNonRetryableCustomerError(error) {
3542
+ const name = error?.name;
3543
+ return !!name && NON_RETRYABLE_CUSTOMER_ERRORS.has(name);
3544
+ }
3545
+
3314
3546
  const STEP_DATA_UPDATED_EVENT = "stepDataUpdated";
3315
3547
  const TERMINAL_STATUSES = [
3316
3548
  OperationStatus.SUCCEEDED,
@@ -3476,6 +3708,11 @@ class CheckpointManager {
3476
3708
  statusCode !== 429) {
3477
3709
  return new CheckpointUnrecoverableExecutionError(`Checkpoint failed: ${errorMessage}`, originalError);
3478
3710
  }
3711
+ // For example: KMS errors from Lambda arrive as 502 errors. These indicate customer-caused
3712
+ // KMS key misconfiguration and should not be retried — treat as execution error.
3713
+ if (isNonRetryableCustomerError(error)) {
3714
+ return new CheckpointUnrecoverableExecutionError(`Checkpoint failed: ${errorMessage}`, originalError);
3715
+ }
3479
3716
  return new CheckpointUnrecoverableInvocationError(`Checkpoint failed: ${errorMessage}`, originalError);
3480
3717
  }
3481
3718
  async processQueue() {
@@ -4243,17 +4480,27 @@ const createDefaultLogger = (executionContext) => {
4243
4480
 
4244
4481
  /**
4245
4482
  * SDK metadata injected by Rollup at build time from package.json.
4483
+ * These values are inserted into UserAgent headers.
4484
+ *
4485
+ * At build time, Rollup replaces "2.0.0-alpha.1"
4486
+ * with actual values from package.json.
4246
4487
  *
4247
- * At build time, Rollup replaces "@aws/durable-execution-sdk-js" and
4248
- * "1.1.2" with actual values from package.json.
4488
+ * SDK_NAME is a fixed string matching the cross-SDK convention:
4489
+ * aws-durable-execution-sdk-\{language\}
4490
+ * Alternate version if SDK path is within the Lambda bundled runtime.
4249
4491
  *
4250
4492
  * Defaults are provided for test environments where Rollup doesn't run
4251
4493
  * and process.env values are undefined.
4252
4494
  *
4253
4495
  * @internal
4254
4496
  */
4255
- const SDK_NAME = "@aws/durable-execution-sdk-js";
4256
- const SDK_VERSION = "1.1.2";
4497
+ const runtimeDir = process.env.LAMBDA_RUNTIME_DIR || "/var/runtime";
4498
+ const isRuntimeBundled = typeof __dirname !== "undefined" && __dirname.startsWith(runtimeDir);
4499
+ const SDK_NAME = "aws-durable-execution-sdk-js";
4500
+ const baseVersion = "2.0.0-alpha.1";
4501
+ const SDK_VERSION = isRuntimeBundled
4502
+ ? `${baseVersion}-bundled`
4503
+ : baseVersion;
4257
4504
 
4258
4505
  let defaultLambdaClient;
4259
4506
  /**
@@ -4697,11 +4944,293 @@ function validateDurableExecutionEvent(event) {
4697
4944
  const withDurableExecution = (handler, config) => {
4698
4945
  return async (event, context) => {
4699
4946
  validateDurableExecutionEvent(event);
4700
- const { executionContext, durableExecutionMode, checkpointToken } = await initializeExecutionContext(event, context, config?.client);
4701
- return runHandler(event, context, executionContext, durableExecutionMode, checkpointToken, handler);
4947
+ try {
4948
+ const { executionContext, durableExecutionMode, checkpointToken } = await initializeExecutionContext(event, context, config?.client);
4949
+ return await runHandler(event, context, executionContext, durableExecutionMode, checkpointToken, handler);
4950
+ }
4951
+ catch (error) {
4952
+ // Non-retryable customer errors (e.g., KMS key misconfiguration) should
4953
+ // fail the execution immediately rather than retrying the invocation.
4954
+ if (isNonRetryableCustomerError(error)) {
4955
+ return {
4956
+ Status: InvocationStatus.FAILED,
4957
+ Error: createErrorObjectFromError(error),
4958
+ };
4959
+ }
4960
+ throw error;
4961
+ }
4702
4962
  };
4703
4963
  };
4704
4964
 
4965
+ /**
4966
+ * Controls whether a preview field is matched by name anywhere in the object
4967
+ * tree, or by exact dot-notation path from the root.
4968
+ *
4969
+ * @public
4970
+ */
4971
+ var FieldMatchMode;
4972
+ (function (FieldMatchMode) {
4973
+ /** Match the field name at any depth in the object tree (default). */
4974
+ FieldMatchMode["ANYWHERE"] = "ANYWHERE";
4975
+ /**
4976
+ * Match by exact dot-notation path from root.
4977
+ * A single segment (e.g. `"email"`) matches only the root-level field.
4978
+ * A dotted path (e.g. `"user.email"`) matches that exact nested location.
4979
+ */
4980
+ FieldMatchMode["PATH"] = "PATH";
4981
+ })(FieldMatchMode || (FieldMatchMode = {}));
4982
+ /**
4983
+ * Controls which fields are included in the preview by default.
4984
+ *
4985
+ * @public
4986
+ */
4987
+ var PreviewMode;
4988
+ (function (PreviewMode) {
4989
+ /** Include all fields, then apply `exclude` and `mask` rules. */
4990
+ PreviewMode["INCLUDE_ALL"] = "INCLUDE_ALL";
4991
+ /** Exclude all fields, then apply `include` and `mask` rules. */
4992
+ PreviewMode["EXCLUDE_ALL"] = "EXCLUDE_ALL";
4993
+ })(PreviewMode || (PreviewMode = {}));
4994
+ /** Returns true if the field at `path` (dot-notation) matches the given PreviewField rule. */
4995
+ function fieldMatches(path, field) {
4996
+ const mode = field.match ?? FieldMatchMode.ANYWHERE;
4997
+ if (mode === FieldMatchMode.PATH) {
4998
+ return path === field.name;
4999
+ }
5000
+ return path.split(".").includes(field.name);
5001
+ }
5002
+ function isMatched(path, fields) {
5003
+ return fields?.some((f) => fieldMatches(path, f)) ?? false;
5004
+ }
5005
+ /**
5006
+ * Builds a preview object from `value` according to `config`.
5007
+ *
5008
+ * Traverses the object tree and collects fields based on the include/exclude/mask
5009
+ * rules in `config`. The result is a nested object mirroring the original structure,
5010
+ * capped at `config.maxPreviewBytes` (default 4096 bytes).
5011
+ *
5012
+ * Priority rules:
5013
+ * - `exclude` always wins — excluded fields are never shown, even if in `mask`
5014
+ * - `mask` implies visibility — masked fields are shown (with `maskString`) unless excluded
5015
+ *
5016
+ * Limitations:
5017
+ * - Field names containing dots are not supported (indistinguishable from path separators)
5018
+ * - Array structure is not preserved — fields from array elements are merged into a plain object
5019
+ * - When array elements have heterogeneous shapes at the same field path, later elements
5020
+ * overwrite earlier primitives in the preview (e.g. `[{ user: "arb" }, { user: { email: "x" } }]`
5021
+ * produces `{ user: { email: "x" } }` — `"arb"` is lost)
5022
+ *
5023
+ * @example
5024
+ * ```typescript
5025
+ * const preview = buildPreview(order, {
5026
+ * mode: PreviewMode.EXCLUDE_ALL,
5027
+ * include: [{ name: "id" }, { name: "status" }],
5028
+ * mask: [{ name: "email" }],
5029
+ * });
5030
+ * // { id: "order-123", status: "pending", user: { email: "***" } }
5031
+ * ```
5032
+ *
5033
+ * @public
5034
+ */
5035
+ function buildPreview(value, config) {
5036
+ if (value === null || typeof value !== "object")
5037
+ return undefined;
5038
+ const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
5039
+ const maskString = config.maskString ?? "***";
5040
+ const maxBytes = config.maxPreviewBytes ?? 4096;
5041
+ const pairs = [];
5042
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5043
+ function collect(obj, pathPrefix) {
5044
+ if (obj === null || typeof obj !== "object")
5045
+ return;
5046
+ if (Array.isArray(obj)) {
5047
+ for (const item of obj) {
5048
+ collect(item, pathPrefix);
5049
+ }
5050
+ return;
5051
+ }
5052
+ for (const key of Object.keys(obj)) {
5053
+ if (DANGEROUS_KEYS.has(key))
5054
+ continue;
5055
+ if (key.includes("."))
5056
+ continue;
5057
+ const path = pathPrefix ? `${pathPrefix}.${key}` : key;
5058
+ const masked = isMatched(path, config.mask);
5059
+ const excluded = isMatched(path, config.exclude);
5060
+ const visible = !excluded &&
5061
+ (masked ||
5062
+ (config.mode === PreviewMode.INCLUDE_ALL
5063
+ ? true
5064
+ : isMatched(path, config.include)));
5065
+ if (!visible) {
5066
+ collect(obj[key], path);
5067
+ continue;
5068
+ }
5069
+ if (masked) {
5070
+ pairs.push([path, maskString]);
5071
+ continue;
5072
+ }
5073
+ if (obj[key] !== null && typeof obj[key] === "object") {
5074
+ collect(obj[key], path);
5075
+ }
5076
+ else {
5077
+ pairs.push([path, obj[key]]);
5078
+ }
5079
+ }
5080
+ }
5081
+ collect(value, "");
5082
+ if (pairs.length === 0)
5083
+ return undefined;
5084
+ const accepted = [];
5085
+ let estimatedSize = 2; // "{}"
5086
+ for (const [path, val] of pairs) {
5087
+ const entrySize = Buffer.byteLength(`"${path}":${JSON.stringify(val)},`, "utf-8");
5088
+ if (estimatedSize + entrySize > maxBytes)
5089
+ break;
5090
+ accepted.push([path, val]);
5091
+ estimatedSize += entrySize;
5092
+ }
5093
+ if (accepted.length === 0)
5094
+ return undefined;
5095
+ const result = {};
5096
+ for (const [path, val] of accepted) {
5097
+ const parts = path.split(".");
5098
+ let node = result;
5099
+ for (let i = 0; i < parts.length - 1; i++) {
5100
+ if (typeof node[parts[i]] !== "object" || node[parts[i]] === null) {
5101
+ node[parts[i]] = {};
5102
+ }
5103
+ node = node[parts[i]];
5104
+ }
5105
+ node[parts[parts.length - 1]] = val;
5106
+ }
5107
+ return Object.keys(result).length > 0 ? result : undefined;
5108
+ }
5109
+
5110
+ // Subtract 1KB headroom for the envelope wrapper and other checkpoint metadata
5111
+ const OVERFLOW_THRESHOLD_BYTES = CHECKPOINT_SIZE_LIMIT_BYTES - 1024;
5112
+ /**
5113
+ * Controls when data is written to the filesystem.
5114
+ *
5115
+ * - `ALWAYS`: Every value is written to a file; the checkpoint stores only a file pointer.
5116
+ * Best for consistently large payloads or when you want predictable checkpoint sizes.
5117
+ *
5118
+ * - `OVERFLOW`: Data is written inline (as JSON) unless it exceeds the durable function
5119
+ * checkpoint size limit (~256KB), in which case it overflows to a file.
5120
+ * Best for mixed workloads where most payloads are small.
5121
+ *
5122
+ * @public
5123
+ */
5124
+ var FileSystemSerdesMode;
5125
+ (function (FileSystemSerdesMode) {
5126
+ FileSystemSerdesMode["ALWAYS"] = "ALWAYS";
5127
+ FileSystemSerdesMode["OVERFLOW"] = "OVERFLOW";
5128
+ })(FileSystemSerdesMode || (FileSystemSerdesMode = {}));
5129
+ async function writeToFile(basePath, value, context) {
5130
+ const dir = join(basePath, encodeURIComponent(context.durableExecutionArn));
5131
+ await mkdir(dir, { recursive: true });
5132
+ const filePath = join(dir, `${context.entityId}.json`);
5133
+ await writeFile(filePath, JSON.stringify(value), "utf-8");
5134
+ return filePath;
5135
+ }
5136
+ /**
5137
+ * Creates a Serdes that stores serialized values on a durable filesystem.
5138
+ *
5139
+ * **⚠️ WARNING: Do NOT use with Lambda's ephemeral `/tmp` storage.**
5140
+ * Lambda's `/tmp` filesystem is local to a single execution environment and is
5141
+ * not shared across invocations or function instances. On replay, a different
5142
+ * execution environment may be used and the file will not be found, causing
5143
+ * deserialization to fail.
5144
+ *
5145
+ * **Use only with a durable, shared filesystem such as:**
5146
+ * - **Amazon S3 Files** — mount an S3 bucket as a filesystem via the Lambda console or IaC
5147
+ * - **Amazon EFS** — mount an EFS file system to your Lambda function
5148
+ *
5149
+ * Both options provide persistence across invocations and are accessible from
5150
+ * multiple concurrent function instances, which is required for correct replay behavior.
5151
+ *
5152
+ * The checkpoint stores a JSON envelope that is either:
5153
+ * - `{"data":"<inline JSON>"}` — value stored inline (OVERFLOW mode, under threshold)
5154
+ * - `{"file":"<path>"}` — value stored in a file
5155
+ * - `{"file":"<path>","preview":{...}}` — file pointer with inline preview (when preview is configured)
5156
+ *
5157
+ * @param basePath - Directory path where data files will be stored (e.g. `/mnt/s3` for S3 Files, `/mnt/efs` for EFS)
5158
+ * @param config - Optional configuration options
5159
+ * @returns A Serdes that reads/writes JSON files under basePath
5160
+ *
5161
+ * @example
5162
+ * ```typescript
5163
+ * // Always write to S3 Files mount (default)
5164
+ * context.configureSerdes({
5165
+ * defaultSerdes: createFileSystemSerdes("/mnt/s3"),
5166
+ * });
5167
+ *
5168
+ * // Only overflow to filesystem when payload exceeds ~256KB
5169
+ * context.configureSerdes({
5170
+ * defaultSerdes: createFileSystemSerdes("/mnt/s3", { storageMode: FileSystemSerdesMode.OVERFLOW }),
5171
+ * });
5172
+ *
5173
+ * // With preview: show id and masked email in checkpoint
5174
+ * context.configureSerdes({
5175
+ * defaultSerdes: createFileSystemSerdes("/mnt/s3", {
5176
+ * generatePreview: (value) => buildPreview(value, {
5177
+ * mode: PreviewMode.EXCLUDE_ALL,
5178
+ * include: [{ name: "id" }, { name: "status" }],
5179
+ * mask: [{ name: "email" }],
5180
+ * }),
5181
+ * }),
5182
+ * });
5183
+ * ```
5184
+ *
5185
+ * Limitations:
5186
+ * - Field names containing dots are not supported in preview field selectors.
5187
+ * A dot in a field name is indistinguishable from a path separator.
5188
+ * - Array structure is not preserved in preview output — fields from array
5189
+ * elements are merged into a plain object at the array's path.
5190
+ *
5191
+ * @public
5192
+ */
5193
+ function createFileSystemSerdes(basePath, config = {}) {
5194
+ const storageMode = config.storageMode ?? FileSystemSerdesMode.ALWAYS;
5195
+ return {
5196
+ serialize: async (
5197
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5198
+ value, context) => {
5199
+ if (value === undefined)
5200
+ return undefined;
5201
+ if (storageMode === FileSystemSerdesMode.ALWAYS) {
5202
+ const filePath = await writeToFile(basePath, value, context);
5203
+ const preview = config.generatePreview?.(value);
5204
+ const envelope = preview
5205
+ ? { file: filePath, preview }
5206
+ : { file: filePath };
5207
+ return JSON.stringify(envelope);
5208
+ }
5209
+ // OVERFLOW mode: serialize inline first, overflow to file if too large
5210
+ const inlineJson = JSON.stringify(value);
5211
+ if (Buffer.byteLength(inlineJson, "utf-8") > OVERFLOW_THRESHOLD_BYTES) {
5212
+ const filePath = await writeToFile(basePath, value, context);
5213
+ const preview = config.generatePreview?.(value);
5214
+ const envelope = preview
5215
+ ? { file: filePath, preview }
5216
+ : { file: filePath };
5217
+ return JSON.stringify(envelope);
5218
+ }
5219
+ return JSON.stringify({ data: inlineJson });
5220
+ },
5221
+ deserialize: async (data, _context) => {
5222
+ if (data === undefined)
5223
+ return undefined;
5224
+ const envelope = JSON.parse(data);
5225
+ if ("file" in envelope) {
5226
+ const contents = await readFile(envelope.file, "utf-8");
5227
+ return JSON.parse(contents);
5228
+ }
5229
+ return JSON.parse(envelope.data);
5230
+ },
5231
+ };
5232
+ }
5233
+
4705
5234
  const DEFAULT_CONFIG = {
4706
5235
  maxAttempts: 60,
4707
5236
  initialDelay: { seconds: 5 },
@@ -4751,5 +5280,42 @@ const createWaitStrategy = (config) => {
4751
5280
  };
4752
5281
  };
4753
5282
 
4754
- export { BatchItemStatus, CallbackError, ChildContextError, DurableExecutionApiClient, DurableExecutionInvocationInputWithClient, DurableOperationError, DurablePromise, InvocationStatus, InvokeError, JitterStrategy, OperationSubType, StepError, StepInterruptedError, StepSemantics, WaitForConditionError, createClassSerdes, createClassSerdesWithDates, createRetryStrategy, createWaitStrategy, defaultSerdes, retryPresets, withDurableExecution };
5283
+ async function withRetry(context, nameOrFunc, funcOrConfig, maybeConfig) {
5284
+ const hasName = typeof nameOrFunc === "string";
5285
+ const name = hasName ? nameOrFunc : undefined;
5286
+ const func = (hasName ? funcOrConfig : nameOrFunc);
5287
+ const config = (hasName ? maybeConfig : funcOrConfig);
5288
+ if (!config) {
5289
+ throw new TypeError("withRetry: config is required");
5290
+ }
5291
+ const { retryStrategy, wrapWithRunInChildContext = true, childContextConfig, } = config;
5292
+ const runLoop = async (ctx) => {
5293
+ let attempt = 0;
5294
+ while (true) {
5295
+ attempt++;
5296
+ try {
5297
+ return await func(ctx, attempt);
5298
+ }
5299
+ catch (err) {
5300
+ const decision = retryStrategy(err, attempt);
5301
+ if (!decision.shouldRetry)
5302
+ throw err;
5303
+ const delay = decision.delay ?? { seconds: 1 };
5304
+ if (name) {
5305
+ await ctx.wait(`${name}-backoff-${attempt}`, delay);
5306
+ }
5307
+ else {
5308
+ await ctx.wait(delay);
5309
+ }
5310
+ }
5311
+ }
5312
+ };
5313
+ return wrapWithRunInChildContext
5314
+ ? name
5315
+ ? await context.runInChildContext(name, runLoop, childContextConfig)
5316
+ : await context.runInChildContext(runLoop, childContextConfig)
5317
+ : await runLoop(context);
5318
+ }
5319
+
5320
+ export { BatchItemStatus, CallbackError, CallbackSubmitterError, CallbackTimeoutError, ChildContextError, DurableExecutionApiClient, DurableExecutionInvocationInputWithClient, DurableOperationError, DurablePromise, FieldMatchMode, FileSystemSerdesMode, InvocationStatus, InvokeError, JitterStrategy, NestingType, OperationSubType, PreviewMode, StepError, StepInterruptedError, StepSemantics, WaitForConditionError, buildPreview, createClassSerdes, createClassSerdesWithDates, createFileSystemSerdes, createLinearRetryStrategy, createRetryStrategy, createWaitStrategy, defaultSerdes, retryPresets, withDurableExecution, withRetry };
4755
5321
  //# sourceMappingURL=index.mjs.map