Skip to content

Commit 10ad3cc

Browse files
ParidelPooyaPooya Paridel
andcommitted
feat(sdk): Add naming support for map and parallel operations (#93)
*Description of changes:* Add support for custom naming in parallel branches and map iterations to improve debugging and monitoring capabilities. ## New Features ### Named Parallel Branches - Add NamedParallelBranch interface with optional name and func properties - Support mixed named and unnamed branches in parallel operations ### Map Item Naming - Add itemNamer function to MapConfig for custom map item names - itemNamer receives (item, index) parameters for flexible naming ### Enhanced Operation Tracking - Add optional name field to ConcurrentExecutionItem interface - Pass item names to runInChildContext for better operation hierarchy - Include item names in ConcurrencyController logging ## Usage Examples ### Named Parallel Branches: ```typescript await context.parallel([ { name: 'fetch-user', func: async (ctx) => fetchUser() }, { name: 'fetch-posts', func: async (ctx) => fetchPosts() } ]); ``` ### Custom Map Item Names: ```typescript await context.map(users, processUser, { itemNamer: (user, index) => `User-${user.id || index}` }); ``` ## Testing - Add unit tests for all naming scenarios - Test named, unnamed, and mixed branch configurations - Verify itemNamer parameter passing and fallback behavior - Ensure backward compatibility with existing code By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. Co-authored-by: Pooya Paridel <parpooya@amazon.com>
1 parent d7a1b4f commit 10ad3cc

File tree

9 files changed

+456
-119
lines changed

9 files changed

+456
-119
lines changed

packages/aws-durable-execution-sdk-js/bundle-size-history.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
11
[
2-
{
3-
"timestamp": "2025-07-10T01:35:30.375Z",
4-
"size": 891887,
5-
"gitCommit": "ee000fbc8653964a49a894906b8741272288490f"
6-
},
72
{
83
"timestamp": "2025-07-10T02:01:22.836Z",
94
"size": 891900,
@@ -248,5 +243,10 @@
248243
"timestamp": "2025-10-02T19:56:14.710Z",
249244
"size": 357050,
250245
"gitCommit": "7934b1ddf1b32907d5a20f1995a49bfa22845821"
246+
},
247+
{
248+
"timestamp": "2025-10-02T22:13:20.410Z",
249+
"size": 357303,
250+
"gitCommit": "678d972dbbde52016b0dde14c31dff7ebcafaef2"
251251
}
252-
]
252+
]

packages/aws-durable-execution-sdk-js/src/context/durable-context/durable-context.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
MapConfig,
1313
ParallelFunc,
1414
ParallelConfig,
15+
NamedParallelBranch,
1516
ConcurrentExecutionItem,
1617
ConcurrentExecutor,
1718
ConcurrencyConfig,
@@ -157,11 +158,11 @@ export const createDurableContext = (
157158
);
158159
};
159160

