ai 7.0.139 → 7.0.140

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.
@@ -881,3 +881,23 @@ Other options carry over with the same names: `prepareStep`, `onStart`, `onStepS
881
881
  - [WorkflowChatTransport API Reference](/docs/reference/ai-sdk-workflow/workflow-chat-transport) for stream reconnection options
882
882
  - [Building Agents](/docs/agents/building-agents) for the in-memory `ToolLoopAgent` alternative
883
883
  - [Loop Control](/docs/agents/loop-control) for advanced stop conditions
884
+
885
+ ## Tool execution concurrency
886
+
887
+ Set `toolCallConcurrency` to limit how many local tool calls execute concurrently
888
+ within a step. Use `1` when calls must execute sequentially in model order:
889
+
890
+ ```ts
891
+ const agent = new WorkflowAgent({
892
+ model,
893
+ tools,
894
+ toolCallConcurrency: 1,
895
+ });
896
+ ```
897
+
898
+ The limit applies to both `generate()` and `stream()`, including approved tool
899
+ calls resumed from message history. It must be a positive integer. By default,
900
+ all local calls execute concurrently; provider-executed tools are unaffected.
901
+ You can also return the setting from `prepareCall` or pass it to `generate()` or
902
+ `stream()`. Explicit call options take precedence over `prepareCall`, which takes
903
+ precedence over the constructor default.
@@ -511,6 +511,35 @@ const result = await generateText({
511
511
  });
512
512
  ```
513
513
 
514
+ ### Tool call execution concurrency
515
+
516
+ By default, when a model generates multiple tool calls in one step,
517
+ `generateText`, `streamText`, and `ToolLoopAgent` execute those calls
518
+ concurrently. Set `toolCallConcurrency` to a positive integer to limit how many
519
+ SDK-executed tool calls run at once.
520
+
521
+ Use `toolCallConcurrency: 1` when later calls need to observe side effects from
522
+ earlier calls. The SDK starts calls sequentially in the order the model
523
+ generated them:
524
+
525
+ ```ts highlight="5"
526
+ const result = await generateText({
527
+ model: __MODEL__,
528
+ tools,
529
+ prompt: 'Update the file, then read it.',
530
+ toolCallConcurrency: 1,
531
+ });
532
+ ```
533
+
534
+ This option controls execution of tool calls that the SDK runs locally. It does
535
+ not change how many tool calls the model may generate, and it does not schedule
536
+ provider-executed tools or client-side tools. Provider options such as
537
+ `parallelToolCalls` control model generation instead.
538
+
539
+ When the operation is aborted, active tools receive the abort signal and queued
540
+ tool calls are not started. A streaming tool holds its concurrency slot until
541
+ its final output or error.
542
+
514
543
  ### Tool execution lifecycle callbacks
515
544
 
516
545
  You can use `onToolExecutionStart` and `onToolExecutionEnd` to observe tool execution.
@@ -571,6 +571,13 @@ To see `generateText` in action, check out [these examples](#examples).
571
571
  description:
572
572
  'Controls the order in which tools are sent to the provider. The list can be partial. Tools not listed in `toolOrder` are sent after the listed tools, sorted alphabetically. Tool names are restricted to the string keys of the tool set.',
573
573
  },
