@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.
- package/dist/index.mjs +669 -103
- package/dist/index.mjs.map +1 -1
- package/dist-cjs/index.js +666 -102
- package/dist-cjs/index.js.map +1 -1
- package/dist-types/context/durable-context/durable-context.d.ts +5 -0
- package/dist-types/context/durable-context/durable-context.d.ts.map +1 -1
- package/dist-types/errors/durable-error/durable-error.d.ts +24 -0
- package/dist-types/errors/durable-error/durable-error.d.ts.map +1 -1
- package/dist-types/errors/non-retryable-errors.d.ts +5 -0
- package/dist-types/errors/non-retryable-errors.d.ts.map +1 -0
- package/dist-types/handlers/callback-handler/callback.d.ts +2 -2
- package/dist-types/handlers/callback-handler/callback.d.ts.map +1 -1
- package/dist-types/handlers/concurrent-execution-handler/concurrent-execution-handler.d.ts +4 -2
- package/dist-types/handlers/concurrent-execution-handler/concurrent-execution-handler.d.ts.map +1 -1
- package/dist-types/handlers/invoke-handler/invoke-handler.d.ts +2 -1
- package/dist-types/handlers/invoke-handler/invoke-handler.d.ts.map +1 -1
- package/dist-types/handlers/map-handler/map-handler.d.ts.map +1 -1
- package/dist-types/handlers/parallel-handler/parallel-handler.d.ts.map +1 -1
- package/dist-types/handlers/promise-handler/promise-handler.d.ts +1 -1
- package/dist-types/handlers/promise-handler/promise-handler.d.ts.map +1 -1
- package/dist-types/handlers/run-in-child-context-handler/run-in-child-context-handler.d.ts +4 -3
- package/dist-types/handlers/run-in-child-context-handler/run-in-child-context-handler.d.ts.map +1 -1
- package/dist-types/handlers/step-handler/step-handler.d.ts +2 -1
- package/dist-types/handlers/step-handler/step-handler.d.ts.map +1 -1
- package/dist-types/handlers/wait-for-callback-handler/wait-for-callback-handler.d.ts +2 -1
- package/dist-types/handlers/wait-for-callback-handler/wait-for-callback-handler.d.ts.map +1 -1
- package/dist-types/handlers/wait-for-condition-handler/wait-for-condition-handler.d.ts +2 -1
- package/dist-types/handlers/wait-for-condition-handler/wait-for-condition-handler.d.ts.map +1 -1
- package/dist-types/index.d.ts +7 -3
- package/dist-types/index.d.ts.map +1 -1
- package/dist-types/testing/test-constants.d.ts +6 -0
- package/dist-types/testing/test-constants.d.ts.map +1 -1
- package/dist-types/types/batch.d.ts +50 -0
- package/dist-types/types/batch.d.ts.map +1 -1
- package/dist-types/types/child-context.d.ts +5 -0
- package/dist-types/types/child-context.d.ts.map +1 -1
- package/dist-types/types/durable-context.d.ts +23 -0
- package/dist-types/types/durable-context.d.ts.map +1 -1
- package/dist-types/utils/checkpoint/checkpoint-manager.d.ts.map +1 -1
- package/dist-types/utils/constants/constants.d.ts +6 -0
- package/dist-types/utils/constants/constants.d.ts.map +1 -1
- package/dist-types/utils/constants/version.d.ts +1 -12
- package/dist-types/utils/constants/version.d.ts.map +1 -1
- package/dist-types/utils/retry/linear-retry-strategy/linear-retry-strategy.d.ts +10 -0
- package/dist-types/utils/retry/linear-retry-strategy/linear-retry-strategy.d.ts.map +1 -0
- package/dist-types/utils/retry/linear-retry-strategy/linear-retry-strategy.test.d.ts +2 -0
- package/dist-types/utils/retry/linear-retry-strategy/linear-retry-strategy.test.d.ts.map +1 -0
- package/dist-types/utils/retry/retry-presets/retry-presets.d.ts +13 -1
- package/dist-types/utils/retry/retry-presets/retry-presets.d.ts.map +1 -1
- package/dist-types/utils/serdes/filesystem-serdes.d.ts +119 -0
- package/dist-types/utils/serdes/filesystem-serdes.d.ts.map +1 -0
- package/dist-types/utils/serdes/filesystem-serdes.test.d.ts +2 -0
- package/dist-types/utils/serdes/filesystem-serdes.test.d.ts.map +1 -0
- package/dist-types/utils/serdes/preview.d.ts +96 -0
- package/dist-types/utils/serdes/preview.d.ts.map +1 -0
- package/dist-types/utils/serdes/serdes.d.ts +29 -0
- package/dist-types/utils/serdes/serdes.d.ts.map +1 -1
- package/dist-types/utils/with-retry/index.d.ts +188 -0
- package/dist-types/utils/with-retry/index.d.ts.map +1 -0
- package/dist-types/utils/with-retry/index.test.d.ts +2 -0
- package/dist-types/utils/with-retry/index.test.d.ts.map +1 -0
- package/dist-types/with-durable-execution.d.ts.map +1 -1
- package/package.json +3 -3
package/dist-cjs/index.js
CHANGED
|
@@ -6,6 +6,8 @@ var async_hooks = require('async_hooks');
|
|
|
6
6
|
var crypto = require('crypto');
|
|
7
7
|
var node_console = require('node:console');
|
|
8
8
|
var util = require('node:util');
|
|
9
|
+
var promises = require('node:fs/promises');
|
|
10
|
+
var node_path = require('node:path');
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* @internal
|
|
@@ -235,6 +237,35 @@ exports.JitterStrategy = void 0;
|
|
|
235
237
|
JitterStrategy["HALF"] = "HALF";
|
|
236
238
|
})(exports.JitterStrategy || (exports.JitterStrategy = {}));
|
|
237
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Nesting type for batch operations (map and parallel)
|
|
242
|
+
*
|
|
243
|
+
* Controls how child contexts are created for each branch/iteration, affecting
|
|
244
|
+
* observability, cost, and scale limits.
|
|
245
|
+
*
|
|
246
|
+
* @public
|
|
247
|
+
*/
|
|
248
|
+
exports.NestingType = void 0;
|
|
249
|
+
(function (NestingType) {
|
|
250
|
+
/**
|
|
251
|
+
* Create CONTEXT operations for each branch/iteration with full checkpointing.
|
|
252
|
+
* Operations within each branch/iteration are wrapped in their own context.
|
|
253
|
+
*
|
|
254
|
+
* - **Observability**: High - each branch/iteration appears as separate operation in execution history
|
|
255
|
+
* - **Cost**: Higher - consumes more operations due to CONTEXT creation overhead
|
|
256
|
+
* - **Scale**: Lower maximum iterations due to operation limits
|
|
257
|
+
*/
|
|
258
|
+
NestingType["NESTED"] = "NESTED";
|
|
259
|
+
/**
|
|
260
|
+
* Skip CONTEXT operations for branches/iterations using virtual contexts.
|
|
261
|
+
* Operations execute directly without individual context wrapping.
|
|
262
|
+
*
|
|
263
|
+
* - **Observability**: Lower - branches/iterations don't appear as separate operations
|
|
264
|
+
* - **Cost**: ~30% lower - reduces operation consumption by skipping CONTEXT overhead
|
|
265
|
+
* - **Scale**: Higher maximum iterations possible within operation limits
|
|
266
|
+
*/
|
|
267
|
+
NestingType["FLAT"] = "FLAT";
|
|
268
|
+
})(exports.NestingType || (exports.NestingType = {}));
|
|
238
269
|
/**
|
|
239
270
|
* The status of a batch item
|
|
240
271
|
* @public
|
|
@@ -559,15 +590,39 @@ const createRetryStrategy = (config = {}) => {
|
|
|
559
590
|
};
|
|
560
591
|
};
|
|
561
592
|
|
|
593
|
+
/**
|
|
594
|
+
* Creates a linear backoff retry strategy
|
|
595
|
+
* @param maxAttempts - Maximum number of attempts (default: 6)
|
|
596
|
+
* @param initialDelay - Initial delay in seconds (default: 1)
|
|
597
|
+
* @param increment - Linear increment per attempt in seconds (default: 1)
|
|
598
|
+
* @returns Retry strategy with linear backoff
|
|
599
|
+
*/
|
|
600
|
+
const createLinearRetryStrategy = (maxAttempts = 6, initialDelay = 1, increment = 1) => {
|
|
601
|
+
return (error, attemptsMade) => {
|
|
602
|
+
if (attemptsMade >= maxAttempts) {
|
|
603
|
+
return { shouldRetry: false };
|
|
604
|
+
}
|
|
605
|
+
return {
|
|
606
|
+
shouldRetry: true,
|
|
607
|
+
delay: { seconds: initialDelay + increment * (attemptsMade - 1) },
|
|
608
|
+
};
|
|
609
|
+
};
|
|
610
|
+
};
|
|
611
|
+
|
|
562
612
|
/**
|
|
563
613
|
* Pre-configured retry strategies for common use cases
|
|
564
614
|
* @example
|
|
565
615
|
* ```typescript
|
|
566
|
-
* // Use default retry preset (
|
|
616
|
+
* // Use default retry preset (6 attempts with exponential backoff)
|
|
567
617
|
* await context.step('my-step', async () => {
|
|
568
618
|
* return await someOperation();
|
|
569
619
|
* }, { retryStrategy: retryPresets.default });
|
|
570
620
|
*
|
|
621
|
+
* // Use linear retry preset (1s, 2s, 3s, 4s, 5s delays)
|
|
622
|
+
* await context.step('linear-step', async () => {
|
|
623
|
+
* return await someOperation();
|
|
624
|
+
* }, { retryStrategy: retryPresets.linear });
|
|
625
|
+
*
|
|
571
626
|
* // Use no-retry preset (fail immediately on error)
|
|
572
627
|
* await context.step('critical-step', async () => {
|
|
573
628
|
* return await criticalOperation();
|
|
@@ -593,6 +648,13 @@ const retryPresets = {
|
|
|
593
648
|
backoffRate: 2,
|
|
594
649
|
jitter: exports.JitterStrategy.FULL,
|
|
595
650
|
}),
|
|
651
|
+
/**
|
|
652
|
+
* Linear retry strategy with fixed increment
|
|
653
|
+
* - 6 total attempts (1 initial + 5 retries)
|
|
654
|
+
* - Delays: 1s, 2s, 3s, 4s, 5s
|
|
655
|
+
* - Total max wait time: 15 seconds
|
|
656
|
+
*/
|
|
657
|
+
linear: createLinearRetryStrategy(6, 1, 1),
|
|
596
658
|
/**
|
|
597
659
|
* No retry strategy - fails immediately on first error
|
|
598
660
|
* - 1 total attempt (no retries)
|
|
@@ -633,6 +695,12 @@ const CHECKPOINT_TERMINATION_COOLDOWN_MS = 20;
|
|
|
633
695
|
* and limit polling duration for long-running operations
|
|
634
696
|
*/
|
|
635
697
|
const MAX_POLL_DURATION_MS = 15 * 60 * 1000;
|
|
698
|
+
/**
|
|
699
|
+
* Maximum checkpoint payload size in bytes (256KB).
|
|
700
|
+
* Payloads exceeding this limit trigger ReplayChildren mode in child contexts,
|
|
701
|
+
* and overflow-to-file behavior in FileSystemSerdes.
|
|
702
|
+
*/
|
|
703
|
+
const CHECKPOINT_SIZE_LIMIT_BYTES = 256 * 1024;
|
|
636
704
|
|
|
637
705
|
/**
|
|
638
706
|
* Base class for all durable operation errors
|
|
@@ -661,6 +729,10 @@ class DurableOperationError extends Error {
|
|
|
661
729
|
return new StepError(errorObject.ErrorMessage || "Step failed", cause, errorObject.ErrorData);
|
|
662
730
|
case "CallbackError":
|
|
663
731
|
return new CallbackError(errorObject.ErrorMessage || "Callback failed", cause, errorObject.ErrorData);
|
|
732
|
+
case "CallbackTimeoutError":
|
|
733
|
+
return new CallbackTimeoutError(errorObject.ErrorMessage || "Callback timed out", cause, errorObject.ErrorData);
|
|
734
|
+
case "CallbackSubmitterError":
|
|
735
|
+
return new CallbackSubmitterError(errorObject.ErrorMessage || "Callback submitter failed", cause, errorObject.ErrorData);
|
|
664
736
|
case "InvokeError":
|
|
665
737
|
return new InvokeError(errorObject.ErrorMessage || "Invoke failed", cause, errorObject.ErrorData);
|
|
666
738
|
case "ChildContextError":
|
|
@@ -703,6 +775,26 @@ class CallbackError extends DurableOperationError {
|
|
|
703
775
|
super(message || "Callback failed", cause, errorData);
|
|
704
776
|
}
|
|
705
777
|
}
|
|
778
|
+
/**
|
|
779
|
+
* Error thrown when a callback operation times out
|
|
780
|
+
* @public
|
|
781
|
+
*/
|
|
782
|
+
class CallbackTimeoutError extends DurableOperationError {
|
|
783
|
+
errorType = "CallbackTimeoutError";
|
|
784
|
+
constructor(message, cause, errorData) {
|
|
785
|
+
super(message || "Callback timed out", cause, errorData);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Error thrown when a callback submitter fails
|
|
790
|
+
* @public
|
|
791
|
+
*/
|
|
792
|
+
class CallbackSubmitterError extends DurableOperationError {
|
|
793
|
+
errorType = "CallbackSubmitterError";
|
|
794
|
+
constructor(message, cause, errorData) {
|
|
795
|
+
super(message || "Callback submitter failed", cause, errorData);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
706
798
|
/**
|
|
707
799
|
* Error thrown when an invoke operation fails
|
|
708
800
|
* @public
|
|
@@ -723,6 +815,16 @@ class ChildContextError extends DurableOperationError {
|
|
|
723
815
|
super(message || "Child context failed", cause, errorData);
|
|
724
816
|
}
|
|
725
817
|
}
|
|
818
|
+
/**
|
|
819
|
+
* Error thrown when a promise combinator operation fails
|
|
820
|
+
* @public
|
|
821
|
+
*/
|
|
822
|
+
class PromiseCombinatorError extends DurableOperationError {
|
|
823
|
+
errorType = "PromiseCombinatorError";
|
|
824
|
+
constructor(message, cause, errorData) {
|
|
825
|
+
super(message || "Promise combinator failed", cause, errorData);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
726
828
|
/**
|
|
727
829
|
* Error thrown when a wait for condition operation fails
|
|
728
830
|
* @public
|
|
@@ -1122,7 +1224,7 @@ const validateReplayConsistency = (stepId, currentOperation, checkpointData, con
|
|
|
1122
1224
|
}
|
|
1123
1225
|
};
|
|
1124
1226
|
|
|
1125
|
-
const createStepHandler = (context, checkpoint, parentContext, createStepId, logger, parentId) => {
|
|
1227
|
+
const createStepHandler = (context, checkpoint, parentContext, createStepId, logger, parentId, getDefaultSerdes) => {
|
|
1126
1228
|
return (nameOrFn, fnOrOptions, maybeOptions) => {
|
|
1127
1229
|
let name;
|
|
1128
1230
|
let fn;
|
|
@@ -1138,7 +1240,8 @@ const createStepHandler = (context, checkpoint, parentContext, createStepId, log
|
|
|
1138
1240
|
}
|
|
1139
1241
|
const stepId = createStepId();
|
|
1140
1242
|
const semantics = options?.semantics || exports.StepSemantics.AtLeastOncePerRetry;
|
|
1141
|
-
const serdes = options?.serdes ||
|
|
1243
|
+
const serdes = options?.serdes ||
|
|
1244
|
+
(getDefaultSerdes ? getDefaultSerdes() : defaultSerdes);
|
|
1142
1245
|
// Phase 1: Execute step
|
|
1143
1246
|
const phase1Promise = (async () => {
|
|
1144
1247
|
let stepData = context.getStepData(stepId);
|
|
@@ -1353,7 +1456,7 @@ const createStepHandler = (context, checkpoint, parentContext, createStepId, log
|
|
|
1353
1456
|
};
|
|
1354
1457
|
};
|
|
1355
1458
|
|
|
1356
|
-
const createInvokeHandler = (context, checkpoint, createStepId, parentId, checkAndUpdateReplayMode) => {
|
|
1459
|
+
const createInvokeHandler = (context, checkpoint, createStepId, parentId, checkAndUpdateReplayMode, getDefaultSerdes) => {
|
|
1357
1460
|
function invokeHandler(nameOrFuncId, funcIdOrInput, inputOrConfig, maybeConfig) {
|
|
1358
1461
|
const isNameFirst = typeof funcIdOrInput === "string";
|
|
1359
1462
|
const name = isNameFirst ? nameOrFuncId : undefined;
|
|
@@ -1411,7 +1514,8 @@ const createInvokeHandler = (context, checkpoint, createStepId, parentId, checkA
|
|
|
1411
1514
|
}
|
|
1412
1515
|
// Start invoke if not already started
|
|
1413
1516
|
if (!stepData) {
|
|
1414
|
-
const serializedPayload = await safeSerialize(config?.payloadSerdes ||
|
|
1517
|
+
const serializedPayload = await safeSerialize(config?.payloadSerdes ||
|
|
1518
|
+
(getDefaultSerdes ? getDefaultSerdes() : defaultSerdes), input, stepId, name, context.terminationManager, context.durableExecutionArn);
|
|
1415
1519
|
await checkpoint.checkpoint(stepId, {
|
|
1416
1520
|
Id: stepId,
|
|
1417
1521
|
ParentId: parentId,
|
|
@@ -1446,7 +1550,8 @@ const createInvokeHandler = (context, checkpoint, createStepId, parentId, checkA
|
|
|
1446
1550
|
const stepData = context.getStepData(stepId);
|
|
1447
1551
|
if (stepData?.Status === clientLambda.OperationStatus.SUCCEEDED) {
|
|
1448
1552
|
const invokeDetails = stepData.ChainedInvokeDetails;
|
|
1449
|
-
return await safeDeserialize(config?.resultSerdes ||
|
|
1553
|
+
return await safeDeserialize(config?.resultSerdes ||
|
|
1554
|
+
(getDefaultSerdes ? getDefaultSerdes() : defaultSerdes), invokeDetails?.Result, stepId, name, context.terminationManager, context.durableExecutionArn);
|
|
1450
1555
|
}
|
|
1451
1556
|
// Handle failure
|
|
1452
1557
|
const invokeDetails = stepData?.ChainedInvokeDetails;
|
|
@@ -1468,7 +1573,8 @@ const createInvokeHandler = (context, checkpoint, createStepId, parentId, checkA
|
|
|
1468
1573
|
checkAndUpdateReplayMode?.();
|
|
1469
1574
|
checkpoint.markOperationState(stepId, OperationLifecycleState.COMPLETED);
|
|
1470
1575
|
const invokeDetails = stepData.ChainedInvokeDetails;
|
|
1471
|
-
return await safeDeserialize(config?.resultSerdes ||
|
|
1576
|
+
return await safeDeserialize(config?.resultSerdes ||
|
|
1577
|
+
(getDefaultSerdes ? getDefaultSerdes() : defaultSerdes), invokeDetails?.Result, stepId, name, context.terminationManager, context.durableExecutionArn);
|
|
1472
1578
|
}
|
|
1473
1579
|
// Handle failure
|
|
1474
1580
|
log("❌", "Invoke failed:", { stepId, status: stepData?.Status });
|
|
@@ -1487,8 +1593,6 @@ const createInvokeHandler = (context, checkpoint, createStepId, parentId, checkA
|
|
|
1487
1593
|
return invokeHandler;
|
|
1488
1594
|
};
|
|
1489
1595
|
|
|
1490
|
-
// Checkpoint size limit in bytes (256KB)
|
|
1491
|
-
const CHECKPOINT_SIZE_LIMIT = 256 * 1024;
|
|
1492
1596
|
const determineChildReplayMode = (context, stepId) => {
|
|
1493
1597
|
const stepData = context.getStepData(stepId);
|
|
1494
1598
|
if (!stepData) {
|
|
@@ -1504,7 +1608,7 @@ const determineChildReplayMode = (context, stepId) => {
|
|
|
1504
1608
|
}
|
|
1505
1609
|
return DurableExecutionMode.ExecutionMode;
|
|
1506
1610
|
};
|
|
1507
|
-
const createRunInChildContextHandler = (context, checkpoint, parentContext, createStepId, getParentLogger, createChildContext, parentId) => {
|
|
1611
|
+
const createRunInChildContextHandler = (context, checkpoint, parentContext, createStepId, getParentLogger, createChildContext, parentId, getDefaultSerdes) => {
|
|
1508
1612
|
return (nameOrFn, fnOrOptions, maybeOptions) => {
|
|
1509
1613
|
let name;
|
|
1510
1614
|
let fn;
|
|
@@ -1542,10 +1646,10 @@ const createRunInChildContextHandler = (context, checkpoint, parentContext, crea
|
|
|
1542
1646
|
currentStepData?.Status === clientLambda.OperationStatus.FAILED) {
|
|
1543
1647
|
// Mark this run-in-child-context as finished to prevent descendant operations
|
|
1544
1648
|
checkpoint.markAncestorFinished(entityId);
|
|
1545
|
-
return handleCompletedChildContext(context, parentContext, entityId, name, fn, options, getParentLogger, createChildContext);
|
|
1649
|
+
return handleCompletedChildContext(context, parentContext, entityId, name, fn, options, getParentLogger, createChildContext, getDefaultSerdes);
|
|
1546
1650
|
}
|
|
1547
1651
|
// Execute if not completed
|
|
1548
|
-
return executeChildContext(context, checkpoint, parentContext, entityId, name, fn, options, getParentLogger, createChildContext, parentId);
|
|
1652
|
+
return executeChildContext(context, checkpoint, parentContext, entityId, name, fn, options, getParentLogger, createChildContext, parentId, getDefaultSerdes);
|
|
1549
1653
|
})()
|
|
1550
1654
|
.then((result) => {
|
|
1551
1655
|
phase1Result = result;
|
|
@@ -1563,14 +1667,19 @@ const createRunInChildContextHandler = (context, checkpoint, parentContext, crea
|
|
|
1563
1667
|
});
|
|
1564
1668
|
};
|
|
1565
1669
|
};
|
|
1566
|
-
const handleCompletedChildContext = async (context, parentContext, entityId, stepName, fn, options, getParentLogger, createChildContext) => {
|
|
1567
|
-
const serdes = options?.serdes || defaultSerdes;
|
|
1670
|
+
const handleCompletedChildContext = async (context, parentContext, entityId, stepName, fn, options, getParentLogger, createChildContext, getDefaultSerdes) => {
|
|
1671
|
+
const serdes = options?.serdes || (getDefaultSerdes ? getDefaultSerdes() : defaultSerdes);
|
|
1672
|
+
const errorMapper = options?.errorMapper;
|
|
1568
1673
|
const stepData = context.getStepData(entityId);
|
|
1569
1674
|
const result = stepData?.ContextDetails?.Result;
|
|
1570
1675
|
// Handle failed child context
|
|
1571
1676
|
if (stepData?.Status === clientLambda.OperationStatus.FAILED) {
|
|
1572
1677
|
if (stepData.ContextDetails?.Error) {
|
|
1573
1678
|
const originalError = DurableOperationError.fromErrorObject(stepData.ContextDetails.Error);
|
|
1679
|
+
// Use errorMapper if provided, otherwise wrap in ChildContextError
|
|
1680
|
+
if (errorMapper) {
|
|
1681
|
+
throw errorMapper(originalError);
|
|
1682
|
+
}
|
|
1574
1683
|
throw new ChildContextError(originalError.message, originalError);
|
|
1575
1684
|
}
|
|
1576
1685
|
else {
|
|
@@ -1589,10 +1698,12 @@ const handleCompletedChildContext = async (context, parentContext, entityId, ste
|
|
|
1589
1698
|
});
|
|
1590
1699
|
return await safeDeserialize(serdes, result, entityId, stepName, context.terminationManager, context.durableExecutionArn);
|
|
1591
1700
|
};
|
|
1592
|
-
const executeChildContext = async (context, checkpoint, parentContext, entityId, name, fn, options, getParentLogger, createChildContext, parentId) => {
|
|
1593
|
-
const serdes = options?.serdes || defaultSerdes;
|
|
1594
|
-
|
|
1595
|
-
|
|
1701
|
+
const executeChildContext = async (context, checkpoint, parentContext, entityId, name, fn, options, getParentLogger, createChildContext, parentId, getDefaultSerdes) => {
|
|
1702
|
+
const serdes = options?.serdes || (getDefaultSerdes ? getDefaultSerdes() : defaultSerdes);
|
|
1703
|
+
const errorMapper = options?.errorMapper;
|
|
1704
|
+
const isVirtual = options?.virtualContext === true;
|
|
1705
|
+
// Checkpoint at start if not already started and not virtual (fire-and-forget for performance)
|
|
1706
|
+
if (!isVirtual && context.getStepData(entityId) === undefined) {
|
|
1596
1707
|
const subType = options?.subType || exports.OperationSubType.RUN_IN_CHILD_CONTEXT;
|
|
1597
1708
|
checkpoint.checkpoint(entityId, {
|
|
1598
1709
|
Id: entityId,
|
|
@@ -1604,8 +1715,14 @@ const executeChildContext = async (context, checkpoint, parentContext, entityId,
|
|
|
1604
1715
|
});
|
|
1605
1716
|
}
|
|
1606
1717
|
const childReplayMode = determineChildReplayMode(context, entityId);
|
|
1607
|
-
// Create a child context with
|
|
1608
|
-
const durableChildContext = createChildContext(context, parentContext, childReplayMode, getParentLogger(), entityId,
|
|
1718
|
+
// Create a child context with appropriate parentId and stepPrefix
|
|
1719
|
+
const durableChildContext = createChildContext(context, parentContext, childReplayMode, getParentLogger(), entityId, // stepPrefix: use entityId for unique step IDs
|
|
1720
|
+
undefined,
|
|
1721
|
+
// parentId: this parameter is used for checkpointing, and should point to
|
|
1722
|
+
// valid parentId tthat is already checkpointed.
|
|
1723
|
+
// If this runInChildContext is a virtual, then we will use the parentId (the ancestor)
|
|
1724
|
+
// But if this runInChildContext is a virtual, then it's entityId can be used
|
|
1725
|
+
isVirtual ? parentId : entityId);
|
|
1609
1726
|
try {
|
|
1610
1727
|
// Execute the child context function with context tracking
|
|
1611
1728
|
const result = await runWithContext(entityId, parentId, () => fn(durableChildContext), undefined, childReplayMode);
|
|
@@ -1615,7 +1732,7 @@ const executeChildContext = async (context, checkpoint, parentContext, entityId,
|
|
|
1615
1732
|
let payloadToCheckpoint = serializedResult;
|
|
1616
1733
|
let replayChildren = false;
|
|
1617
1734
|
if (serializedResult &&
|
|
1618
|
-
Buffer.byteLength(serializedResult, "utf8") >
|
|
1735
|
+
Buffer.byteLength(serializedResult, "utf8") > CHECKPOINT_SIZE_LIMIT_BYTES) {
|
|
1619
1736
|
replayChildren = true;
|
|
1620
1737
|
// Use summary generator if provided, otherwise use empty string
|
|
1621
1738
|
if (options?.summaryGenerator) {
|
|
@@ -1628,50 +1745,63 @@ const executeChildContext = async (context, checkpoint, parentContext, entityId,
|
|
|
1628
1745
|
entityId,
|
|
1629
1746
|
name,
|
|
1630
1747
|
payloadSize: Buffer.byteLength(serializedResult, "utf8"),
|
|
1631
|
-
limit:
|
|
1748
|
+
limit: CHECKPOINT_SIZE_LIMIT_BYTES,
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
// Mark this run-in-child-context as finished to prevent descendant operations (only for non-virtual)
|
|
1752
|
+
if (!isVirtual) {
|
|
1753
|
+
checkpoint.markAncestorFinished(entityId);
|
|
1754
|
+
const subType = options?.subType || exports.OperationSubType.RUN_IN_CHILD_CONTEXT;
|
|
1755
|
+
checkpoint.checkpoint(entityId, {
|
|
1756
|
+
Id: entityId,
|
|
1757
|
+
ParentId: parentId,
|
|
1758
|
+
Action: clientLambda.OperationAction.SUCCEED,
|
|
1759
|
+
SubType: subType,
|
|
1760
|
+
Type: clientLambda.OperationType.CONTEXT,
|
|
1761
|
+
Payload: payloadToCheckpoint,
|
|
1762
|
+
ContextOptions: replayChildren ? { ReplayChildren: true } : undefined,
|
|
1763
|
+
Name: name,
|
|
1764
|
+
});
|
|
1765
|
+
log("✅", "Child context completed successfully:", {
|
|
1766
|
+
entityId,
|
|
1767
|
+
name,
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
else {
|
|
1771
|
+
log("✅", "Virtual child context completed successfully:", {
|
|
1772
|
+
entityId,
|
|
1773
|
+
name,
|
|
1632
1774
|
});
|
|
1633
1775
|
}
|
|
1634
|
-
// Mark this run-in-child-context as finished to prevent descendant operations
|
|
1635
|
-
checkpoint.markAncestorFinished(entityId);
|
|
1636
|
-
const subType = options?.subType || exports.OperationSubType.RUN_IN_CHILD_CONTEXT;
|
|
1637
|
-
checkpoint.checkpoint(entityId, {
|
|
1638
|
-
Id: entityId,
|
|
1639
|
-
ParentId: parentId,
|
|
1640
|
-
Action: clientLambda.OperationAction.SUCCEED,
|
|
1641
|
-
SubType: subType,
|
|
1642
|
-
Type: clientLambda.OperationType.CONTEXT,
|
|
1643
|
-
Payload: payloadToCheckpoint,
|
|
1644
|
-
ContextOptions: replayChildren ? { ReplayChildren: true } : undefined,
|
|
1645
|
-
Name: name,
|
|
1646
|
-
});
|
|
1647
|
-
log("✅", "Child context completed successfully:", {
|
|
1648
|
-
entityId,
|
|
1649
|
-
name,
|
|
1650
|
-
});
|
|
1651
1776
|
return result;
|
|
1652
1777
|
}
|
|
1653
1778
|
catch (error) {
|
|
1654
|
-
log("❌", "Child context failed:", {
|
|
1779
|
+
log("❌", isVirtual ? "Virtual child context failed:" : "Child context failed:", {
|
|
1655
1780
|
entityId,
|
|
1656
1781
|
name,
|
|
1657
1782
|
error,
|
|
1658
1783
|
});
|
|
1659
|
-
// Mark this run-in-child-context as finished
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1784
|
+
// Mark this run-in-child-context as finished and checkpoint failure (only for non-virtual)
|
|
1785
|
+
if (!isVirtual) {
|
|
1786
|
+
checkpoint.markAncestorFinished(entityId);
|
|
1787
|
+
const subType = options?.subType || exports.OperationSubType.RUN_IN_CHILD_CONTEXT;
|
|
1788
|
+
checkpoint.checkpoint(entityId, {
|
|
1789
|
+
Id: entityId,
|
|
1790
|
+
ParentId: parentId,
|
|
1791
|
+
Action: clientLambda.OperationAction.FAIL,
|
|
1792
|
+
SubType: subType,
|
|
1793
|
+
Type: clientLambda.OperationType.CONTEXT,
|
|
1794
|
+
Error: createErrorObjectFromError(error),
|
|
1795
|
+
Name: name,
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
// Always wrap in ChildContextError for consistent error handling
|
|
1673
1799
|
const errorObject = createErrorObjectFromError(error);
|
|
1674
1800
|
const reconstructedError = DurableOperationError.fromErrorObject(errorObject);
|
|
1801
|
+
// Use errorMapper if provided, otherwise wrap in ChildContextError
|
|
1802
|
+
if (errorMapper) {
|
|
1803
|
+
throw errorMapper(reconstructedError);
|
|
1804
|
+
}
|
|
1675
1805
|
throw new ChildContextError(reconstructedError.message, reconstructedError);
|
|
1676
1806
|
}
|
|
1677
1807
|
};
|
|
@@ -1778,7 +1908,7 @@ const createWaitHandler = (context, checkpoint, createStepId, parentId, checkAnd
|
|
|
1778
1908
|
return waitHandler;
|
|
1779
1909
|
};
|
|
1780
1910
|
|
|
1781
|
-
const createWaitForConditionHandler = (context, checkpoint, createStepId, logger, parentId) => {
|
|
1911
|
+
const createWaitForConditionHandler = (context, checkpoint, createStepId, logger, parentId, getDefaultSerdes) => {
|
|
1782
1912
|
return (nameOrCheck, checkOrConfig, maybeConfig) => {
|
|
1783
1913
|
let name;
|
|
1784
1914
|
let check;
|
|
@@ -1796,7 +1926,7 @@ const createWaitForConditionHandler = (context, checkpoint, createStepId, logger
|
|
|
1796
1926
|
throw new Error("waitForCondition requires config with waitStrategy and initialState");
|
|
1797
1927
|
}
|
|
1798
1928
|
const stepId = createStepId();
|
|
1799
|
-
const serdes = config.serdes || defaultSerdes;
|
|
1929
|
+
const serdes = config.serdes || (getDefaultSerdes ? getDefaultSerdes() : defaultSerdes);
|
|
1800
1930
|
const phase1Promise = (async () => {
|
|
1801
1931
|
let stepData = context.getStepData(stepId);
|
|
1802
1932
|
// Check if already completed
|
|
@@ -2001,7 +2131,7 @@ const createPassThroughSerdes = () => ({
|
|
|
2001
2131
|
serialize: async (value) => value,
|
|
2002
2132
|
deserialize: async (data) => data,
|
|
2003
2133
|
});
|
|
2004
|
-
const createCallback = (context, checkpoint, createStepId, checkAndUpdateReplayMode, parentId) => {
|
|
2134
|
+
const createCallback = (context, checkpoint, createStepId, checkAndUpdateReplayMode, parentId, getDefaultCallbackDeserializer) => {
|
|
2005
2135
|
return (nameOrConfig, maybeConfig) => {
|
|
2006
2136
|
let name;
|
|
2007
2137
|
let config;
|
|
@@ -2013,7 +2143,10 @@ const createCallback = (context, checkpoint, createStepId, checkAndUpdateReplayM
|
|
|
2013
2143
|
config = nameOrConfig;
|
|
2014
2144
|
}
|
|
2015
2145
|
const stepId = createStepId();
|
|
2016
|
-
const serdes = config?.serdes ||
|
|
2146
|
+
const serdes = config?.serdes ||
|
|
2147
|
+
(getDefaultCallbackDeserializer
|
|
2148
|
+
? getDefaultCallbackDeserializer()
|
|
2149
|
+
: createPassThroughSerdes());
|
|
2017
2150
|
// Phase 1: Setup and checkpoint
|
|
2018
2151
|
let isCompleted = false;
|
|
2019
2152
|
const phase1Promise = (async () => {
|
|
@@ -2105,16 +2238,22 @@ const createCallback = (context, checkpoint, createStepId, checkAndUpdateReplayM
|
|
|
2105
2238
|
const resolvedPromise = new DurablePromise(async () => deserializedResult);
|
|
2106
2239
|
return [resolvedPromise, callbackData.CallbackId];
|
|
2107
2240
|
}
|
|
2108
|
-
// Handle failure
|
|
2241
|
+
// Handle failure or timeout
|
|
2109
2242
|
const error = stepData?.CallbackDetails?.Error;
|
|
2243
|
+
const isTimeout = stepData?.Status === clientLambda.OperationStatus.TIMED_OUT;
|
|
2110
2244
|
const callbackError = error
|
|
2111
2245
|
? (() => {
|
|
2112
2246
|
const cause = new Error(error.ErrorMessage);
|
|
2113
2247
|
cause.name = error.ErrorType || "Error";
|
|
2114
2248
|
cause.stack = error.StackTrace?.join("\n");
|
|
2249
|
+
if (isTimeout) {
|
|
2250
|
+
return new CallbackTimeoutError(error.ErrorMessage || "Callback timed out", cause, error.ErrorData);
|
|
2251
|
+
}
|
|
2115
2252
|
return new CallbackError(error.ErrorMessage || "Callback failed", cause, error.ErrorData);
|
|
2116
2253
|
})()
|
|
2117
|
-
:
|
|
2254
|
+
: isTimeout
|
|
2255
|
+
? new CallbackTimeoutError("Callback timed out")
|
|
2256
|
+
: new CallbackError("Callback failed");
|
|
2118
2257
|
const rejectedPromise = new DurablePromise(async () => {
|
|
2119
2258
|
throw callbackError;
|
|
2120
2259
|
});
|
|
@@ -2134,7 +2273,7 @@ const createCallback = (context, checkpoint, createStepId, checkAndUpdateReplayM
|
|
|
2134
2273
|
};
|
|
2135
2274
|
};
|
|
2136
2275
|
|
|
2137
|
-
const createWaitForCallbackHandler = (context, getNextStepId, runInChildContext) => {
|
|
2276
|
+
const createWaitForCallbackHandler = (context, getNextStepId, runInChildContext, getDefaultCallbackDeserializer) => {
|
|
2138
2277
|
return (nameOrSubmitter, submitterOrConfig, maybeConfig) => {
|
|
2139
2278
|
let name;
|
|
2140
2279
|
let submitter;
|
|
@@ -2169,11 +2308,16 @@ const createWaitForCallbackHandler = (context, getNextStepId, runInChildContext)
|
|
|
2169
2308
|
});
|
|
2170
2309
|
// Use runInChildContext to ensure proper ID generation and isolation
|
|
2171
2310
|
const childFunction = async (childCtx) => {
|
|
2172
|
-
// Convert WaitForCallbackConfig to CreateCallbackConfig
|
|
2173
|
-
|
|
2311
|
+
// Convert WaitForCallbackConfig to CreateCallbackConfig.
|
|
2312
|
+
// When a defaultCallbackDeserializer is configured, force passthrough serdes
|
|
2313
|
+
// on the inner createCallback so the raw string is preserved for phase 2.
|
|
2314
|
+
const createCallbackConfig = config || getDefaultCallbackDeserializer
|
|
2174
2315
|
? {
|
|
2175
|
-
timeout: config
|
|
2176
|
-
heartbeatTimeout: config
|
|
2316
|
+
timeout: config?.timeout,
|
|
2317
|
+
heartbeatTimeout: config?.heartbeatTimeout,
|
|
2318
|
+
...(getDefaultCallbackDeserializer && {
|
|
2319
|
+
serdes: createPassThroughSerdes(),
|
|
2320
|
+
}),
|
|
2177
2321
|
}
|
|
2178
2322
|
: undefined;
|
|
2179
2323
|
// Create callback and get the promise + callbackId
|
|
@@ -2211,6 +2355,26 @@ const createWaitForCallbackHandler = (context, getNextStepId, runInChildContext)
|
|
|
2211
2355
|
return {
|
|
2212
2356
|
result: await runInChildContext(name, childFunction, {
|
|
2213
2357
|
subType: exports.OperationSubType.WAIT_FOR_CALLBACK,
|
|
2358
|
+
// When a defaultCallbackDeserializer is configured, use passthrough serdes
|
|
2359
|
+
// so the raw callback string is preserved through the runInChildContext
|
|
2360
|
+
// round-trip and phase 2 can apply the deserializer exactly once.
|
|
2361
|
+
// Without this, defaultSerdes (JSON) would add an extra encode/decode layer.
|
|
2362
|
+
...(getDefaultCallbackDeserializer && {
|
|
2363
|
+
serdes: createPassThroughSerdes(),
|
|
2364
|
+
}),
|
|
2365
|
+
errorMapper: (originalError) => {
|
|
2366
|
+
// Pass through callback errors directly (both timeout and failure)
|
|
2367
|
+
if (originalError.errorType === "CallbackTimeoutError" ||
|
|
2368
|
+
originalError.errorType === "CallbackError") {
|
|
2369
|
+
return originalError;
|
|
2370
|
+
}
|
|
2371
|
+
// Map step errors to CallbackSubmitterError
|
|
2372
|
+
if (originalError.errorType === "StepError") {
|
|
2373
|
+
return new CallbackSubmitterError(originalError.message, originalError);
|
|
2374
|
+
}
|
|
2375
|
+
// Wrap other errors in ChildContextError
|
|
2376
|
+
return new ChildContextError(originalError.message, originalError);
|
|
2377
|
+
},
|
|
2214
2378
|
}),
|
|
2215
2379
|
stepId,
|
|
2216
2380
|
};
|
|
@@ -2222,7 +2386,10 @@ const createWaitForCallbackHandler = (context, getNextStepId, runInChildContext)
|
|
|
2222
2386
|
return new DurablePromise(async () => {
|
|
2223
2387
|
const { result, stepId } = await phase1Promise;
|
|
2224
2388
|
// Always deserialize the result since it's a string
|
|
2225
|
-
return (await safeDeserialize(config?.serdes ??
|
|
2389
|
+
return (await safeDeserialize(config?.serdes ??
|
|
2390
|
+
(getDefaultCallbackDeserializer
|
|
2391
|
+
? getDefaultCallbackDeserializer()
|
|
2392
|
+
: createPassThroughSerdes()), result, stepId, name, context.terminationManager, context.durableExecutionArn));
|
|
2226
2393
|
});
|
|
2227
2394
|
};
|
|
2228
2395
|
};
|
|
@@ -2306,6 +2473,7 @@ const createMapHandler = (context, executeConcurrently) => {
|
|
|
2306
2473
|
completionConfig: config?.completionConfig,
|
|
2307
2474
|
serdes: config?.serdes,
|
|
2308
2475
|
itemSerdes: config?.itemSerdes,
|
|
2476
|
+
nesting: config?.nesting,
|
|
2309
2477
|
});
|
|
2310
2478
|
log("🗺️", "Map operation completed successfully:", {
|
|
2311
2479
|
resultCount: result.totalCount,
|
|
@@ -2386,6 +2554,7 @@ const createParallelHandler = (context, executeConcurrently) => {
|
|
|
2386
2554
|
completionConfig: config?.completionConfig,
|
|
2387
2555
|
serdes: config?.serdes,
|
|
2388
2556
|
itemSerdes: config?.itemSerdes,
|
|
2557
|
+
nesting: config?.nesting,
|
|
2389
2558
|
});
|
|
2390
2559
|
log("🔀", "Parallel operation completed successfully:", {
|
|
2391
2560
|
resultCount: result.totalCount,
|
|
@@ -2447,13 +2616,7 @@ function createErrorAwareSerdes() {
|
|
|
2447
2616
|
: undefined,
|
|
2448
2617
|
};
|
|
2449
2618
|
}
|
|
2450
|
-
|
|
2451
|
-
const stepConfig = {
|
|
2452
|
-
retryStrategy: () => ({
|
|
2453
|
-
shouldRetry: false,
|
|
2454
|
-
}),
|
|
2455
|
-
};
|
|
2456
|
-
const createPromiseHandler = (step) => {
|
|
2619
|
+
const createPromiseHandler = (runInChildContext) => {
|
|
2457
2620
|
const parseParams = (nameOrPromises, maybePromises) => {
|
|
2458
2621
|
if (typeof nameOrPromises === "string" || nameOrPromises === undefined) {
|
|
2459
2622
|
return { name: nameOrPromises, promises: maybePromises };
|
|
@@ -2463,32 +2626,38 @@ const createPromiseHandler = (step) => {
|
|
|
2463
2626
|
const all = (nameOrPromises, maybePromises) => {
|
|
2464
2627
|
return new DurablePromise(async () => {
|
|
2465
2628
|
const { name, promises } = parseParams(nameOrPromises, maybePromises);
|
|
2466
|
-
// Wrap Promise.all execution in a
|
|
2467
|
-
return await
|
|
2629
|
+
// Wrap Promise.all execution in a child context for persistence
|
|
2630
|
+
return await runInChildContext(name, () => Promise.all(promises), {
|
|
2631
|
+
errorMapper: (error) => new PromiseCombinatorError(error.message, error),
|
|
2632
|
+
});
|
|
2468
2633
|
});
|
|
2469
2634
|
};
|
|
2470
2635
|
const allSettled = (nameOrPromises, maybePromises) => {
|
|
2471
2636
|
return new DurablePromise(async () => {
|
|
2472
2637
|
const { name, promises } = parseParams(nameOrPromises, maybePromises);
|
|
2473
|
-
// Wrap Promise.allSettled execution in a
|
|
2474
|
-
return await
|
|
2475
|
-
...stepConfig,
|
|
2638
|
+
// Wrap Promise.allSettled execution in a child context for persistence
|
|
2639
|
+
return await runInChildContext(name, () => Promise.allSettled(promises), {
|
|
2476
2640
|
serdes: createErrorAwareSerdes(),
|
|
2641
|
+
errorMapper: (error) => new PromiseCombinatorError(error.message, error),
|
|
2477
2642
|
});
|
|
2478
2643
|
});
|
|
2479
2644
|
};
|
|
2480
2645
|
const any = (nameOrPromises, maybePromises) => {
|
|
2481
2646
|
return new DurablePromise(async () => {
|
|
2482
2647
|
const { name, promises } = parseParams(nameOrPromises, maybePromises);
|
|
2483
|
-
// Wrap Promise.any execution in a
|
|
2484
|
-
return await
|
|
2648
|
+
// Wrap Promise.any execution in a child context for persistence
|
|
2649
|
+
return await runInChildContext(name, () => Promise.any(promises), {
|
|
2650
|
+
errorMapper: (error) => new PromiseCombinatorError(error.message, error),
|
|
2651
|
+
});
|
|
2485
2652
|
});
|
|
2486
2653
|
};
|
|
2487
2654
|
const race = (nameOrPromises, maybePromises) => {
|
|
2488
2655
|
return new DurablePromise(async () => {
|
|
2489
2656
|
const { name, promises } = parseParams(nameOrPromises, maybePromises);
|
|
2490
|
-
// Wrap Promise.race execution in a
|
|
2491
|
-
return await
|
|
2657
|
+
// Wrap Promise.race execution in a child context for persistence
|
|
2658
|
+
return await runInChildContext(name, () => Promise.race(promises), {
|
|
2659
|
+
errorMapper: (error) => new PromiseCombinatorError(error.message, error),
|
|
2660
|
+
});
|
|
2492
2661
|
});
|
|
2493
2662
|
};
|
|
2494
2663
|
return {
|
|
@@ -2578,9 +2747,11 @@ function restoreBatchResult(data) {
|
|
|
2578
2747
|
class ConcurrencyController {
|
|
2579
2748
|
operationName;
|
|
2580
2749
|
skipNextOperation;
|
|
2581
|
-
|
|
2750
|
+
getDefaultSerdes;
|
|
2751
|
+
constructor(operationName, skipNextOperation, getDefaultSerdes) {
|
|
2582
2752
|
this.operationName = operationName;
|
|
2583
2753
|
this.skipNextOperation = skipNextOperation;
|
|
2754
|
+
this.getDefaultSerdes = getDefaultSerdes;
|
|
2584
2755
|
}
|
|
2585
2756
|
isChildEntityCompleted(executionContext, parentEntityId, completedCount) {
|
|
2586
2757
|
const childEntityId = `${parentEntityId}-${completedCount + 1}`;
|
|
@@ -2638,7 +2809,8 @@ class ConcurrencyController {
|
|
|
2638
2809
|
const summaryPayload = stepData?.ContextDetails?.Result;
|
|
2639
2810
|
if (summaryPayload) {
|
|
2640
2811
|
try {
|
|
2641
|
-
const serdes = config.serdes ||
|
|
2812
|
+
const serdes = config.serdes ||
|
|
2813
|
+
(this.getDefaultSerdes ? this.getDefaultSerdes() : defaultSerdes);
|
|
2642
2814
|
const parsedSummary = await serdes.deserialize(summaryPayload, {
|
|
2643
2815
|
entityId: entityId,
|
|
2644
2816
|
durableExecutionArn: executionContext.durableExecutionArn,
|
|
@@ -2701,7 +2873,11 @@ class ConcurrencyController {
|
|
|
2701
2873
|
continue;
|
|
2702
2874
|
}
|
|
2703
2875
|
try {
|
|
2704
|
-
const result = await parentContext.runInChildContext(item.name || item.id, (childContext) => executor(item, childContext), {
|
|
2876
|
+
const result = await parentContext.runInChildContext(item.name || item.id, (childContext) => executor(item, childContext), {
|
|
2877
|
+
subType: config.iterationSubType,
|
|
2878
|
+
serdes: config.itemSerdes,
|
|
2879
|
+
virtualContext: config.nesting === exports.NestingType.FLAT,
|
|
2880
|
+
});
|
|
2705
2881
|
resultItems.push({
|
|
2706
2882
|
result,
|
|
2707
2883
|
index: item.index,
|
|
@@ -2806,7 +2982,11 @@ class ConcurrencyController {
|
|
|
2806
2982
|
itemName: item.name,
|
|
2807
2983
|
});
|
|
2808
2984
|
parentContext
|
|
2809
|
-
.runInChildContext(item.name || item.id, (childContext) => executor(item, childContext), {
|
|
2985
|
+
.runInChildContext(item.name || item.id, (childContext) => executor(item, childContext), {
|
|
2986
|
+
subType: config.iterationSubType,
|
|
2987
|
+
serdes: config.itemSerdes,
|
|
2988
|
+
virtualContext: config.nesting === exports.NestingType.FLAT,
|
|
2989
|
+
})
|
|
2810
2990
|
.then((result) => {
|
|
2811
2991
|
resultItems[index] = {
|
|
2812
2992
|
result,
|
|
@@ -2876,7 +3056,7 @@ class ConcurrencyController {
|
|
|
2876
3056
|
});
|
|
2877
3057
|
}
|
|
2878
3058
|
}
|
|
2879
|
-
const createConcurrentExecutionHandler = (context, runInChildContext, skipNextOperation) => {
|
|
3059
|
+
const createConcurrentExecutionHandler = (context, runInChildContext, skipNextOperation, getDefaultSerdes) => {
|
|
2880
3060
|
return (nameOrItems, itemsOrExecutor, executorOrConfig, maybeConfig) => {
|
|
2881
3061
|
// Phase 1: Start execution immediately
|
|
2882
3062
|
const phase1Promise = (async () => {
|
|
@@ -2912,7 +3092,7 @@ const createConcurrentExecutionHandler = (context, runInChildContext, skipNextOp
|
|
|
2912
3092
|
throw new Error(`Invalid maxConcurrency: ${config.maxConcurrency}. Must be a positive number or undefined for unlimited concurrency.`);
|
|
2913
3093
|
}
|
|
2914
3094
|
const executeOperation = async (executionContext) => {
|
|
2915
|
-
const concurrencyController = new ConcurrencyController("concurrent-execution", skipNextOperation);
|
|
3095
|
+
const concurrencyController = new ConcurrencyController("concurrent-execution", skipNextOperation, getDefaultSerdes);
|
|
2916
3096
|
// Access durableExecutionMode from the context - it's set by runInChildContext
|
|
2917
3097
|
// based on determineChildReplayMode logic
|
|
2918
3098
|
const durableExecutionMode = executionContext.durableExecutionMode;
|
|
@@ -3035,6 +3215,9 @@ class DurableContextImpl {
|
|
|
3035
3215
|
_parentId;
|
|
3036
3216
|
modeManagement;
|
|
3037
3217
|
durableExecution;
|
|
3218
|
+
_defaultSerdes = defaultSerdes;
|
|
3219
|
+
_defaultCallbackDeserializer = createPassThroughSerdes();
|
|
3220
|
+
_customCallbackDeserializerSet = false;
|
|
3038
3221
|
logger;
|
|
3039
3222
|
executionContext;
|
|
3040
3223
|
constructor(_executionContext, lambdaContext, durableExecutionMode, inheritedLogger, stepPrefix, durableExecution, parentId) {
|
|
@@ -3175,14 +3358,14 @@ class DurableContextImpl {
|
|
|
3175
3358
|
step(nameOrFn, fnOrOptions, maybeOptions) {
|
|
3176
3359
|
validateContextUsage(this._stepPrefix, "step", this._executionContext.terminationManager);
|
|
3177
3360
|
return this.withDurableModeManagement(() => {
|
|
3178
|
-
const stepHandler = createStepHandler(this._executionContext, this.checkpoint, this.lambdaContext, this.createStepId.bind(this), this.durableLogger, this._parentId);
|
|
3361
|
+
const stepHandler = createStepHandler(this._executionContext, this.checkpoint, this.lambdaContext, this.createStepId.bind(this), this.durableLogger, this._parentId, () => this._defaultSerdes);
|
|
3179
3362
|
return stepHandler(nameOrFn, fnOrOptions, maybeOptions);
|
|
3180
3363
|
});
|
|
3181
3364
|
}
|
|
3182
3365
|
invoke(nameOrFuncId, funcIdOrInput, inputOrConfig, maybeConfig) {
|
|
3183
3366
|
validateContextUsage(this._stepPrefix, "invoke", this._executionContext.terminationManager);
|
|
3184
3367
|
return this.withDurableModeManagement(() => {
|
|
3185
|
-
const invokeHandler = createInvokeHandler(this._executionContext, this.checkpoint, this.createStepId.bind(this), this._parentId, this.checkAndUpdateReplayMode.bind(this));
|
|
3368
|
+
const invokeHandler = createInvokeHandler(this._executionContext, this.checkpoint, this.createStepId.bind(this), this._parentId, this.checkAndUpdateReplayMode.bind(this), () => this._defaultSerdes);
|
|
3186
3369
|
return invokeHandler(...[
|
|
3187
3370
|
nameOrFuncId,
|
|
3188
3371
|
funcIdOrInput,
|
|
@@ -3196,7 +3379,17 @@ class DurableContextImpl {
|
|
|
3196
3379
|
return this.withDurableModeManagement(() => {
|
|
3197
3380
|
const blockHandler = createRunInChildContextHandler(this._executionContext, this.checkpoint, this.lambdaContext, this.createStepId.bind(this), () => this.durableLogger,
|
|
3198
3381
|
// Adapter function to maintain compatibility
|
|
3199
|
-
(executionContext, parentContext, durableExecutionMode, inheritedLogger, stepPrefix, _checkpointToken, parentId) =>
|
|
3382
|
+
(executionContext, parentContext, durableExecutionMode, inheritedLogger, stepPrefix, _checkpointToken, parentId) => {
|
|
3383
|
+
const childCtx = createDurableContext(executionContext, parentContext, durableExecutionMode, inheritedLogger, stepPrefix, this.durableExecution, parentId);
|
|
3384
|
+
// Propagate serdes config to child context
|
|
3385
|
+
childCtx.configureSerdes({
|
|
3386
|
+
defaultSerdes: this._defaultSerdes,
|
|
3387
|
+
...(this._customCallbackDeserializerSet && {
|
|
3388
|
+
defaultCallbackDeserializer: this._defaultCallbackDeserializer,
|
|
3389
|
+
}),
|
|
3390
|
+
});
|
|
3391
|
+
return childCtx;
|
|
3392
|
+
}, this._parentId, () => this._defaultSerdes);
|
|
3200
3393
|
return blockHandler(nameOrFn, fnOrOptions, maybeOptions);
|
|
3201
3394
|
});
|
|
3202
3395
|
}
|
|
@@ -3234,24 +3427,39 @@ class DurableContextImpl {
|
|
|
3234
3427
|
this.modeAwareLoggingEnabled = config.modeAware;
|
|
3235
3428
|
}
|
|
3236
3429
|
}
|
|
3430
|
+
configureSerdes(config) {
|
|
3431
|
+
if (config.defaultSerdes !== undefined) {
|
|
3432
|
+
this._defaultSerdes = config.defaultSerdes;
|
|
3433
|
+
}
|
|
3434
|
+
if (config.defaultCallbackDeserializer !== undefined) {
|
|
3435
|
+
this._defaultCallbackDeserializer = config.defaultCallbackDeserializer;
|
|
3436
|
+
this._customCallbackDeserializerSet = true;
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3237
3439
|
createCallback(nameOrConfig, maybeConfig) {
|
|
3238
3440
|
validateContextUsage(this._stepPrefix, "createCallback", this._executionContext.terminationManager);
|
|
3239
3441
|
return this.withDurableModeManagement(() => {
|
|
3240
|
-
const callbackFactory = createCallback(this._executionContext, this.checkpoint, this.createStepId.bind(this), this.checkAndUpdateReplayMode.bind(this), this._parentId);
|
|
3442
|
+
const callbackFactory = createCallback(this._executionContext, this.checkpoint, this.createStepId.bind(this), this.checkAndUpdateReplayMode.bind(this), this._parentId, () => this._defaultCallbackDeserializer);
|
|
3241
3443
|
return callbackFactory(nameOrConfig, maybeConfig);
|
|
3242
3444
|
});
|
|
3243
3445
|
}
|
|
3244
3446
|
waitForCallback(nameOrSubmitter, submitterOrConfig, maybeConfig) {
|
|
3245
3447
|
validateContextUsage(this._stepPrefix, "waitForCallback", this._executionContext.terminationManager);
|
|
3246
3448
|
return this.withDurableModeManagement(() => {
|
|
3247
|
-
const waitForCallbackHandler = createWaitForCallbackHandler(this._executionContext, this.getNextStepId.bind(this), this.runInChildContext.bind(this)
|
|
3449
|
+
const waitForCallbackHandler = createWaitForCallbackHandler(this._executionContext, this.getNextStepId.bind(this), this.runInChildContext.bind(this),
|
|
3450
|
+
// Only pass the getter when the user has explicitly configured a custom
|
|
3451
|
+
// deserializer. The default is createPassThroughSerdes() which matches
|
|
3452
|
+
// the original behavior, so no injection is needed in that case.
|
|
3453
|
+
this._customCallbackDeserializerSet
|
|
3454
|
+
? () => this._defaultCallbackDeserializer
|
|
3455
|
+
: undefined);
|
|
3248
3456
|
return waitForCallbackHandler(nameOrSubmitter, submitterOrConfig, maybeConfig);
|
|
3249
3457
|
});
|
|
3250
3458
|
}
|
|
3251
3459
|
waitForCondition(nameOrCheckFunc, checkFuncOrConfig, maybeConfig) {
|
|
3252
3460
|
validateContextUsage(this._stepPrefix, "waitForCondition", this._executionContext.terminationManager);
|
|
3253
3461
|
return this.withDurableModeManagement(() => {
|
|
3254
|
-
const waitForConditionHandler = createWaitForConditionHandler(this._executionContext, this.checkpoint, this.createStepId.bind(this), this.durableLogger, this._parentId);
|
|
3462
|
+
const waitForConditionHandler = createWaitForConditionHandler(this._executionContext, this.checkpoint, this.createStepId.bind(this), this.durableLogger, this._parentId, () => this._defaultSerdes);
|
|
3255
3463
|
return typeof nameOrCheckFunc === "string" ||
|
|
3256
3464
|
nameOrCheckFunc === undefined
|
|
3257
3465
|
? waitForConditionHandler(nameOrCheckFunc, checkFuncOrConfig, maybeConfig)
|
|
@@ -3275,7 +3483,7 @@ class DurableContextImpl {
|
|
|
3275
3483
|
_executeConcurrently(nameOrItems, itemsOrExecutor, executorOrConfig, maybeConfig) {
|
|
3276
3484
|
validateContextUsage(this._stepPrefix, "_executeConcurrently", this._executionContext.terminationManager);
|
|
3277
3485
|
return this.withDurableModeManagement(() => {
|
|
3278
|
-
const concurrentExecutionHandler = createConcurrentExecutionHandler(this._executionContext, this.runInChildContext.bind(this), this.skipNextOperation.bind(this));
|
|
3486
|
+
const concurrentExecutionHandler = createConcurrentExecutionHandler(this._executionContext, this.runInChildContext.bind(this), this.skipNextOperation.bind(this), () => this._defaultSerdes);
|
|
3279
3487
|
const promise = concurrentExecutionHandler(nameOrItems, itemsOrExecutor, executorOrConfig, maybeConfig);
|
|
3280
3488
|
// Prevent unhandled promise rejections
|
|
3281
3489
|
promise?.catch(() => { });
|
|
@@ -3283,7 +3491,7 @@ class DurableContextImpl {
|
|
|
3283
3491
|
});
|
|
3284
3492
|
}
|
|
3285
3493
|
get promise() {
|
|
3286
|
-
return createPromiseHandler(this.
|
|
3494
|
+
return createPromiseHandler(this.runInChildContext.bind(this));
|
|
3287
3495
|
}
|
|
3288
3496
|
}
|
|
3289
3497
|
const createDurableContext = (executionContext, parentContext, durableExecutionMode, inheritedLogger, stepPrefix, durableExecution, parentId) => {
|
|
@@ -3313,6 +3521,22 @@ class CheckpointUnrecoverableExecutionError extends UnrecoverableExecutionError
|
|
|
3313
3521
|
}
|
|
3314
3522
|
}
|
|
3315
3523
|
|
|
3524
|
+
// KMS errors from Lambda that indicate customer-caused key misconfiguration.
|
|
3525
|
+
// These arrive as 502 errors but are non-retryable.
|
|
3526
|
+
const NON_RETRYABLE_CUSTOMER_ERRORS = new Set([
|
|
3527
|
+
"KMSAccessDeniedException",
|
|
3528
|
+
"KMSDisabledException",
|
|
3529
|
+
"KMSInvalidStateException",
|
|
3530
|
+
"KMSNotFoundException",
|
|
3531
|
+
]);
|
|
3532
|
+
/**
|
|
3533
|
+
* Returns true if the error is a non-retryable customer error (e.g., KMS key misconfiguration).
|
|
3534
|
+
*/
|
|
3535
|
+
function isNonRetryableCustomerError(error) {
|
|
3536
|
+
const name = error?.name;
|
|
3537
|
+
return !!name && NON_RETRYABLE_CUSTOMER_ERRORS.has(name);
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3316
3540
|
const STEP_DATA_UPDATED_EVENT = "stepDataUpdated";
|
|
3317
3541
|
const TERMINAL_STATUSES = [
|
|
3318
3542
|
clientLambda.OperationStatus.SUCCEEDED,
|
|
@@ -3478,6 +3702,11 @@ class CheckpointManager {
|
|
|
3478
3702
|
statusCode !== 429) {
|
|
3479
3703
|
return new CheckpointUnrecoverableExecutionError(`Checkpoint failed: ${errorMessage}`, originalError);
|
|
3480
3704
|
}
|
|
3705
|
+
// For example: KMS errors from Lambda arrive as 502 errors. These indicate customer-caused
|
|
3706
|
+
// KMS key misconfiguration and should not be retried — treat as execution error.
|
|
3707
|
+
if (isNonRetryableCustomerError(error)) {
|
|
3708
|
+
return new CheckpointUnrecoverableExecutionError(`Checkpoint failed: ${errorMessage}`, originalError);
|
|
3709
|
+
}
|
|
3481
3710
|
return new CheckpointUnrecoverableInvocationError(`Checkpoint failed: ${errorMessage}`, originalError);
|
|
3482
3711
|
}
|
|
3483
3712
|
async processQueue() {
|
|
@@ -4245,17 +4474,27 @@ const createDefaultLogger = (executionContext) => {
|
|
|
4245
4474
|
|
|
4246
4475
|
/**
|
|
4247
4476
|
* SDK metadata injected by Rollup at build time from package.json.
|
|
4477
|
+
* These values are inserted into UserAgent headers.
|
|
4478
|
+
*
|
|
4479
|
+
* At build time, Rollup replaces "2.0.0-alpha.1"
|
|
4480
|
+
* with actual values from package.json.
|
|
4248
4481
|
*
|
|
4249
|
-
*
|
|
4250
|
-
*
|
|
4482
|
+
* SDK_NAME is a fixed string matching the cross-SDK convention:
|
|
4483
|
+
* aws-durable-execution-sdk-\{language\}
|
|
4484
|
+
* Alternate version if SDK path is within the Lambda bundled runtime.
|
|
4251
4485
|
*
|
|
4252
4486
|
* Defaults are provided for test environments where Rollup doesn't run
|
|
4253
4487
|
* and process.env values are undefined.
|
|
4254
4488
|
*
|
|
4255
4489
|
* @internal
|
|
4256
4490
|
*/
|
|
4257
|
-
const
|
|
4258
|
-
const
|
|
4491
|
+
const runtimeDir = process.env.LAMBDA_RUNTIME_DIR || "/var/runtime";
|
|
4492
|
+
const isRuntimeBundled = typeof __dirname !== "undefined" && __dirname.startsWith(runtimeDir);
|
|
4493
|
+
const SDK_NAME = "aws-durable-execution-sdk-js";
|
|
4494
|
+
const baseVersion = "2.0.0-alpha.1";
|
|
4495
|
+
const SDK_VERSION = isRuntimeBundled
|
|
4496
|
+
? `${baseVersion}-bundled`
|
|
4497
|
+
: baseVersion;
|
|
4259
4498
|
|
|
4260
4499
|
let defaultLambdaClient;
|
|
4261
4500
|
/**
|
|
@@ -4699,11 +4938,293 @@ function validateDurableExecutionEvent(event) {
|
|
|
4699
4938
|
const withDurableExecution = (handler, config) => {
|
|
4700
4939
|
return async (event, context) => {
|
|
4701
4940
|
validateDurableExecutionEvent(event);
|
|
4702
|
-
|
|
4703
|
-
|
|
4941
|
+
try {
|
|
4942
|
+
const { executionContext, durableExecutionMode, checkpointToken } = await initializeExecutionContext(event, context, config?.client);
|
|
4943
|
+
return await runHandler(event, context, executionContext, durableExecutionMode, checkpointToken, handler);
|
|
4944
|
+
}
|
|
4945
|
+
catch (error) {
|
|
4946
|
+
// Non-retryable customer errors (e.g., KMS key misconfiguration) should
|
|
4947
|
+
// fail the execution immediately rather than retrying the invocation.
|
|
4948
|
+
if (isNonRetryableCustomerError(error)) {
|
|
4949
|
+
return {
|
|
4950
|
+
Status: exports.InvocationStatus.FAILED,
|
|
4951
|
+
Error: createErrorObjectFromError(error),
|
|
4952
|
+
};
|
|
4953
|
+
}
|
|
4954
|
+
throw error;
|
|
4955
|
+
}
|
|
4704
4956
|
};
|
|
4705
4957
|
};
|
|
4706
4958
|
|
|
4959
|
+
/**
|
|
4960
|
+
* Controls whether a preview field is matched by name anywhere in the object
|
|
4961
|
+
* tree, or by exact dot-notation path from the root.
|
|
4962
|
+
*
|
|
4963
|
+
* @public
|
|
4964
|
+
*/
|
|
4965
|
+
exports.FieldMatchMode = void 0;
|
|
4966
|
+
(function (FieldMatchMode) {
|
|
4967
|
+
/** Match the field name at any depth in the object tree (default). */
|
|
4968
|
+
FieldMatchMode["ANYWHERE"] = "ANYWHERE";
|
|
4969
|
+
/**
|
|
4970
|
+
* Match by exact dot-notation path from root.
|
|
4971
|
+
* A single segment (e.g. `"email"`) matches only the root-level field.
|
|
4972
|
+
* A dotted path (e.g. `"user.email"`) matches that exact nested location.
|
|
4973
|
+
*/
|
|
4974
|
+
FieldMatchMode["PATH"] = "PATH";
|
|
4975
|
+
})(exports.FieldMatchMode || (exports.FieldMatchMode = {}));
|
|
4976
|
+
/**
|
|
4977
|
+
* Controls which fields are included in the preview by default.
|
|
4978
|
+
*
|
|
4979
|
+
* @public
|
|
4980
|
+
*/
|
|
4981
|
+
exports.PreviewMode = void 0;
|
|
4982
|
+
(function (PreviewMode) {
|
|
4983
|
+
/** Include all fields, then apply `exclude` and `mask` rules. */
|
|
4984
|
+
PreviewMode["INCLUDE_ALL"] = "INCLUDE_ALL";
|
|
4985
|
+
/** Exclude all fields, then apply `include` and `mask` rules. */
|
|
4986
|
+
PreviewMode["EXCLUDE_ALL"] = "EXCLUDE_ALL";
|
|
4987
|
+
})(exports.PreviewMode || (exports.PreviewMode = {}));
|
|
4988
|
+
/** Returns true if the field at `path` (dot-notation) matches the given PreviewField rule. */
|
|
4989
|
+
function fieldMatches(path, field) {
|
|
4990
|
+
const mode = field.match ?? exports.FieldMatchMode.ANYWHERE;
|
|
4991
|
+
if (mode === exports.FieldMatchMode.PATH) {
|
|
4992
|
+
return path === field.name;
|
|
4993
|
+
}
|
|
4994
|
+
return path.split(".").includes(field.name);
|
|
4995
|
+
}
|
|
4996
|
+
function isMatched(path, fields) {
|
|
4997
|
+
return fields?.some((f) => fieldMatches(path, f)) ?? false;
|
|
4998
|
+
}
|
|
4999
|
+
/**
|
|
5000
|
+
* Builds a preview object from `value` according to `config`.
|
|
5001
|
+
*
|
|
5002
|
+
* Traverses the object tree and collects fields based on the include/exclude/mask
|
|
5003
|
+
* rules in `config`. The result is a nested object mirroring the original structure,
|
|
5004
|
+
* capped at `config.maxPreviewBytes` (default 4096 bytes).
|
|
5005
|
+
*
|
|
5006
|
+
* Priority rules:
|
|
5007
|
+
* - `exclude` always wins — excluded fields are never shown, even if in `mask`
|
|
5008
|
+
* - `mask` implies visibility — masked fields are shown (with `maskString`) unless excluded
|
|
5009
|
+
*
|
|
5010
|
+
* Limitations:
|
|
5011
|
+
* - Field names containing dots are not supported (indistinguishable from path separators)
|
|
5012
|
+
* - Array structure is not preserved — fields from array elements are merged into a plain object
|
|
5013
|
+
* - When array elements have heterogeneous shapes at the same field path, later elements
|
|
5014
|
+
* overwrite earlier primitives in the preview (e.g. `[{ user: "arb" }, { user: { email: "x" } }]`
|
|
5015
|
+
* produces `{ user: { email: "x" } }` — `"arb"` is lost)
|
|
5016
|
+
*
|
|
5017
|
+
* @example
|
|
5018
|
+
* ```typescript
|
|
5019
|
+
* const preview = buildPreview(order, {
|
|
5020
|
+
* mode: PreviewMode.EXCLUDE_ALL,
|
|
5021
|
+
* include: [{ name: "id" }, { name: "status" }],
|
|
5022
|
+
* mask: [{ name: "email" }],
|
|
5023
|
+
* });
|
|
5024
|
+
* // { id: "order-123", status: "pending", user: { email: "***" } }
|
|
5025
|
+
* ```
|
|
5026
|
+
*
|
|
5027
|
+
* @public
|
|
5028
|
+
*/
|
|
5029
|
+
function buildPreview(value, config) {
|
|
5030
|
+
if (value === null || typeof value !== "object")
|
|
5031
|
+
return undefined;
|
|
5032
|
+
const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
5033
|
+
const maskString = config.maskString ?? "***";
|
|
5034
|
+
const maxBytes = config.maxPreviewBytes ?? 4096;
|
|
5035
|
+
const pairs = [];
|
|
5036
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5037
|
+
function collect(obj, pathPrefix) {
|
|
5038
|
+
if (obj === null || typeof obj !== "object")
|
|
5039
|
+
return;
|
|
5040
|
+
if (Array.isArray(obj)) {
|
|
5041
|
+
for (const item of obj) {
|
|
5042
|
+
collect(item, pathPrefix);
|
|
5043
|
+
}
|
|
5044
|
+
return;
|
|
5045
|
+
}
|
|
5046
|
+
for (const key of Object.keys(obj)) {
|
|
5047
|
+
if (DANGEROUS_KEYS.has(key))
|
|
5048
|
+
continue;
|
|
5049
|
+
if (key.includes("."))
|
|
5050
|
+
continue;
|
|
5051
|
+
const path = pathPrefix ? `${pathPrefix}.${key}` : key;
|
|
5052
|
+
const masked = isMatched(path, config.mask);
|
|
5053
|
+
const excluded = isMatched(path, config.exclude);
|
|
5054
|
+
const visible = !excluded &&
|
|
5055
|
+
(masked ||
|
|
5056
|
+
(config.mode === exports.PreviewMode.INCLUDE_ALL
|
|
5057
|
+
? true
|
|
5058
|
+
: isMatched(path, config.include)));
|
|
5059
|
+
if (!visible) {
|
|
5060
|
+
collect(obj[key], path);
|
|
5061
|
+
continue;
|
|
5062
|
+
}
|
|
5063
|
+
if (masked) {
|
|
5064
|
+
pairs.push([path, maskString]);
|
|
5065
|
+
continue;
|
|
5066
|
+
}
|
|
5067
|
+
if (obj[key] !== null && typeof obj[key] === "object") {
|
|
5068
|
+
collect(obj[key], path);
|
|
5069
|
+
}
|
|
5070
|
+
else {
|
|
5071
|
+
pairs.push([path, obj[key]]);
|
|
5072
|
+
}
|
|
5073
|
+
}
|
|
5074
|
+
}
|
|
5075
|
+
collect(value, "");
|
|
5076
|
+
if (pairs.length === 0)
|
|
5077
|
+
return undefined;
|
|
5078
|
+
const accepted = [];
|
|
5079
|
+
let estimatedSize = 2; // "{}"
|
|
5080
|
+
for (const [path, val] of pairs) {
|
|
5081
|
+
const entrySize = Buffer.byteLength(`"${path}":${JSON.stringify(val)},`, "utf-8");
|
|
5082
|
+
if (estimatedSize + entrySize > maxBytes)
|
|
5083
|
+
break;
|
|
5084
|
+
accepted.push([path, val]);
|
|
5085
|
+
estimatedSize += entrySize;
|
|
5086
|
+
}
|
|
5087
|
+
if (accepted.length === 0)
|
|
5088
|
+
return undefined;
|
|
5089
|
+
const result = {};
|
|
5090
|
+
for (const [path, val] of accepted) {
|
|
5091
|
+
const parts = path.split(".");
|
|
5092
|
+
let node = result;
|
|
5093
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
5094
|
+
if (typeof node[parts[i]] !== "object" || node[parts[i]] === null) {
|
|
5095
|
+
node[parts[i]] = {};
|
|
5096
|
+
}
|
|
5097
|
+
node = node[parts[i]];
|
|
5098
|
+
}
|
|
5099
|
+
node[parts[parts.length - 1]] = val;
|
|
5100
|
+
}
|
|
5101
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
5102
|
+
}
|
|
5103
|
+
|
|
5104
|
+
// Subtract 1KB headroom for the envelope wrapper and other checkpoint metadata
|
|
5105
|
+
const OVERFLOW_THRESHOLD_BYTES = CHECKPOINT_SIZE_LIMIT_BYTES - 1024;
|
|
5106
|
+
/**
|
|
5107
|
+
* Controls when data is written to the filesystem.
|
|
5108
|
+
*
|
|
5109
|
+
* - `ALWAYS`: Every value is written to a file; the checkpoint stores only a file pointer.
|
|
5110
|
+
* Best for consistently large payloads or when you want predictable checkpoint sizes.
|
|
5111
|
+
*
|
|
5112
|
+
* - `OVERFLOW`: Data is written inline (as JSON) unless it exceeds the durable function
|
|
5113
|
+
* checkpoint size limit (~256KB), in which case it overflows to a file.
|
|
5114
|
+
* Best for mixed workloads where most payloads are small.
|
|
5115
|
+
*
|
|
5116
|
+
* @public
|
|
5117
|
+
*/
|
|
5118
|
+
exports.FileSystemSerdesMode = void 0;
|
|
5119
|
+
(function (FileSystemSerdesMode) {
|
|
5120
|
+
FileSystemSerdesMode["ALWAYS"] = "ALWAYS";
|
|
5121
|
+
FileSystemSerdesMode["OVERFLOW"] = "OVERFLOW";
|
|
5122
|
+
})(exports.FileSystemSerdesMode || (exports.FileSystemSerdesMode = {}));
|
|
5123
|
+
async function writeToFile(basePath, value, context) {
|
|
5124
|
+
const dir = node_path.join(basePath, encodeURIComponent(context.durableExecutionArn));
|
|
5125
|
+
await promises.mkdir(dir, { recursive: true });
|
|
5126
|
+
const filePath = node_path.join(dir, `${context.entityId}.json`);
|
|
5127
|
+
await promises.writeFile(filePath, JSON.stringify(value), "utf-8");
|
|
5128
|
+
return filePath;
|
|
5129
|
+
}
|
|
5130
|
+
/**
|
|
5131
|
+
* Creates a Serdes that stores serialized values on a durable filesystem.
|
|
5132
|
+
*
|
|
5133
|
+
* **⚠️ WARNING: Do NOT use with Lambda's ephemeral `/tmp` storage.**
|
|
5134
|
+
* Lambda's `/tmp` filesystem is local to a single execution environment and is
|
|
5135
|
+
* not shared across invocations or function instances. On replay, a different
|
|
5136
|
+
* execution environment may be used and the file will not be found, causing
|
|
5137
|
+
* deserialization to fail.
|
|
5138
|
+
*
|
|
5139
|
+
* **Use only with a durable, shared filesystem such as:**
|
|
5140
|
+
* - **Amazon S3 Files** — mount an S3 bucket as a filesystem via the Lambda console or IaC
|
|
5141
|
+
* - **Amazon EFS** — mount an EFS file system to your Lambda function
|
|
5142
|
+
*
|
|
5143
|
+
* Both options provide persistence across invocations and are accessible from
|
|
5144
|
+
* multiple concurrent function instances, which is required for correct replay behavior.
|
|
5145
|
+
*
|
|
5146
|
+
* The checkpoint stores a JSON envelope that is either:
|
|
5147
|
+
* - `{"data":"<inline JSON>"}` — value stored inline (OVERFLOW mode, under threshold)
|
|
5148
|
+
* - `{"file":"<path>"}` — value stored in a file
|
|
5149
|
+
* - `{"file":"<path>","preview":{...}}` — file pointer with inline preview (when preview is configured)
|
|
5150
|
+
*
|
|
5151
|
+
* @param basePath - Directory path where data files will be stored (e.g. `/mnt/s3` for S3 Files, `/mnt/efs` for EFS)
|
|
5152
|
+
* @param config - Optional configuration options
|
|
5153
|
+
* @returns A Serdes that reads/writes JSON files under basePath
|
|
5154
|
+
*
|
|
5155
|
+
* @example
|
|
5156
|
+
* ```typescript
|
|
5157
|
+
* // Always write to S3 Files mount (default)
|
|
5158
|
+
* context.configureSerdes({
|
|
5159
|
+
* defaultSerdes: createFileSystemSerdes("/mnt/s3"),
|
|
5160
|
+
* });
|
|
5161
|
+
*
|
|
5162
|
+
* // Only overflow to filesystem when payload exceeds ~256KB
|
|
5163
|
+
* context.configureSerdes({
|
|
5164
|
+
* defaultSerdes: createFileSystemSerdes("/mnt/s3", { storageMode: FileSystemSerdesMode.OVERFLOW }),
|
|
5165
|
+
* });
|
|
5166
|
+
*
|
|
5167
|
+
* // With preview: show id and masked email in checkpoint
|
|
5168
|
+
* context.configureSerdes({
|
|
5169
|
+
* defaultSerdes: createFileSystemSerdes("/mnt/s3", {
|
|
5170
|
+
* generatePreview: (value) => buildPreview(value, {
|
|
5171
|
+
* mode: PreviewMode.EXCLUDE_ALL,
|
|
5172
|
+
* include: [{ name: "id" }, { name: "status" }],
|
|
5173
|
+
* mask: [{ name: "email" }],
|
|
5174
|
+
* }),
|
|
5175
|
+
* }),
|
|
5176
|
+
* });
|
|
5177
|
+
* ```
|
|
5178
|
+
*
|
|
5179
|
+
* Limitations:
|
|
5180
|
+
* - Field names containing dots are not supported in preview field selectors.
|
|
5181
|
+
* A dot in a field name is indistinguishable from a path separator.
|
|
5182
|
+
* - Array structure is not preserved in preview output — fields from array
|
|
5183
|
+
* elements are merged into a plain object at the array's path.
|
|
5184
|
+
*
|
|
5185
|
+
* @public
|
|
5186
|
+
*/
|
|
5187
|
+
function createFileSystemSerdes(basePath, config = {}) {
|
|
5188
|
+
const storageMode = config.storageMode ?? exports.FileSystemSerdesMode.ALWAYS;
|
|
5189
|
+
return {
|
|
5190
|
+
serialize: async (
|
|
5191
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5192
|
+
value, context) => {
|
|
5193
|
+
if (value === undefined)
|
|
5194
|
+
return undefined;
|
|
5195
|
+
if (storageMode === exports.FileSystemSerdesMode.ALWAYS) {
|
|
5196
|
+
const filePath = await writeToFile(basePath, value, context);
|
|
5197
|
+
const preview = config.generatePreview?.(value);
|
|
5198
|
+
const envelope = preview
|
|
5199
|
+
? { file: filePath, preview }
|
|
5200
|
+
: { file: filePath };
|
|
5201
|
+
return JSON.stringify(envelope);
|
|
5202
|
+
}
|
|
5203
|
+
// OVERFLOW mode: serialize inline first, overflow to file if too large
|
|
5204
|
+
const inlineJson = JSON.stringify(value);
|
|
5205
|
+
if (Buffer.byteLength(inlineJson, "utf-8") > OVERFLOW_THRESHOLD_BYTES) {
|
|
5206
|
+
const filePath = await writeToFile(basePath, value, context);
|
|
5207
|
+
const preview = config.generatePreview?.(value);
|
|
5208
|
+
const envelope = preview
|
|
5209
|
+
? { file: filePath, preview }
|
|
5210
|
+
: { file: filePath };
|
|
5211
|
+
return JSON.stringify(envelope);
|
|
5212
|
+
}
|
|
5213
|
+
return JSON.stringify({ data: inlineJson });
|
|
5214
|
+
},
|
|
5215
|
+
deserialize: async (data, _context) => {
|
|
5216
|
+
if (data === undefined)
|
|
5217
|
+
return undefined;
|
|
5218
|
+
const envelope = JSON.parse(data);
|
|
5219
|
+
if ("file" in envelope) {
|
|
5220
|
+
const contents = await promises.readFile(envelope.file, "utf-8");
|
|
5221
|
+
return JSON.parse(contents);
|
|
5222
|
+
}
|
|
5223
|
+
return JSON.parse(envelope.data);
|
|
5224
|
+
},
|
|
5225
|
+
};
|
|
5226
|
+
}
|
|
5227
|
+
|
|
4707
5228
|
const DEFAULT_CONFIG = {
|
|
4708
5229
|
maxAttempts: 60,
|
|
4709
5230
|
initialDelay: { seconds: 5 },
|
|
@@ -4753,7 +5274,46 @@ const createWaitStrategy = (config) => {
|
|
|
4753
5274
|
};
|
|
4754
5275
|
};
|
|
4755
5276
|
|
|
5277
|
+
async function withRetry(context, nameOrFunc, funcOrConfig, maybeConfig) {
|
|
5278
|
+
const hasName = typeof nameOrFunc === "string";
|
|
5279
|
+
const name = hasName ? nameOrFunc : undefined;
|
|
5280
|
+
const func = (hasName ? funcOrConfig : nameOrFunc);
|
|
5281
|
+
const config = (hasName ? maybeConfig : funcOrConfig);
|
|
5282
|
+
if (!config) {
|
|
5283
|
+
throw new TypeError("withRetry: config is required");
|
|
5284
|
+
}
|
|
5285
|
+
const { retryStrategy, wrapWithRunInChildContext = true, childContextConfig, } = config;
|
|
5286
|
+
const runLoop = async (ctx) => {
|
|
5287
|
+
let attempt = 0;
|
|
5288
|
+
while (true) {
|
|
5289
|
+
attempt++;
|
|
5290
|
+
try {
|
|
5291
|
+
return await func(ctx, attempt);
|
|
5292
|
+
}
|
|
5293
|
+
catch (err) {
|
|
5294
|
+
const decision = retryStrategy(err, attempt);
|
|
5295
|
+
if (!decision.shouldRetry)
|
|
5296
|
+
throw err;
|
|
5297
|
+
const delay = decision.delay ?? { seconds: 1 };
|
|
5298
|
+
if (name) {
|
|
5299
|
+
await ctx.wait(`${name}-backoff-${attempt}`, delay);
|
|
5300
|
+
}
|
|
5301
|
+
else {
|
|
5302
|
+
await ctx.wait(delay);
|
|
5303
|
+
}
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5306
|
+
};
|
|
5307
|
+
return wrapWithRunInChildContext
|
|
5308
|
+
? name
|
|
5309
|
+
? await context.runInChildContext(name, runLoop, childContextConfig)
|
|
5310
|
+
: await context.runInChildContext(runLoop, childContextConfig)
|
|
5311
|
+
: await runLoop(context);
|
|
5312
|
+
}
|
|
5313
|
+
|
|
4756
5314
|
exports.CallbackError = CallbackError;
|
|
5315
|
+
exports.CallbackSubmitterError = CallbackSubmitterError;
|
|
5316
|
+
exports.CallbackTimeoutError = CallbackTimeoutError;
|
|
4757
5317
|
exports.ChildContextError = ChildContextError;
|
|
4758
5318
|
exports.DurableExecutionApiClient = DurableExecutionApiClient;
|
|
4759
5319
|
exports.DurableExecutionInvocationInputWithClient = DurableExecutionInvocationInputWithClient;
|
|
@@ -4763,11 +5323,15 @@ exports.InvokeError = InvokeError;
|
|
|
4763
5323
|
exports.StepError = StepError;
|
|
4764
5324
|
exports.StepInterruptedError = StepInterruptedError;
|
|
4765
5325
|
exports.WaitForConditionError = WaitForConditionError;
|
|
5326
|
+
exports.buildPreview = buildPreview;
|
|
4766
5327
|
exports.createClassSerdes = createClassSerdes;
|
|
4767
5328
|
exports.createClassSerdesWithDates = createClassSerdesWithDates;
|
|
5329
|
+
exports.createFileSystemSerdes = createFileSystemSerdes;
|
|
5330
|
+
exports.createLinearRetryStrategy = createLinearRetryStrategy;
|
|
4768
5331
|
exports.createRetryStrategy = createRetryStrategy;
|
|
4769
5332
|
exports.createWaitStrategy = createWaitStrategy;
|
|
4770
5333
|
exports.defaultSerdes = defaultSerdes;
|
|
4771
5334
|
exports.retryPresets = retryPresets;
|
|
4772
5335
|
exports.withDurableExecution = withDurableExecution;
|
|
5336
|
+
exports.withRetry = withRetry;
|
|
4773
5337
|
//# sourceMappingURL=index.js.map
|