160-
const map: DurableContext["map"] = <T>(
161-
nameOrItems: string | undefined | any[],
162-
itemsOrMapFunc: any[] | MapFunc<T>,
163-
mapFuncOrConfig?: MapFunc<T> | MapConfig,
164-
maybeConfig?: MapConfig,
161+
const map: DurableContext["map"] = <TInput, TOutput>(
162+
nameOrItems: string | undefined | TInput[],
163+
itemsOrMapFunc: TInput[] | MapFunc<TInput, TOutput>,
164+
mapFuncOrConfig?: MapFunc<TInput, TOutput> | MapConfig<TInput>,
165+
maybeConfig?: MapConfig<TInput>,
165166
) => {
166167
const mapHandler = createMapHandler(executionContext, executeConcurrently);
167168
return mapHandler(
@@ -173,8 +174,13 @@ export const createDurableContext = (
173174
};
174175

175176
const parallel: DurableContext["parallel"] = <T>(
176-
nameOrBranches: string | undefined | ParallelFunc<T>[],
177-
branchesOrConfig?: ParallelFunc<T>[] | ParallelConfig,
177+
nameOrBranches:
178+
| string
179+
| undefined
180+
| (ParallelFunc<T> | NamedParallelBranch<T>)[],
181+
branchesOrConfig?:
182+
| (ParallelFunc<T> | NamedParallelBranch<T>)[]
183+
| ParallelConfig,
178184
maybeConfig?: ParallelConfig,
179185
) => {
180186
const parallelHandler = createParallelHandler(

packages/aws-durable-execution-sdk-js/src/handlers/concurrent-execution-handler/concurrent-execution-handler.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface ConcurrentExecutionItem<T> {
99
id: string;
1010
data: T;
1111
index: number;
12+
name?: string;
1213
}
1314

1415
/**
@@ -141,11 +142,12 @@ export class ConcurrencyController {
141142
log(this.isVerbose, "▶️", `Starting ${this.operationName} item:`, {
142143
index,
143144
itemId: item.id,
145+
itemName: item.name,
144146
});
145147

146148
parentContext
147149
.runInChildContext(
148-
item.id,
150+
item.name || item.id,
149151
(childContext) => executor(item, childContext),
150152
{ subType: config.iterationSubType },
151153
)
@@ -164,6 +166,7 @@ export class ConcurrencyController {
164166
{
165167
index,
166168
itemId: item.id,
169+
itemName: item.name,
167170
},
168171
);
169172
onComplete();
@@ -184,6 +187,7 @@ export class ConcurrencyController {
184187
{
185188
index,
186189
itemId: item.id,
190+
itemName: item.name,
187191
error: err.message,
188192
},
189193
);

packages/aws-durable-execution-sdk-js/src/handlers/map-handler/map-handler.test.ts

Lines changed: 132 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ describe("Map Handler", () => {
3030
describe("parameter parsing", () => {
3131
it("should parse parameters with name", async () => {
3232
const items = ["item1", "item2"];
33-
const mapFunc: MapFunc<string> = jest.fn().mockResolvedValue("result");
33+
const mapFunc: MapFunc<string, string> = jest
34+
.fn()
35+
.mockResolvedValue("result");
3436

3537
const mockResult = new MockBatchResult([
3638
{ index: 0, result: "result1", status: BatchItemStatus.SUCCEEDED },
@@ -43,8 +45,8 @@ describe("Map Handler", () => {
4345
expect(mockExecuteConcurrently).toHaveBeenCalledWith(
4446
"test-map",
4547
[
46-
{ id: "map-item-0", data: "item1", index: 0 },
47-
{ id: "map-item-1", data: "item2", index: 1 },
48+
{ id: "map-item-0", data: "item1", index: 0, name: undefined },
49+
{ id: "map-item-1", data: "item2", index: 1, name: undefined },
4850
],
4951
expect.any(Function),
5052
{
@@ -61,7 +63,9 @@ describe("Map Handler", () => {
6163

6264
it("should parse parameters without name", async () => {
6365
const items = ["item1", "item2"];
64-
const mapFunc: MapFunc<string> = jest.fn().mockResolvedValue("result");
66+
const mapFunc: MapFunc<string, string> = jest
67+
.fn()
68+
.mockResolvedValue("result");
6569

6670
const mockResult = new MockBatchResult([
6771
{ index: 0, result: "result1", status: BatchItemStatus.SUCCEEDED },
@@ -74,8 +78,8 @@ describe("Map Handler", () => {
7478
expect(mockExecuteConcurrently).toHaveBeenCalledWith(
7579
undefined,
7680
[
77-
{ id: "map-item-0", data: "item1", index: 0 },
78-
{ id: "map-item-1", data: "item2", index: 1 },
81+
{ id: "map-item-0", data: "item1", index: 0, name: undefined },
82+
{ id: "map-item-1", data: "item2", index: 1, name: undefined },
7983
],
8084
expect.any(Function),
8185
{
@@ -88,7 +92,7 @@ describe("Map Handler", () => {
8892

8993
it("should accept undefined as name parameter", async () => {
9094
const items = ["item"];
91-
const mapFunc: MapFunc<string> = jest.fn();
95+
const mapFunc: MapFunc<string, string> = jest.fn();
9296

9397
const mockResult = new MockBatchResult([
9498
{ index: 0, result: "result", status: BatchItemStatus.SUCCEEDED },
@@ -99,7 +103,7 @@ describe("Map Handler", () => {
99103

100104
expect(mockExecuteConcurrently).toHaveBeenCalledWith(
101105
undefined,
102-
[{ id: "map-item-0", data: "item", index: 0 }],
106+
[{ id: "map-item-0", data: "item", index: 0, name: undefined }],
103107
expect.any(Function),
104108
{
105109
...TEST_CONSTANTS.DEFAULT_MAP_CONFIG,
@@ -111,7 +115,9 @@ describe("Map Handler", () => {
111115

112116
it("should parse parameters with config", async () => {
113117
const items = ["item1", "item2"];
114-
const mapFunc: MapFunc<string> = jest.fn().mockResolvedValue("result");
118+
const mapFunc: MapFunc<string, string> = jest
119+
.fn()
120+
.mockResolvedValue("result");
115121
const config = {
116122
...{
117123
...TEST_CONSTANTS.DEFAULT_MAP_CONFIG,
@@ -132,8 +138,8 @@ describe("Map Handler", () => {
132138
expect(mockExecuteConcurrently).toHaveBeenCalledWith(
133139
undefined,
134140
[
135-
{ id: "map-item-0", data: "item1", index: 0 },
136-
{ id: "map-item-1", data: "item2", index: 1 },
141+
{ id: "map-item-0", data: "item1", index: 0, name: undefined },
142+
{ id: "map-item-1", data: "item2", index: 1, name: undefined },
137143
],
138144
expect.any(Function),
139145
{
@@ -150,7 +156,7 @@ describe("Map Handler", () => {
150156

151157
describe("validation", () => {
152158
it("should throw error for non-array items", async () => {
153-
const mapFunc: MapFunc<string> = jest.fn();
159+
const mapFunc: MapFunc<string, string> = jest.fn();
154160

155161
await expect(mapHandler("not-an-array" as any, mapFunc)).rejects.toThrow(
156162
"Map operation requires an array of items",
@@ -169,7 +175,7 @@ describe("Map Handler", () => {
169175
describe("execution", () => {
170176
it("should handle empty array", async () => {
171177
const items: string[] = [];
172-
const mapFunc: MapFunc<string> = jest.fn();
178+
const mapFunc: MapFunc<string, string> = jest.fn();
173179

174180
const mockResult = new MockBatchResult([]);
175181
mockExecuteConcurrently.mockResolvedValue(mockResult as any);
@@ -191,7 +197,9 @@ describe("Map Handler", () => {
191197

192198
it("should create correct execution items", async () => {
193199
const items = ["item1", "item2", "item3"];
194-
const mapFunc: MapFunc<string> = jest.fn().mockResolvedValue("result");
200+
const mapFunc: MapFunc<string, string> = jest
201+
.fn()
202+
.mockResolvedValue("result");
195203

196204
const mockResult = new MockBatchResult([
197205
{ index: 0, result: "result1", status: BatchItemStatus.SUCCEEDED },
@@ -205,9 +213,9 @@ describe("Map Handler", () => {
205213
expect(mockExecuteConcurrently).toHaveBeenCalledWith(
206214
undefined,
207215
[
208-
{ id: "map-item-0", data: "item1", index: 0 },
209-
{ id: "map-item-1", data: "item2", index: 1 },
210-
{ id: "map-item-2", data: "item3", index: 2 },
216+
{ id: "map-item-0", data: "item1", index: 0, name: undefined },
217+
{ id: "map-item-1", data: "item2", index: 1, name: undefined },
218+
{ id: "map-item-2", data: "item3", index: 2, name: undefined },
211219
],
212220
expect.any(Function),
213221
{
@@ -220,7 +228,7 @@ describe("Map Handler", () => {
220228

221229
it("should return BatchResult with correct structure", async () => {
222230
const items = ["item1", "item2"];
223-
const mapFunc: MapFunc<string> = jest
231+
const mapFunc: MapFunc<string, string> = jest
224232
.fn()
225233
.mockResolvedValueOnce("result1")
226234
.mockResolvedValueOnce("result2");
@@ -241,7 +249,7 @@ describe("Map Handler", () => {
241249

242250
it("should create executor that calls mapFunc correctly", async () => {
243251
const items = ["item1", "item2"];
244-
const mapFunc: MapFunc<string> = jest
252+
const mapFunc: MapFunc<string, string> = jest
245253
.fn()
246254
.mockResolvedValueOnce("result1")
247255
.mockResolvedValueOnce("result2");
@@ -284,7 +292,9 @@ describe("Map Handler", () => {
284292

285293
it("should pass through maxConcurrency config", async () => {
286294
const items = ["item1", "item2"];
287-
const mapFunc: MapFunc<string> = jest.fn().mockResolvedValue("result");
295+
const mapFunc: MapFunc<string, string> = jest
296+
.fn()
297+
.mockResolvedValue("result");
288298
const config = {
289299
...{
290300
...TEST_CONSTANTS.DEFAULT_MAP_CONFIG,
@@ -309,5 +319,107 @@ describe("Map Handler", () => {
309319
config,
310320
);
311321
});
322+
323+
describe("itemNamer functionality", () => {
324+
it("should use custom itemNamer when provided", async () => {
325+
const items = [
326+
{ id: "user1", name: "Alice" },
327+
{ id: "user2", name: "Bob" },
328+
];
329+
const mapFunc: MapFunc<{ id: string; name: string }, string> = jest
330+
.fn()
331+
.mockResolvedValue("processed");
332+
const itemNamer = (item: any, index: number) => `User-${item.id}`;
333+
334+
const mockResult = new MockBatchResult([
335+
{ index: 0, result: "processed", status: BatchItemStatus.SUCCEEDED },
336+
{ index: 1, result: "processed", status: BatchItemStatus.SUCCEEDED },
337+
]);
338+
mockExecuteConcurrently.mockResolvedValue(mockResult as any);
339+
340+
await mapHandler(items, mapFunc, { itemNamer });
341+
342+
expect(mockExecuteConcurrently).toHaveBeenCalledWith(
343+
undefined,
344+
[
345+
{ id: "map-item-0", data: items[0], index: 0, name: "User-user1" },
346+
{ id: "map-item-1", data: items[1], index: 1, name: "User-user2" },
347+
],
348+
expect.any(Function),
349+
{
350+
...TEST_CONSTANTS.DEFAULT_MAP_CONFIG,
351+
summaryGenerator: expect.any(Function),
352+
completionConfig: undefined,
353+
},
354+
);
355+
});
356+
357+
it("should use undefined names when itemNamer is not provided", async () => {
358+
const items = ["item1", "item2"];
359+
const mapFunc: MapFunc<string, string> = jest
360+
.fn()
361+
.mockResolvedValue("processed");
362+
363+
const mockResult = new MockBatchResult([
364+
{ index: 0, result: "processed", status: BatchItemStatus.SUCCEEDED },
365+
{ index: 1, result: "processed", status: BatchItemStatus.SUCCEEDED },
366+
]);
367+
mockExecuteConcurrently.mockResolvedValue(mockResult as any);
368+
369+
await mapHandler(items, mapFunc);
370+
371+
expect(mockExecuteConcurrently).toHaveBeenCalledWith(
372+
undefined,
373+
[
374+
{ id: "map-item-0", data: "item1", index: 0, name: undefined },
375+
{ id: "map-item-1", data: "item2", index: 1, name: undefined },
376+
],
377+
expect.any(Function),
378+
{
379+
...TEST_CONSTANTS.DEFAULT_MAP_CONFIG,
380+
summaryGenerator: expect.any(Function),
381+
completionConfig: undefined,
382+
},
383+
);
384+
});
385+
386+
it("should pass item and index to itemNamer", async () => {
387+
const items = ["a", "b", "c"];
388+
const mapFunc: MapFunc<string, string> = jest
389+
.fn()
390+
.mockResolvedValue("processed");
391+
const itemNamer = jest.fn(
392+
(item: string, index: number) => `${item}-${index}`,
393+
);
394+
395+
const mockResult = new MockBatchResult([
396+
{ index: 0, result: "processed", status: BatchItemStatus.SUCCEEDED },
397+
{ index: 1, result: "processed", status: BatchItemStatus.SUCCEEDED },
398+
{ index: 2, result: "processed", status: BatchItemStatus.SUCCEEDED },
399+
]);
400+
mockExecuteConcurrently.mockResolvedValue(mockResult as any);
401+
402+
await mapHandler(items, mapFunc, { itemNamer });
403+
404+
expect(itemNamer).toHaveBeenCalledWith("a", 0);
405+
expect(itemNamer).toHaveBeenCalledWith("b", 1);
406+
expect(itemNamer).toHaveBeenCalledWith("c", 2);
407+
408+
expect(mockExecuteConcurrently).toHaveBeenCalledWith(
409+
undefined,
410+
[
411+
{ id: "map-item-0", data: "a", index: 0, name: "a-0" },
412+
{ id: "map-item-1", data: "b", index: 1, name: "b-1" },
413+
{ id: "map-item-2", data: "c", index: 2, name: "c-2" },
414+
],
415+
expect.any(Function),
416+
{
417+
...TEST_CONSTANTS.DEFAULT_MAP_CONFIG,
418+
summaryGenerator: expect.any(Function),
419+
completionConfig: undefined,
420+
},
421+
);
422+
});
423+
});
312424
});
313425
});

0 commit comments

Comments
 (0)