574
+ {
575
+ name: 'toolCallConcurrency',
576
+ type: 'number',
577
+ isOptional: true,
578
+ description:
579
+ 'Maximum number of tool calls that the SDK executes concurrently within a step. Set to `1` to execute calls sequentially in model-generated order. Must be a positive integer. By default, all tool calls execute concurrently.',
580
+ },
574
581
  {
575
582
  name: 'toolApproval',
576
583
  type: 'ToolApprovalConfiguration<TOOLS, RUNTIME_CONTEXT>',
@@ -622,6 +622,13 @@ To see `streamText` in action, check out [these examples](#examples).
622
622
  description:
623
623
  'Controls the order in which tools are sent to the provider. The list can be partial. Tools not listed in `toolOrder` are sent after the listed tools, sorted alphabetically. Tool names are restricted to the string keys of the tool set.',
624
624
  },
625
+ {
626
+ name: 'toolCallConcurrency',
627
+ type: 'number',
628
+ isOptional: true,
629
+ description:
630
+ 'Maximum number of tool calls that the SDK executes concurrently within a step. Set to `1` to execute calls sequentially in model-generated order. Must be a positive integer. By default, all tool calls execute concurrently.',
631
+ },
625
632
  {
626
633
  name: 'toolApproval',
627
634
  type: 'ToolApprovalConfiguration<TOOLS, RUNTIME_CONTEXT>',
@@ -104,6 +104,13 @@ To see `ToolLoopAgent` in action, check out [these examples](#examples).
104
104
  description:
105
105
  'Controls the order in which tools are sent to the provider. The list can be partial. Tools not listed in `toolOrder` are sent after the listed tools, sorted alphabetically. Tool names are restricted to the string keys of the tool set.',
106
106
  },
107
+ {
108
+ name: 'toolCallConcurrency',
109
+ type: 'number',
110
+ isOptional: true,
111
+ description:
112
+ 'Maximum number of tool calls that the SDK executes concurrently within an agent step. Set to `1` to execute calls sequentially in model-generated order. Must be a positive integer. By default, all tool calls execute concurrently.',
113
+ },
107
114
  {
108
115
  name: 'toolApproval',
109
116
  type: 'ToolApprovalConfiguration<TOOLS, RUNTIME_CONTEXT>',
@@ -101,6 +101,13 @@ To see `WorkflowAgent` in action, check out [these examples](#examples).
101
101
  description:
102
102
  'Default stop condition for the agent loop. When omitted, generate defaults to 20 steps, while stream has no maximum step count. Use `isStepCount()` to bound execution. Per-call values override this default.',
103
103
  },
104
+ {
105
+ name: 'toolCallConcurrency',
106
+ type: 'number',
107
+ isOptional: true,
108
+ description:
109
+ 'Maximum number of local tool calls executing concurrently within a step, including approved calls. Must be a positive integer. Set to 1 for sequential execution in model order. Defaults to unlimited concurrency. Can be changed by prepareCall; explicit generate/stream call options take precedence. Provider-executed tools are unaffected.',
110
+ },
104
111
  {
105
112
  name: 'activeTools',
106
113
  type: 'ActiveTools<TTools>',
@@ -569,6 +576,13 @@ const result = await agent.stream({
569
576
  isOptional: true,
570
577
  description: "Override the tool choice strategy for this call. Default: 'auto'.",
571
578
  },
579
+ {
580
+ name: 'toolCallConcurrency',
581
+ type: 'number',
582
+ isOptional: true,
583
+ description:
584
+ 'Override the local tool call concurrency limit for this call. Must be a positive integer. Set to 1 for sequential execution in model order, including approved calls. Takes precedence over the constructor and prepareCall.',
585
+ },
572
586
  {
573
587
  name: 'activeTools',
574
588
  type: 'ActiveTools<TTools>',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "7.0.139",
3
+ "version": "7.0.140",
4
4
  "type": "module",
5
5
  "description": "AI SDK by Vercel - build apps like ChatGPT, Claude, Gemini, and more with a single interface for any model using the Vercel AI Gateway or go direct to OpenAI, Anthropic, Google, or any other model provider.",
6
6
  "license": "Apache-2.0",
@@ -42,7 +42,7 @@
42
42
  }
43
43
  },
44
44
  "dependencies": {
45
- "@ai-sdk/gateway": "4.0.112",
45
+ "@ai-sdk/gateway": "4.0.113",
46
46
  "@ai-sdk/provider": "4.0.27",
47
47
  "@ai-sdk/provider-utils": "5.0.59"
48
48
  },
@@ -121,6 +121,14 @@ export type ToolLoopAgentSettings<
121
121
  */
122
122
  toolOrder?: ToolOrder<NoInfer<TOOLS>>;
123
123
 
124
+ /**
125
+ * Maximum number of tool calls that may execute concurrently within a step.
126
+ *
127
+ * Set to `1` to execute tool calls sequentially in the order they were
128
+ * generated. By default, all tool calls execute concurrently.
129
+ */
130
+ toolCallConcurrency?: number;
131
+
124
132
  /**
125
133
  * Optional specification for generating structured outputs.
126
134
  */
@@ -370,6 +378,7 @@ export type ToolLoopAgentSettings<
370
378
  | 'experimental_telemetry'
371
379
  | 'activeTools'
372
380
  | 'toolOrder'
381
+ | 'toolCallConcurrency'
373
382
  | 'toolApproval'
374
383
  | 'experimental_toolCallers'
375
384
  | 'experimental_toolApprovalSecret'
@@ -412,6 +421,7 @@ export type ToolLoopAgentSettings<
412
421
  | 'experimental_telemetry'
413
422
  | 'activeTools'
414
423
  | 'toolOrder'
424
+ | 'toolCallConcurrency'
415
425
  | 'toolApproval'
416
426
  | 'experimental_toolCallers'
417
427
  | 'experimental_toolApprovalSecret'
@@ -50,12 +50,15 @@ export function collectToolApprovals<TOOLS extends ToolSet>({
50
50
  string,
51
51
  TypedToolCall<TOOLS>
52
52
  > = Object.create(null);
53
+ const toolCallOrder: Record<string, number> = Object.create(null);
54
+ let nextToolCallIndex = 0;
53
55
  for (const message of messages) {
54
56
  if (message.role === 'assistant' && typeof message.content !== 'string') {
55
57
  const content = message.content;
56
58
  for (const part of content) {
57
59
  if (part.type === 'tool-call') {
58
60
  toolCallsByToolCallId[part.toolCallId] = part as TypedToolCall<TOOLS>;
61
+ toolCallOrder[part.toolCallId] = nextToolCallIndex++;
59
62
  }
60
63
  }
61
64
  }
@@ -130,5 +133,13 @@ export function collectToolApprovals<TOOLS extends ToolSet>({
130
133
  }
131
134
  }
132
135
 
136
+ // Approval responses can arrive in a different order than the model's calls.
137
+ // Restore model order before either generation API schedules their execution.
138
+ approvedToolApprovals.sort(
139
+ (a, b) =>
140
+ toolCallOrder[a.toolCall.toolCallId] -
141
+ toolCallOrder[b.toolCall.toolCallId],
142
+ );
143
+
133
144
  return { approvedToolApprovals, deniedToolApprovals };
134
145
  }
@@ -26,6 +26,7 @@ import type {
26
26
  OnToolExecutionStartCallback,
27
27
  } from './tool-execution-events';
28
28
  import type { StaticToolOutputDenied } from './tool-output-denied';
29
+ import { mapWithConcurrency } from './tool-call-concurrency';
29
30
 
30
31
  export type ToolExecutionEndStreamPart = {
31
32
  type: 'tool-execution-end';
@@ -63,6 +64,7 @@ export function executeToolsFromStream<
63
64
  onToolExecutionEnd,
64
65
  executeToolInTelemetryContext,
65
66
  runInTracingChannelSpan,
67
+ toolCallConcurrency,
66
68
  }: {
67
69
  stream: ReadableStream<ExecuteToolsInputStreamPart<TOOLS>>;
68
70
  tools: TOOLS | undefined;
@@ -82,6 +84,7 @@ export function executeToolsFromStream<
82
84
  runInTracingChannelSpan?: NonNullable<
83
85
  TelemetryDispatcher['runInTracingChannelSpan']
84
86
  >;
87
+ toolCallConcurrency?: number;
85
88
  }): ReadableStream<ExecuteToolsStreamPart<TOOLS>> {
86
89
  const toolCallsToExecute: Array<TypedToolCall<TOOLS>> = [];
87
90
 
@@ -226,8 +229,11 @@ export function executeToolsFromStream<
226
229
  return;
227
230
  }
228
231
 
229
- await Promise.all(
230
- toolCallsToExecute.map(async toolCall => {
232
+ await mapWithConcurrency({
233
+ items: toolCallsToExecute,
234
+ concurrency: toolCallConcurrency,
235
+ abortSignal,
236
+ execute: async toolCall => {
231
237
  try {
232
238
  // Note: we don't await the tool execution here (by leaving out 'await' on recordSpan),
233
239
  // because we want to process the next chunk as soon as possible.
@@ -263,8 +269,8 @@ export function executeToolsFromStream<
263
269
  error,
264
270
  });
265
271
  }
266
- }),
267
- );
272
+ },
273
+ });
268
274
  }
269
275
  }
270
276
  },
