@aws/durable-execution-sdk-js 1.1.1 → 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/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 (
|
|
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 ||
|
|
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 ||
|
|
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 ||
|
|
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 ||
|
|
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
|
-
|
|
1593
|
-
|
|
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
|
|
1606
|
-
const durableChildContext = createChildContext(context, parentContext, childReplayMode, getParentLogger(), 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") >
|
|
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:
|
|
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
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
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 ||
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
|
2174
|
-
heartbeatTimeout: config
|
|
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 ??
|
|
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
|
-
|
|
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
|
|
2465
|
-
return await
|
|
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
|
|
2472
|
-
return await
|
|
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
|
|
2482
|
-
return await
|
|
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
|
|
2489
|
-
return await
|
|
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
|
-
|
|
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 ||
|
|
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), {
|
|
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), {
|
|
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) =>
|
|
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.
|
|
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
|
-
*
|
|
4248
|
-
*
|
|
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
|
|
4256
|
-
const
|
|
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
|
-
|
|
4701
|
-
|
|
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
|
-
|
|
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
|