@@ -124,6 +124,10 @@ import type { ToolOutput } from './tool-output';
124
124
  import type { ToolsContextParameter } from './tools-context-parameter';
125
125
  import { maybeSignApproval } from './tool-approval-signature';
126
126
  import { validateApprovedToolApprovals } from './validate-tool-approvals';
127
+ import {
128
+ mapWithConcurrency,
129
+ prepareToolCallConcurrency,
130
+ } from './tool-call-concurrency';
127
131
 
128
132
  const originalGenerateId = createIdGenerator({
129
133
  prefix: 'aitxt',
@@ -170,6 +174,7 @@ export type GenerateTextInclude = {
170
174
  * @param tools - Tools that are accessible to and can be called by the model. The model needs to support calling tools.
171
175
  * @param toolChoice - The tool choice strategy. Default: 'auto'.
172
176
  * @param toolOrder - Controls the order in which tools are sent to the provider. Tools not listed are appended alphabetically.
177
+ * @param toolCallConcurrency - Maximum number of tool calls that may execute concurrently within a step. Set to 1 for sequential execution. Default: unlimited.
173
178
  *
174
179
  * @param system - A system message that will be part of the prompt.
175
180
  * @param prompt - A simple text prompt. You can either use `prompt` or `messages` but not both.
@@ -255,6 +260,7 @@ export async function generateText<
255
260
  providerOptions,
256
261
  activeTools,
257
262
  toolOrder,
263
+ toolCallConcurrency,
258
264
  prepareStep,
259
265
  experimental_repairToolCall,
260
266
  repairToolCall = experimental_repairToolCall,
@@ -353,6 +359,14 @@ export async function generateText<
353
359
  */
354
360
  toolOrder?: ToolOrder<NoInfer<TOOLS>>;
355
361
 
362
+ /**
363
+ * Maximum number of tool calls that may execute concurrently within a step.
364
+ *
365
+ * Set to `1` to execute tool calls sequentially in the order they were
366
+ * generated. By default, all tool calls execute concurrently.
367
+ */
368
+ toolCallConcurrency?: number;
369
+
356
370
  /**
357
371
  * Optional specification for parsing structured outputs from the LLM response.
358
372
  */
@@ -571,6 +585,8 @@ export async function generateText<
571
585
  responseBody: include?.responseBody ?? false,
572
586
  };
573
587
 
588
+ const resolvedToolCallConcurrency =
589
+ prepareToolCallConcurrency(toolCallConcurrency);
574
590
  const model = resolveLanguageModel(modelArg);
575
591
  const resolvedToolCallers = resolveToolCallerConfiguration({
576
592
  tools,
@@ -770,6 +786,7 @@ export async function generateText<
770
786
  }),
771
787
  executeToolInTelemetryContext: telemetryDispatcher.executeTool,
772
788
  runInTracingChannelSpan,
789
+ toolCallConcurrency: resolvedToolCallConcurrency,
773
790
  });
774
791
 
775
792
  const toolContent: Array<any> = [];
@@ -1371,6 +1388,7 @@ export async function generateText<
1371
1388
  executeToolInTelemetryContext:
1372
1389
  telemetryDispatcher.executeTool,
1373
1390
  runInTracingChannelSpan,
1391
+ toolCallConcurrency: resolvedToolCallConcurrency,
1374
1392
  });
1375
1393
 
1376
1394
  for (const result of toolExecutionResults) {
@@ -1643,6 +1661,7 @@ async function executeTools<TOOLS extends ToolSet>({
1643
1661
  onToolExecutionEnd,
1644
1662
  executeToolInTelemetryContext,
1645
1663
  runInTracingChannelSpan,
1664
+ toolCallConcurrency,
1646
1665
  }: {
1647
1666
  toolCalls: Array<TypedToolCall<TOOLS>>;
1648
1667
  tools: TOOLS;
@@ -1658,31 +1677,33 @@ async function executeTools<TOOLS extends ToolSet>({
1658
1677
  runInTracingChannelSpan?: NonNullable<
1659
1678
  TelemetryDispatcher['runInTracingChannelSpan']
1660
1679
  >;
1680
+ toolCallConcurrency: number | undefined;
1661
1681
  }): Promise<
1662
1682
  Array<{
1663
1683
  output: ToolOutput<TOOLS>;
1664
1684
  toolExecutionMs: number;
1665
1685
  }>
1666
1686
  > {
1667
- const toolResults = await Promise.all(
1668
- toolCalls.map(
1669
- async toolCall =>
1670
- await executeToolCall({
1671
- toolCall,
1672
- tools,
1673
- callId,
1674
- messages,
1675
- abortSignal,
1676
- timeout,
1677
- experimental_sandbox: sandbox,
1678
- toolsContext,
1679
- onToolExecutionStart,
1680
- onToolExecutionEnd,
1681
- executeToolInTelemetryContext,
1682
- runInTracingChannelSpan,
1683
- }),
1684
- ),
1685
- );
1687
+ const toolResults = await mapWithConcurrency({
1688
+ items: toolCalls,
1689
+ concurrency: toolCallConcurrency,
1690
+ abortSignal,
1691
+ execute: async toolCall =>
1692
+ await executeToolCall({
1693
+ toolCall,
1694
+ tools,
1695
+ callId,
1696
+ messages,
1697
+ abortSignal,
1698
+ timeout,
1699
+ experimental_sandbox: sandbox,
1700
+ toolsContext,
1701
+ onToolExecutionStart,
1702
+ onToolExecutionEnd,
1703
+ executeToolInTelemetryContext,
1704
+ runInTracingChannelSpan,
1705
+ }),
1706
+ });
1686
1707
 
1687
1708
  return toolResults.filter(
1688
1709
  (result): result is NonNullable<typeof result> => result != null,
@@ -166,6 +166,10 @@ import type { ToolOutput } from './tool-output';
166
166
  import type { StaticToolOutputDenied } from './tool-output-denied';
167
167
  import type { ToolsContextParameter } from './tools-context-parameter';
168
168
  import { validateApprovedToolApprovals } from './validate-tool-approvals';
169
+ import {
170
+ mapWithConcurrency,
171
+ prepareToolCallConcurrency,
172
+ } from './tool-call-concurrency';
169
173
 
170
174
  const originalGenerateId = createIdGenerator({
171
175
  prefix: 'aitxt',
@@ -288,6 +292,7 @@ export type StreamTextOnAbortCallback<
288
292
  * @param model - The language model to use.
289
293
  * @param tools - Tools that are accessible to and can be called by the model. The model needs to support calling tools.
290
294
  * @param toolOrder - Controls the order in which tools are sent to the provider. Tools not listed are appended alphabetically.
295
+ * @param toolCallConcurrency - Maximum number of tool calls that may execute concurrently within a step. Set to 1 for sequential execution. Default: unlimited.
291
296
  *
292
297
  * @param system - A system message that will be part of the prompt.
293
298
  * @param prompt - A simple text prompt. You can either use `prompt` or `messages` but not both.
@@ -377,6 +382,7 @@ export function streamText<
377
382
  providerOptions,
378
383
  activeTools,
379
384
  toolOrder,
385
+ toolCallConcurrency,
380
386
  experimental_repairToolCall,
381
387
  repairToolCall = experimental_repairToolCall,
382
388
  experimental_refineToolInput: refineToolInput,
@@ -479,6 +485,14 @@ export function streamText<
479
485
  */
480
486
  toolOrder?: ToolOrder<NoInfer<TOOLS>>;
481
487
 
488
+ /**
489
+ * Maximum number of tool calls that may execute concurrently within a step.
490
+ *
491
+ * Set to `1` to execute tool calls sequentially in the order they were
492
+ * generated. By default, all tool calls execute concurrently.
493
+ */
494
+ toolCallConcurrency?: number;
495
+
482
496
  /**
483
497
  * Optional specification for parsing structured outputs from the LLM response.
484
498
  */
@@ -769,6 +783,8 @@ export function streamText<
769
783
  generateCallId?: IdGenerator;
770
784
  };
771
785
  }): StreamTextResult<TOOLS, RUNTIME_CONTEXT, OUTPUT> {
786
+ const resolvedToolCallConcurrency =
787
+ prepareToolCallConcurrency(toolCallConcurrency);
772
788
  const totalTimeoutMs = getTotalTimeoutMs(timeout);
773
789
  const stepTimeoutMs = getStepTimeoutMs(timeout);
774
790
  const firstChunkTimeoutMs = getFirstChunkTimeoutMs(timeout);
@@ -828,6 +844,7 @@ export function streamText<
828
844
  transforms: asArray(transform),
829
845
  activeTools,
830
846
  toolOrder,
847
+ toolCallConcurrency: resolvedToolCallConcurrency,
831
848
  repairToolCall,
832
849
  refineToolInput,
833
850
  stopConditions: asArray(stopWhen),
@@ -1212,6 +1229,7 @@ class DefaultStreamTextResult<
1212
1229
  transforms,
1213
1230
  activeTools,
1214
1231
  toolOrder,
1232
+ toolCallConcurrency,
1215
1233
  repairToolCall,
1216
1234
  refineToolInput,
1217
1235
  stopConditions,
@@ -1268,6 +1286,7 @@ class DefaultStreamTextResult<
1268
1286
  transforms: Array<StreamTextTransform<TOOLS>>;
1269
1287
  activeTools: ActiveTools<TOOLS>;
1270
1288
  toolOrder: ToolOrder<TOOLS>;
1289
+ toolCallConcurrency: number | undefined;
1271
1290
  repairToolCall: ToolCallRepairFunction<TOOLS> | undefined;
1272
1291
  refineToolInput: ToolInputRefinement<TOOLS> | undefined;
1273
1292
  stopConditions: Array<
@@ -2075,8 +2094,11 @@ class DefaultStreamTextResult<
2075
2094
 
2076
2095
  const toolOutputs: Array<ToolOutput<TOOLS>> = [];
2077
2096
 
2078
- await Promise.all(
2079
- localApprovedToolApprovals.map(async toolApproval => {
2097
+ await mapWithConcurrency({
2098
+ items: localApprovedToolApprovals,
2099
+ concurrency: toolCallConcurrency,
2100
+ abortSignal,
2101
+ execute: async toolApproval => {
2080
2102
  const result = await executeToolCall({
2081
2103
  toolCall: toolApproval.toolCall,
2082
2104
  tools,
@@ -2105,8 +2127,8 @@ class DefaultStreamTextResult<
2105
2127
  toolExecutionStepStreamController?.enqueue(result.output);
2106
2128
  toolOutputs.push(result.output);
2107
2129
  }
2108
- }),
2109
- );
2130
+ },
2131
+ });
2110
2132
 
2111
2133
  // Local tool results (approved + denied) are sent as tool results:
2112
2134
  if (
@@ -2709,6 +2731,7 @@ class DefaultStreamTextResult<
2709
2731
 
2710
2732
  executeToolInTelemetryContext: telemetryDispatcher.executeTool,
2711
2733
  runInTracingChannelSpan: runInTracingChannelSpanInStep,
2734
+ toolCallConcurrency,
2712
2735
  });
2713
2736
 
2714
2737
  // Conditionally include request.body based on include settings.
@@ -0,0 +1,80 @@
1
+ import { InvalidArgumentError } from '../error/invalid-argument-error';
2
+
3
+ /**
4
+ * Validates the maximum number of tool calls that may execute concurrently.
5
+ */
6
+ export function prepareToolCallConcurrency(
7
+ toolCallConcurrency: number | undefined,
8
+ ): number | undefined {
9
+ if (toolCallConcurrency == null) {
10
+ return undefined;
11
+ }
12
+
13
+ if (!Number.isInteger(toolCallConcurrency)) {
14
+ throw new InvalidArgumentError({
15
+ parameter: 'toolCallConcurrency',
16
+ value: toolCallConcurrency,
17
+ message: 'toolCallConcurrency must be an integer',
18
+ });
19
+ }
20
+
21
+ if (toolCallConcurrency < 1) {
22
+ throw new InvalidArgumentError({
23
+ parameter: 'toolCallConcurrency',
24
+ value: toolCallConcurrency,
25
+ message: 'toolCallConcurrency must be >= 1',
26
+ });
27
+ }
28
+
29
+ return toolCallConcurrency;
30
+ }
31
+
32
+ /**
33
+ * Maps items with an optional concurrency limit while preserving result order.
34
+ *
35
+ * Workers claim items in input order. When the signal is aborted or an
36
+ * execution rejects, no additional queued items are started.
37
+ */
38
+ export async function mapWithConcurrency<INPUT, OUTPUT>({
39
+ items,
40
+ concurrency,
41
+ abortSignal,
42
+ execute,
43
+ }: {
44
+ items: Array<INPUT>;
45
+ concurrency: number | undefined;
46
+ abortSignal?: AbortSignal;
47
+ execute: (item: INPUT, index: number) => Promise<OUTPUT>;
48
+ }): Promise<Array<OUTPUT>> {
49
+ if (concurrency == null || concurrency >= items.length) {
50
+ return Promise.all(items.map(execute));
51
+ }
52
+
53
+ const results = new Array<OUTPUT>(items.length);
54
+ let nextIndex = 0;
55
+ let stopped = false;
56
+
57
+ const worker = async () => {
58
+ while (!stopped) {
59
+ abortSignal?.throwIfAborted();
60
+
61
+ const index = nextIndex++;
62
+ if (index >= items.length) {
63
+ return;
64
+ }
65
+
66
+ try {
67
+ results[index] = await execute(items[index], index);
68
+ } catch (error) {
69
+ stopped = true;
70
+ throw error;
71
+ }
72
+ }
73
+ };
74
+
75
+ await Promise.all(
76
+ Array.from({ length: Math.min(concurrency, items.length) }, worker),
77
+ );
78
+
79
+ return results;
80
+ }