@trigger.dev/sdk 4.3.0 → 4.3.2

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.
@@ -1,5 +1,5 @@
1
1
  import { SpanKind } from "@opentelemetry/api";
2
- import { accessoryAttributes, apiClientManager, conditionallyImportPacket, convertToolParametersToSchema, createErrorTaskError, defaultRetryOptions, flattenIdempotencyKey, getEnvVar, getSchemaParseFn, lifecycleHooks, makeIdempotencyKey, parsePacket, resourceCatalog, runtime, SemanticInternalAttributes, stringifyIO, SubtaskUnwrapError, taskContext, TaskRunPromise, } from "@trigger.dev/core/v3";
2
+ import { accessoryAttributes, ApiError, apiClientManager, conditionallyImportPacket, convertToolParametersToSchema, createErrorTaskError, defaultRetryOptions, flattenIdempotencyKey, getEnvVar, getSchemaParseFn, lifecycleHooks, makeIdempotencyKey, parsePacket, RateLimitError, resourceCatalog, runtime, SemanticInternalAttributes, stringifyIO, SubtaskUnwrapError, taskContext, TaskRunPromise, } from "@trigger.dev/core/v3";
3
3
  import { tracer } from "./tracer.js";
4
4
  export { SubtaskUnwrapError, TaskRunPromise };
5
5
  export function queue(options) {
@@ -221,63 +221,14 @@ export async function batchTriggerAndWait(id, items, options, requestOptions) {
221
221
  export async function batchTrigger(id, items, options, requestOptions) {
222
222
  return await batchTrigger_internal("tasks.batchTrigger()", id, items, options, undefined, requestOptions);
223
223
  }
224
- /**
225
- * Triggers multiple runs of different tasks with specified payloads and options.
226
- *
227
- * @template TTask - The type of task(s) to be triggered, extends AnyTask
228
- *
229
- * @param {Array<BatchByIdItem<InferRunTypes<TTask>>>} items - Array of task items to trigger
230
- * @param {BatchTriggerOptions} [options] - Optional batch-level trigger options
231
- * @param {TriggerApiRequestOptions} [requestOptions] - Optional API request configuration
232
- *
233
- * @returns {Promise<BatchRunHandleFromTypes<InferRunTypes<TTask>>>} A promise that resolves with the batch run handle
234
- * containing batch ID, cached status, idempotency info, runs, and public access token
235
- *
236
- * @example
237
- * ```ts
238
- * import { batch } from "@trigger.dev/sdk/v3";
239
- * import type { myTask1, myTask2 } from "~/trigger/myTasks";
240
- *
241
- * // Trigger multiple tasks with different payloads
242
- * const result = await batch.trigger<typeof myTask1 | typeof myTask2>([
243
- * {
244
- * id: "my-task-1",
245
- * payload: { some: "data" },
246
- * options: {
247
- * queue: "default",
248
- * concurrencyKey: "key",
249
- * idempotencyKey: "unique-key",
250
- * delay: "5m",
251
- * tags: ["tag1", "tag2"]
252
- * }
253
- * },
254
- * {
255
- * id: "my-task-2",
256
- * payload: { other: "data" }
257
- * }
258
- * ]);
259
- * ```
260
- *
261
- * @description
262
- * Each task item in the array can include:
263
- * - `id`: The unique identifier of the task
264
- * - `payload`: The data to pass to the task
265
- * - `options`: Optional task-specific settings including:
266
- * - `queue`: Specify a queue for the task
267
- * - `concurrencyKey`: Control concurrent execution
268
- * - `idempotencyKey`: Prevent duplicate runs
269
- * - `idempotencyKeyTTL`: Time-to-live for idempotency key
270
- * - `delay`: Delay before task execution
271
- * - `ttl`: Time-to-live for the task
272
- * - `tags`: Array of tags for the task
273
- * - `maxAttempts`: Maximum retry attempts
274
- * - `metadata`: Additional metadata
275
- * - `maxDuration`: Maximum execution duration
276
- */
277
- export async function batchTriggerById(items, options, requestOptions) {
224
+ // Implementation
225
+ export async function batchTriggerById(...args) {
226
+ const [items, options, requestOptions] = args;
278
227
  const apiClient = apiClientManager.clientOrThrow(requestOptions?.clientConfig);
279
- const response = await apiClient.batchTriggerV3({
280
- items: await Promise.all(items.map(async (item, index) => {
228
+ // Check if items is an array or a stream
229
+ if (Array.isArray(items)) {
230
+ // Array path: existing logic
231
+ const ndJsonItems = await Promise.all(items.map(async (item, index) => {
281
232
  const taskMetadata = resourceCatalog.getTask(item.id);
282
233
  const parsedPayload = taskMetadata?.fns.parsePayload
283
234
  ? await taskMetadata?.fns.parsePayload(item.payload)
@@ -285,6 +236,7 @@ export async function batchTriggerById(items, options, requestOptions) {
285
236
  const payloadPacket = await stringifyIO(parsedPayload);
286
237
  const batchItemIdempotencyKey = await makeIdempotencyKey(flattenIdempotencyKey([options?.idempotencyKey, `${index}`]));
287
238
  return {
239
+ index,
288
240
  task: item.id,
289
241
  payload: payloadPacket.data,
290
242
  options: {
@@ -304,257 +256,266 @@ export async function batchTriggerById(items, options, requestOptions) {
304
256
  priority: item.options?.priority,
305
257
  region: item.options?.region,
306
258
  lockToVersion: item.options?.version ?? getEnvVar("TRIGGER_VERSION"),
259
+ debounce: item.options?.debounce,
307
260
  },
308
261
  };
309
- })),
310
- parentRunId: taskContext.ctx?.run.id,
311
- }, {
312
- spanParentAsLink: true,
313
- processingStrategy: options?.triggerSequentially ? "sequential" : undefined,
314
- }, {
315
- name: "batch.trigger()",
316
- tracer,
317
- icon: "trigger",
318
- onResponseBody(body, span) {
319
- if (body && typeof body === "object" && !Array.isArray(body)) {
320
- if ("id" in body && typeof body.id === "string") {
321
- span.setAttribute("batchId", body.id);
322
- }
323
- if ("runCount" in body && typeof body.runCount === "number") {
324
- span.setAttribute("runCount", body.runCount);
325
- }
326
- }
327
- },
328
- ...requestOptions,
329
- });
330
- const handle = {
331
- batchId: response.id,
332
- runCount: response.runCount,
333
- publicAccessToken: response.publicAccessToken,
334
- };
335
- return handle;
262
+ }));
263
+ // Execute 2-phase batch
264
+ const response = await tracer.startActiveSpan("batch.trigger()", async (span) => {
265
+ const result = await executeBatchTwoPhase(apiClient, ndJsonItems, {
266
+ parentRunId: taskContext.ctx?.run.id,
267
+ idempotencyKey: await makeIdempotencyKey(options?.idempotencyKey),
268
+ spanParentAsLink: true, // Fire-and-forget: child runs get separate trace IDs
269
+ }, requestOptions);
270
+ span.setAttribute("batchId", result.id);
271
+ span.setAttribute("runCount", result.runCount);
272
+ return result;
273
+ }, {
274
+ kind: SpanKind.PRODUCER,
275
+ attributes: {
276
+ [SemanticInternalAttributes.STYLE_ICON]: "trigger",
277
+ },
278
+ });
279
+ const handle = {
280
+ batchId: response.id,
281
+ runCount: response.runCount,
282
+ publicAccessToken: response.publicAccessToken,
283
+ };
284
+ return handle;
285
+ }
286
+ else {
287
+ // Stream path: convert to AsyncIterable and transform
288
+ const asyncItems = normalizeToAsyncIterable(items);
289
+ const transformedItems = transformBatchItemsStream(asyncItems, options);
290
+ // Execute streaming 2-phase batch
291
+ const response = await tracer.startActiveSpan("batch.trigger()", async (span) => {
292
+ const result = await executeBatchTwoPhaseStreaming(apiClient, transformedItems, {
293
+ parentRunId: taskContext.ctx?.run.id,
294
+ idempotencyKey: await makeIdempotencyKey(options?.idempotencyKey),
295
+ spanParentAsLink: true, // Fire-and-forget: child runs get separate trace IDs
296
+ }, requestOptions);
297
+ span.setAttribute("batchId", result.id);
298
+ span.setAttribute("runCount", result.runCount);
299
+ return result;
300
+ }, {
301
+ kind: SpanKind.PRODUCER,
302
+ attributes: {
303
+ [SemanticInternalAttributes.STYLE_ICON]: "trigger",
304
+ },
305
+ });
306
+ const handle = {
307
+ batchId: response.id,
308
+ runCount: response.runCount,
309
+ publicAccessToken: response.publicAccessToken,
310
+ };
311
+ return handle;
312
+ }
336
313
  }
337
- /**
338
- * Triggers multiple tasks and waits for all of them to complete before returning their results.
339
- * This function must be called from within a task.run() context.
340
- *
341
- * @template TTask - Union type of tasks to be triggered, extends AnyTask
342
- *
343
- * @param {Array<BatchByIdAndWaitItem<InferRunTypes<TTask>>>} items - Array of task items to trigger
344
- * @param {TriggerApiRequestOptions} [requestOptions] - Optional API request configuration
345
- *
346
- * @returns {Promise<BatchByIdResult<TTask>>} A promise that resolves with the batch results, including
347
- * success/failure status and strongly-typed outputs for each task
348
- *
349
- * @throws {Error} If called outside of a task.run() context
350
- * @throws {Error} If no API client is configured
351
- *
352
- * @example
353
- * ```ts
354
- * import { batch, task } from "@trigger.dev/sdk/v3";
355
- *
356
- * export const parentTask = task({
357
- * id: "parent-task",
358
- * run: async (payload: string) => {
359
- * const results = await batch.triggerAndWait<typeof childTask1 | typeof childTask2>([
360
- * {
361
- * id: "child-task-1",
362
- * payload: { foo: "World" },
363
- * options: {
364
- * queue: "default",
365
- * delay: "5m",
366
- * tags: ["batch", "child1"]
367
- * }
368
- * },
369
- * {
370
- * id: "child-task-2",
371
- * payload: { bar: 42 }
372
- * }
373
- * ]);
374
- *
375
- * // Type-safe result handling
376
- * for (const result of results) {
377
- * if (result.ok) {
378
- * switch (result.taskIdentifier) {
379
- * case "child-task-1":
380
- * console.log("Child task 1 output:", result.output); // string type
381
- * break;
382
- * case "child-task-2":
383
- * console.log("Child task 2 output:", result.output); // number type
384
- * break;
385
- * }
386
- * } else {
387
- * console.error("Task failed:", result.error);
388
- * }
389
- * }
390
- * }
391
- * });
392
- * ```
393
- *
394
- * @description
395
- * Each task item in the array can include:
396
- * - `id`: The task identifier (must match one of the tasks in the union type)
397
- * - `payload`: Strongly-typed payload matching the task's input type
398
- * - `options`: Optional task-specific settings including:
399
- * - `queue`: Specify a queue for the task
400
- * - `concurrencyKey`: Control concurrent execution
401
- * - `delay`: Delay before task execution
402
- * - `ttl`: Time-to-live for the task
403
- * - `tags`: Array of tags for the task
404
- * - `maxAttempts`: Maximum retry attempts
405
- * - `metadata`: Additional metadata
406
- * - `maxDuration`: Maximum execution duration
407
- *
408
- * The function provides full type safety for:
409
- * - Task IDs
410
- * - Payload types
411
- * - Return value types
412
- * - Error handling
413
- */
414
- export async function batchTriggerByIdAndWait(items, options, requestOptions) {
314
+ // Implementation
315
+ export async function batchTriggerByIdAndWait(...args) {
316
+ const [items, options, requestOptions] = args;
415
317
  const ctx = taskContext.ctx;
416
318
  if (!ctx) {
417
319
  throw new Error("batchTriggerAndWait can only be used from inside a task.run()");
418
320
  }
419
321
  const apiClient = apiClientManager.clientOrThrow(requestOptions?.clientConfig);
420
- return await tracer.startActiveSpan("batch.triggerAndWait()", async (span) => {
421
- const response = await apiClient.batchTriggerV3({
422
- items: await Promise.all(items.map(async (item, index) => {
423
- const taskMetadata = resourceCatalog.getTask(item.id);
424
- const parsedPayload = taskMetadata?.fns.parsePayload
425
- ? await taskMetadata?.fns.parsePayload(item.payload)
426
- : item.payload;
427
- const payloadPacket = await stringifyIO(parsedPayload);
428
- const batchItemIdempotencyKey = await makeIdempotencyKey(flattenIdempotencyKey([options?.idempotencyKey, `${index}`]));
429
- return {
430
- task: item.id,
431
- payload: payloadPacket.data,
432
- options: {
433
- lockToVersion: taskContext.worker?.version,
434
- queue: item.options?.queue ? { name: item.options.queue } : undefined,
435
- concurrencyKey: item.options?.concurrencyKey,
436
- test: taskContext.ctx?.run.isTest,
437
- payloadType: payloadPacket.dataType,
438
- delay: item.options?.delay,
439
- ttl: item.options?.ttl,
440
- tags: item.options?.tags,
441
- maxAttempts: item.options?.maxAttempts,
442
- metadata: item.options?.metadata,
443
- maxDuration: item.options?.maxDuration,
444
- idempotencyKey: (await makeIdempotencyKey(item.options?.idempotencyKey)) ??
445
- batchItemIdempotencyKey,
446
- idempotencyKeyTTL: item.options?.idempotencyKeyTTL ?? options?.idempotencyKeyTTL,
447
- machine: item.options?.machine,
448
- priority: item.options?.priority,
449
- region: item.options?.region,
450
- },
451
- };
452
- })),
453
- parentRunId: ctx.run.id,
454
- resumeParentOnCompletion: true,
322
+ // Check if items is an array or a stream
323
+ if (Array.isArray(items)) {
324
+ // Array path: existing logic
325
+ const ndJsonItems = await Promise.all(items.map(async (item, index) => {
326
+ const taskMetadata = resourceCatalog.getTask(item.id);
327
+ const parsedPayload = taskMetadata?.fns.parsePayload
328
+ ? await taskMetadata?.fns.parsePayload(item.payload)
329
+ : item.payload;
330
+ const payloadPacket = await stringifyIO(parsedPayload);
331
+ const batchItemIdempotencyKey = await makeIdempotencyKey(flattenIdempotencyKey([options?.idempotencyKey, `${index}`]));
332
+ return {
333
+ index,
334
+ task: item.id,
335
+ payload: payloadPacket.data,
336
+ options: {
337
+ lockToVersion: taskContext.worker?.version,
338
+ queue: item.options?.queue ? { name: item.options.queue } : undefined,
339
+ concurrencyKey: item.options?.concurrencyKey,
340
+ test: taskContext.ctx?.run.isTest,
341
+ payloadType: payloadPacket.dataType,
342
+ delay: item.options?.delay,
343
+ ttl: item.options?.ttl,
344
+ tags: item.options?.tags,
345
+ maxAttempts: item.options?.maxAttempts,
346
+ metadata: item.options?.metadata,
347
+ maxDuration: item.options?.maxDuration,
348
+ idempotencyKey: (await makeIdempotencyKey(item.options?.idempotencyKey)) ?? batchItemIdempotencyKey,
349
+ idempotencyKeyTTL: item.options?.idempotencyKeyTTL ?? options?.idempotencyKeyTTL,
350
+ machine: item.options?.machine,
351
+ priority: item.options?.priority,
352
+ region: item.options?.region,
353
+ debounce: item.options?.debounce,
354
+ },
355
+ };
356
+ }));
357
+ return await tracer.startActiveSpan("batch.triggerAndWait()", async (span) => {
358
+ // Execute 2-phase batch
359
+ const response = await executeBatchTwoPhase(apiClient, ndJsonItems, {
360
+ parentRunId: ctx.run.id,
361
+ resumeParentOnCompletion: true,
362
+ idempotencyKey: await makeIdempotencyKey(options?.idempotencyKey),
363
+ spanParentAsLink: false, // Waiting: child runs share parent's trace ID
364
+ }, requestOptions);
365
+ span.setAttribute("batchId", response.id);
366
+ span.setAttribute("runCount", response.runCount);
367
+ const result = await runtime.waitForBatch({
368
+ id: response.id,
369
+ runCount: response.runCount,
370
+ ctx,
371
+ });
372
+ const runs = await handleBatchTaskRunExecutionResultV2(result.items);
373
+ return {
374
+ id: result.id,
375
+ runs,
376
+ };
455
377
  }, {
456
- processingStrategy: options?.triggerSequentially ? "sequential" : undefined,
457
- }, requestOptions);
458
- span.setAttribute("batchId", response.id);
459
- span.setAttribute("runCount", response.runCount);
460
- const result = await runtime.waitForBatch({
461
- id: response.id,
378
+ kind: SpanKind.PRODUCER,
379
+ attributes: {
380
+ [SemanticInternalAttributes.STYLE_ICON]: "trigger",
381
+ },
382
+ });
383
+ }
384
+ else {
385
+ // Stream path: convert to AsyncIterable and transform
386
+ const asyncItems = normalizeToAsyncIterable(items);
387
+ const transformedItems = transformBatchItemsStreamForWait(asyncItems, options);
388
+ return await tracer.startActiveSpan("batch.triggerAndWait()", async (span) => {
389
+ // Execute streaming 2-phase batch
390
+ const response = await executeBatchTwoPhaseStreaming(apiClient, transformedItems, {
391
+ parentRunId: ctx.run.id,
392
+ resumeParentOnCompletion: true,
393
+ idempotencyKey: await makeIdempotencyKey(options?.idempotencyKey),
394
+ spanParentAsLink: false, // Waiting: child runs share parent's trace ID
395
+ }, requestOptions);
396
+ span.setAttribute("batchId", response.id);
397
+ span.setAttribute("runCount", response.runCount);
398
+ const result = await runtime.waitForBatch({
399
+ id: response.id,
400
+ runCount: response.runCount,
401
+ ctx,
402
+ });
403
+ const runs = await handleBatchTaskRunExecutionResultV2(result.items);
404
+ return {
405
+ id: result.id,
406
+ runs,
407
+ };
408
+ }, {
409
+ kind: SpanKind.PRODUCER,
410
+ attributes: {
411
+ [SemanticInternalAttributes.STYLE_ICON]: "trigger",
412
+ },
413
+ });
414
+ }
415
+ }
416
+ // Implementation
417
+ export async function batchTriggerTasks(...args) {
418
+ const [items, options, requestOptions] = args;
419
+ const apiClient = apiClientManager.clientOrThrow(requestOptions?.clientConfig);
420
+ // Check if items is an array or a stream
421
+ if (Array.isArray(items)) {
422
+ // Array path: existing logic
423
+ const ndJsonItems = await Promise.all(items.map(async (item, index) => {
424
+ const taskMetadata = resourceCatalog.getTask(item.task.id);
425
+ const parsedPayload = taskMetadata?.fns.parsePayload
426
+ ? await taskMetadata?.fns.parsePayload(item.payload)
427
+ : item.payload;
428
+ const payloadPacket = await stringifyIO(parsedPayload);
429
+ const batchItemIdempotencyKey = await makeIdempotencyKey(flattenIdempotencyKey([options?.idempotencyKey, `${index}`]));
430
+ return {
431
+ index,
432
+ task: item.task.id,
433
+ payload: payloadPacket.data,
434
+ options: {
435
+ queue: item.options?.queue ? { name: item.options.queue } : undefined,
436
+ concurrencyKey: item.options?.concurrencyKey,
437
+ test: taskContext.ctx?.run.isTest,
438
+ payloadType: payloadPacket.dataType,
439
+ delay: item.options?.delay,
440
+ ttl: item.options?.ttl,
441
+ tags: item.options?.tags,
442
+ maxAttempts: item.options?.maxAttempts,
443
+ metadata: item.options?.metadata,
444
+ maxDuration: item.options?.maxDuration,
445
+ idempotencyKey: (await makeIdempotencyKey(item.options?.idempotencyKey)) ?? batchItemIdempotencyKey,
446
+ idempotencyKeyTTL: item.options?.idempotencyKeyTTL ?? options?.idempotencyKeyTTL,
447
+ machine: item.options?.machine,
448
+ priority: item.options?.priority,
449
+ region: item.options?.region,
450
+ lockToVersion: item.options?.version ?? getEnvVar("TRIGGER_VERSION"),
451
+ debounce: item.options?.debounce,
452
+ },
453
+ };
454
+ }));
455
+ // Execute 2-phase batch
456
+ const response = await tracer.startActiveSpan("batch.triggerByTask()", async (span) => {
457
+ const result = await executeBatchTwoPhase(apiClient, ndJsonItems, {
458
+ parentRunId: taskContext.ctx?.run.id,
459
+ idempotencyKey: await makeIdempotencyKey(options?.idempotencyKey),
460
+ spanParentAsLink: true, // Fire-and-forget: child runs get separate trace IDs
461
+ }, requestOptions);
462
+ span.setAttribute("batchId", result.id);
463
+ span.setAttribute("runCount", result.runCount);
464
+ return result;
465
+ }, {
466
+ kind: SpanKind.PRODUCER,
467
+ attributes: {
468
+ [SemanticInternalAttributes.STYLE_ICON]: "trigger",
469
+ },
470
+ });
471
+ const handle = {
472
+ batchId: response.id,
462
473
  runCount: response.runCount,
463
- ctx,
474
+ publicAccessToken: response.publicAccessToken,
475
+ };
476
+ return handle;
477
+ }
478
+ else {
479
+ // Stream path: convert to AsyncIterable and transform
480
+ const streamItems = items;
481
+ const asyncItems = normalizeToAsyncIterable(streamItems);
482
+ const transformedItems = transformBatchByTaskItemsStream(asyncItems, options);
483
+ // Execute streaming 2-phase batch
484
+ const response = await tracer.startActiveSpan("batch.triggerByTask()", async (span) => {
485
+ const result = await executeBatchTwoPhaseStreaming(apiClient, transformedItems, {
486
+ parentRunId: taskContext.ctx?.run.id,
487
+ idempotencyKey: await makeIdempotencyKey(options?.idempotencyKey),
488
+ spanParentAsLink: true, // Fire-and-forget: child runs get separate trace IDs
489
+ }, requestOptions);
490
+ span.setAttribute("batchId", result.id);
491
+ span.setAttribute("runCount", result.runCount);
492
+ return result;
493
+ }, {
494
+ kind: SpanKind.PRODUCER,
495
+ attributes: {
496
+ [SemanticInternalAttributes.STYLE_ICON]: "trigger",
497
+ },
464
498
  });
465
- const runs = await handleBatchTaskRunExecutionResultV2(result.items);
466
- return {
467
- id: result.id,
468
- runs,
499
+ const handle = {
500
+ batchId: response.id,
501
+ runCount: response.runCount,
502
+ publicAccessToken: response.publicAccessToken,
469
503
  };
470
- }, {
471
- kind: SpanKind.PRODUCER,
472
- attributes: {
473
- [SemanticInternalAttributes.STYLE_ICON]: "trigger",
474
- },
475
- });
504
+ return handle;
505
+ }
476
506
  }
477
- /**
478
- * Triggers multiple tasks and waits for all of them to complete before returning their results.
479
- * This function must be called from within a task.run() context.
480
- *
481
- * @template TTask - Union type of tasks to be triggered, extends AnyTask
482
- *
483
- * @param {Array<BatchByIdAndWaitItem<InferRunTypes<TTask>>>} items - Array of task items to trigger
484
- * @param {TriggerApiRequestOptions} [requestOptions] - Optional API request configuration
485
- *
486
- * @returns {Promise<BatchByIdResult<TTask>>} A promise that resolves with the batch results, including
487
- * success/failure status and strongly-typed outputs for each task
488
- *
489
- * @throws {Error} If called outside of a task.run() context
490
- * @throws {Error} If no API client is configured
491
- *
492
- * @example
493
- * ```ts
494
- * import { batch, task } from "@trigger.dev/sdk/v3";
495
- *
496
- * export const parentTask = task({
497
- * id: "parent-task",
498
- * run: async (payload: string) => {
499
- * const results = await batch.triggerAndWait<typeof childTask1 | typeof childTask2>([
500
- * {
501
- * id: "child-task-1",
502
- * payload: { foo: "World" },
503
- * options: {
504
- * queue: "default",
505
- * delay: "5m",
506
- * tags: ["batch", "child1"]
507
- * }
508
- * },
509
- * {
510
- * id: "child-task-2",
511
- * payload: { bar: 42 }
512
- * }
513
- * ]);
514
- *
515
- * // Type-safe result handling
516
- * for (const result of results) {
517
- * if (result.ok) {
518
- * switch (result.taskIdentifier) {
519
- * case "child-task-1":
520
- * console.log("Child task 1 output:", result.output); // string type
521
- * break;
522
- * case "child-task-2":
523
- * console.log("Child task 2 output:", result.output); // number type
524
- * break;
525
- * }
526
- * } else {
527
- * console.error("Task failed:", result.error);
528
- * }
529
- * }
530
- * }
531
- * });
532
- * ```
533
- *
534
- * @description
535
- * Each task item in the array can include:
536
- * - `id`: The task identifier (must match one of the tasks in the union type)
537
- * - `payload`: Strongly-typed payload matching the task's input type
538
- * - `options`: Optional task-specific settings including:
539
- * - `queue`: Specify a queue for the task
540
- * - `concurrencyKey`: Control concurrent execution
541
- * - `delay`: Delay before task execution
542
- * - `ttl`: Time-to-live for the task
543
- * - `tags`: Array of tags for the task
544
- * - `maxAttempts`: Maximum retry attempts
545
- * - `metadata`: Additional metadata
546
- * - `maxDuration`: Maximum execution duration
547
- *
548
- * The function provides full type safety for:
549
- * - Task IDs
550
- * - Payload types
551
- * - Return value types
552
- * - Error handling
553
- */
554
- export async function batchTriggerTasks(items, options, requestOptions) {
507
+ // Implementation
508
+ export async function batchTriggerAndWaitTasks(...args) {
509
+ const [items, options, requestOptions] = args;
510
+ const ctx = taskContext.ctx;
511
+ if (!ctx) {
512
+ throw new Error("batchTriggerAndWait can only be used from inside a task.run()");
513
+ }
555
514
  const apiClient = apiClientManager.clientOrThrow(requestOptions?.clientConfig);
556
- const response = await apiClient.batchTriggerV3({
557
- items: await Promise.all(items.map(async (item, index) => {
515
+ // Check if items is an array or a stream
516
+ if (Array.isArray(items)) {
517
+ // Array path: existing logic
518
+ const ndJsonItems = await Promise.all(items.map(async (item, index) => {
558
519
  const taskMetadata = resourceCatalog.getTask(item.task.id);
559
520
  const parsedPayload = taskMetadata?.fns.parsePayload
560
521
  ? await taskMetadata?.fns.parsePayload(item.payload)
@@ -562,9 +523,11 @@ export async function batchTriggerTasks(items, options, requestOptions) {
562
523
  const payloadPacket = await stringifyIO(parsedPayload);
563
524
  const batchItemIdempotencyKey = await makeIdempotencyKey(flattenIdempotencyKey([options?.idempotencyKey, `${index}`]));
564
525
  return {
526
+ index,
565
527
  task: item.task.id,
566
528
  payload: payloadPacket.data,
567
529
  options: {
530
+ lockToVersion: taskContext.worker?.version,
568
531
  queue: item.options?.queue ? { name: item.options.queue } : undefined,
569
532
  concurrencyKey: item.options?.concurrencyKey,
570
533
  test: taskContext.ctx?.run.isTest,
@@ -580,176 +543,511 @@ export async function batchTriggerTasks(items, options, requestOptions) {
580
543
  machine: item.options?.machine,
581
544
  priority: item.options?.priority,
582
545
  region: item.options?.region,
583
- lockToVersion: item.options?.version ?? getEnvVar("TRIGGER_VERSION"),
546
+ debounce: item.options?.debounce,
584
547
  },
585
548
  };
586
- })),
587
- parentRunId: taskContext.ctx?.run.id,
588
- }, {
589
- spanParentAsLink: true,
590
- processingStrategy: options?.triggerSequentially ? "sequential" : undefined,
591
- }, {
592
- name: "batch.triggerByTask()",
593
- tracer,
594
- icon: "trigger",
595
- onResponseBody(body, span) {
596
- if (body && typeof body === "object" && !Array.isArray(body)) {
597
- if ("id" in body && typeof body.id === "string") {
598
- span.setAttribute("batchId", body.id);
599
- }
600
- if ("runCount" in body && typeof body.runCount === "number") {
601
- span.setAttribute("runCount", body.runCount);
602
- }
603
- }
604
- },
605
- ...requestOptions,
606
- });
607
- const handle = {
608
- batchId: response.id,
609
- runCount: response.runCount,
610
- publicAccessToken: response.publicAccessToken,
611
- };
612
- return handle;
549
+ }));
550
+ return await tracer.startActiveSpan("batch.triggerByTaskAndWait()", async (span) => {
551
+ // Execute 2-phase batch
552
+ const response = await executeBatchTwoPhase(apiClient, ndJsonItems, {
553
+ parentRunId: ctx.run.id,
554
+ resumeParentOnCompletion: true,
555
+ idempotencyKey: await makeIdempotencyKey(options?.idempotencyKey),
556
+ spanParentAsLink: false, // Waiting: child runs share parent's trace ID
557
+ }, requestOptions);
558
+ span.setAttribute("batchId", response.id);
559
+ span.setAttribute("runCount", response.runCount);
560
+ const result = await runtime.waitForBatch({
561
+ id: response.id,
562
+ runCount: response.runCount,
563
+ ctx,
564
+ });
565
+ const runs = await handleBatchTaskRunExecutionResultV2(result.items);
566
+ return {
567
+ id: result.id,
568
+ runs,
569
+ };
570
+ }, {
571
+ kind: SpanKind.PRODUCER,
572
+ attributes: {
573
+ [SemanticInternalAttributes.STYLE_ICON]: "trigger",
574
+ },
575
+ });
576
+ }
577
+ else {
578
+ // Stream path: convert to AsyncIterable and transform
579
+ const streamItems = items;
580
+ const asyncItems = normalizeToAsyncIterable(streamItems);
581
+ const transformedItems = transformBatchByTaskItemsStreamForWait(asyncItems, options);
582
+ return await tracer.startActiveSpan("batch.triggerByTaskAndWait()", async (span) => {
583
+ // Execute streaming 2-phase batch
584
+ const response = await executeBatchTwoPhaseStreaming(apiClient, transformedItems, {
585
+ parentRunId: ctx.run.id,
586
+ resumeParentOnCompletion: true,
587
+ idempotencyKey: await makeIdempotencyKey(options?.idempotencyKey),
588
+ spanParentAsLink: false, // Waiting: child runs share parent's trace ID
589
+ }, requestOptions);
590
+ span.setAttribute("batchId", response.id);
591
+ span.setAttribute("runCount", response.runCount);
592
+ const result = await runtime.waitForBatch({
593
+ id: response.id,
594
+ runCount: response.runCount,
595
+ ctx,
596
+ });
597
+ const runs = await handleBatchTaskRunExecutionResultV2(result.items);
598
+ return {
599
+ id: result.id,
600
+ runs,
601
+ };
602
+ }, {
603
+ kind: SpanKind.PRODUCER,
604
+ attributes: {
605
+ [SemanticInternalAttributes.STYLE_ICON]: "trigger",
606
+ },
607
+ });
608
+ }
613
609
  }
614
610
  /**
615
- * Triggers multiple tasks and waits for all of them to complete before returning their results.
616
- * This function must be called from within a task.run() context.
611
+ * Helper function that executes a 2-phase batch trigger:
612
+ * 1. Creates the batch record with expected run count
613
+ * 2. Streams items as NDJSON to the server
617
614
  *
618
- * @template TTask - Union type of tasks to be triggered, extends AnyTask
615
+ * @param apiClient - The API client instance
616
+ * @param items - Array of batch items
617
+ * @param options - Batch options including trace context settings
618
+ * @param options.spanParentAsLink - If true, child runs will have separate trace IDs with a link to parent.
619
+ * Use true for batchTrigger (fire-and-forget), false for batchTriggerAndWait.
620
+ * @param requestOptions - Optional request options
621
+ * @internal
622
+ */
623
+ async function executeBatchTwoPhase(apiClient, items, options, requestOptions) {
624
+ let batch;
625
+ try {
626
+ // Phase 1: Create batch
627
+ batch = await apiClient.createBatch({
628
+ runCount: items.length,
629
+ parentRunId: options.parentRunId,
630
+ resumeParentOnCompletion: options.resumeParentOnCompletion,
631
+ idempotencyKey: options.idempotencyKey,
632
+ }, { spanParentAsLink: options.spanParentAsLink }, requestOptions);
633
+ }
634
+ catch (error) {
635
+ // Wrap with context about which phase failed
636
+ throw new BatchTriggerError(`Failed to create batch with ${items.length} items`, {
637
+ cause: error,
638
+ phase: "create",
639
+ itemCount: items.length,
640
+ });
641
+ }
642
+ // If the batch was cached (idempotent replay), skip streaming items
643
+ if (!batch.isCached) {
644
+ try {
645
+ // Phase 2: Stream items
646
+ await apiClient.streamBatchItems(batch.id, items, requestOptions);
647
+ }
648
+ catch (error) {
649
+ // Wrap with context about which phase failed and include batch ID
650
+ throw new BatchTriggerError(`Failed to stream items for batch ${batch.id} (${items.length} items)`, { cause: error, phase: "stream", batchId: batch.id, itemCount: items.length });
651
+ }
652
+ }
653
+ return {
654
+ id: batch.id,
655
+ runCount: batch.runCount,
656
+ publicAccessToken: batch.publicAccessToken,
657
+ };
658
+ }
659
+ /**
660
+ * Error thrown when batch trigger operations fail.
661
+ * Includes context about which phase failed and the batch details.
619
662
  *
620
- * @param {Array<BatchByIdAndWaitItem<InferRunTypes<TTask>>>} items - Array of task items to trigger
621
- * @param {TriggerApiRequestOptions} [requestOptions] - Optional API request configuration
663
+ * When the underlying error is a rate limit (429), additional properties are exposed:
664
+ * - `isRateLimited`: true
665
+ * - `retryAfterMs`: milliseconds until the rate limit resets
666
+ */
667
+ export class BatchTriggerError extends Error {
668
+ phase;
669
+ batchId;
670
+ itemCount;
671
+ /** True if the error was caused by rate limiting (HTTP 429) */
672
+ isRateLimited;
673
+ /** Milliseconds until the rate limit resets. Only set when `isRateLimited` is true. */
674
+ retryAfterMs;
675
+ /** The underlying API error, if the cause was an ApiError */
676
+ apiError;
677
+ /** The underlying cause of the error */
678
+ cause;
679
+ constructor(message, options) {
680
+ // Build enhanced message that includes the cause's message
681
+ const fullMessage = buildBatchErrorMessage(message, options.cause);
682
+ super(fullMessage, { cause: options.cause });
683
+ this.name = "BatchTriggerError";
684
+ this.cause = options.cause;
685
+ this.phase = options.phase;
686
+ this.batchId = options.batchId;
687
+ this.itemCount = options.itemCount;
688
+ // Extract rate limit info from cause
689
+ if (options.cause instanceof RateLimitError) {
690
+ this.isRateLimited = true;
691
+ this.retryAfterMs = options.cause.millisecondsUntilReset;
692
+ this.apiError = options.cause;
693
+ }
694
+ else if (options.cause instanceof ApiError) {
695
+ this.isRateLimited = options.cause.status === 429;
696
+ this.apiError = options.cause;
697
+ }
698
+ else {
699
+ this.isRateLimited = false;
700
+ }
701
+ }
702
+ }
703
+ /**
704
+ * Build an enhanced error message that includes context from the cause.
705
+ */
706
+ function buildBatchErrorMessage(baseMessage, cause) {
707
+ if (!cause) {
708
+ return baseMessage;
709
+ }
710
+ // Handle RateLimitError specifically for better messaging
711
+ if (cause instanceof RateLimitError) {
712
+ const retryMs = cause.millisecondsUntilReset;
713
+ if (retryMs !== undefined) {
714
+ const retrySeconds = Math.ceil(retryMs / 1000);
715
+ return `${baseMessage}: Rate limit exceeded - retry after ${retrySeconds}s`;
716
+ }
717
+ return `${baseMessage}: Rate limit exceeded`;
718
+ }
719
+ // Handle other ApiErrors
720
+ if (cause instanceof ApiError) {
721
+ return `${baseMessage}: ${cause.message}`;
722
+ }
723
+ // Handle generic errors
724
+ if (cause instanceof Error) {
725
+ return `${baseMessage}: ${cause.message}`;
726
+ }
727
+ return baseMessage;
728
+ }
729
+ /**
730
+ * Execute a streaming 2-phase batch trigger where items are streamed from an AsyncIterable.
731
+ * Unlike executeBatchTwoPhase, this doesn't know the count upfront.
622
732
  *
623
- * @returns {Promise<BatchByIdResult<TTask>>} A promise that resolves with the batch results, including
624
- * success/failure status and strongly-typed outputs for each task
733
+ * @param apiClient - The API client instance
734
+ * @param items - AsyncIterable of batch items
735
+ * @param options - Batch options including trace context settings
736
+ * @param options.spanParentAsLink - If true, child runs will have separate trace IDs with a link to parent.
737
+ * Use true for batchTrigger (fire-and-forget), false for batchTriggerAndWait.
738
+ * @param requestOptions - Optional request options
739
+ * @internal
740
+ */
741
+ async function executeBatchTwoPhaseStreaming(apiClient, items, options, requestOptions) {
742
+ // For streaming, we need to buffer items to get the count first
743
+ // This is because createBatch requires runCount upfront
744
+ // In the future, we could add a streaming-first endpoint that doesn't require this
745
+ const itemsArray = [];
746
+ for await (const item of items) {
747
+ itemsArray.push(item);
748
+ }
749
+ // Now we can use the regular 2-phase approach
750
+ return executeBatchTwoPhase(apiClient, itemsArray, options, requestOptions);
751
+ }
752
+ // ============================================================================
753
+ // Streaming Helpers
754
+ // ============================================================================
755
+ /**
756
+ * Type guard to check if a value is an AsyncIterable
757
+ */
758
+ function isAsyncIterable(value) {
759
+ return (value != null &&
760
+ typeof value === "object" &&
761
+ Symbol.asyncIterator in value &&
762
+ typeof value[Symbol.asyncIterator] === "function");
763
+ }
764
+ /**
765
+ * Type guard to check if a value is a ReadableStream
766
+ */
767
+ function isReadableStream(value) {
768
+ return (value != null &&
769
+ typeof value === "object" &&
770
+ "getReader" in value &&
771
+ typeof value.getReader === "function");
772
+ }
773
+ /**
774
+ * Convert a ReadableStream to an AsyncIterable.
775
+ * Properly cancels the stream when the consumer terminates early.
625
776
  *
626
- * @throws {Error} If called outside of a task.run() context
627
- * @throws {Error} If no API client is configured
777
+ * @internal Exported for testing purposes
778
+ */
779
+ export async function* readableStreamToAsyncIterable(stream) {
780
+ const reader = stream.getReader();
781
+ try {
782
+ while (true) {
783
+ const { done, value } = await reader.read();
784
+ if (done)
785
+ break;
786
+ yield value;
787
+ }
788
+ }
789
+ finally {
790
+ try {
791
+ await reader.cancel();
792
+ }
793
+ catch {
794
+ // Ignore errors - stream might already be errored or closed
795
+ }
796
+ reader.releaseLock();
797
+ }
798
+ }
799
+ /**
800
+ * Normalize stream input to AsyncIterable
801
+ */
802
+ function normalizeToAsyncIterable(input) {
803
+ if (isReadableStream(input)) {
804
+ return readableStreamToAsyncIterable(input);
805
+ }
806
+ return input;
807
+ }
808
+ /**
809
+ * Transform a stream of BatchByIdItem to BatchItemNDJSON format.
810
+ * Handles payload serialization and idempotency key generation.
628
811
  *
629
- * @example
630
- * ```ts
631
- * import { batch, task } from "@trigger.dev/sdk/v3";
812
+ * @internal
813
+ */
814
+ async function* transformBatchItemsStream(items, options) {
815
+ let index = 0;
816
+ for await (const item of items) {
817
+ const taskMetadata = resourceCatalog.getTask(item.id);
818
+ const parsedPayload = taskMetadata?.fns.parsePayload
819
+ ? await taskMetadata?.fns.parsePayload(item.payload)
820
+ : item.payload;
821
+ const payloadPacket = await stringifyIO(parsedPayload);
822
+ const batchItemIdempotencyKey = await makeIdempotencyKey(flattenIdempotencyKey([options?.idempotencyKey, `${index}`]));
823
+ yield {
824
+ index: index++,
825
+ task: item.id,
826
+ payload: payloadPacket.data,
827
+ options: {
828
+ queue: item.options?.queue ? { name: item.options.queue } : undefined,
829
+ concurrencyKey: item.options?.concurrencyKey,
830
+ test: taskContext.ctx?.run.isTest,
831
+ payloadType: payloadPacket.dataType,
832
+ delay: item.options?.delay,
833
+ ttl: item.options?.ttl,
834
+ tags: item.options?.tags,
835
+ maxAttempts: item.options?.maxAttempts,
836
+ metadata: item.options?.metadata,
837
+ maxDuration: item.options?.maxDuration,
838
+ idempotencyKey: (await makeIdempotencyKey(item.options?.idempotencyKey)) ?? batchItemIdempotencyKey,
839
+ idempotencyKeyTTL: item.options?.idempotencyKeyTTL ?? options?.idempotencyKeyTTL,
840
+ machine: item.options?.machine,
841
+ priority: item.options?.priority,
842
+ region: item.options?.region,
843
+ lockToVersion: item.options?.version ?? getEnvVar("TRIGGER_VERSION"),
844
+ debounce: item.options?.debounce,
845
+ },
846
+ };
847
+ }
848
+ }
849
+ /**
850
+ * Transform a stream of BatchByIdAndWaitItem to BatchItemNDJSON format for triggerAndWait.
851
+ * Uses the current worker version for lockToVersion.
632
852
  *
633
- * export const parentTask = task({
634
- * id: "parent-task",
635
- * run: async (payload: string) => {
636
- * const results = await batch.triggerAndWait<typeof childTask1 | typeof childTask2>([
637
- * {
638
- * id: "child-task-1",
639
- * payload: { foo: "World" },
640
- * options: {
641
- * queue: "default",
642
- * delay: "5m",
643
- * tags: ["batch", "child1"]
644
- * }
645
- * },
646
- * {
647
- * id: "child-task-2",
648
- * payload: { bar: 42 }
649
- * }
650
- * ]);
853
+ * @internal
854
+ */
855
+ async function* transformBatchItemsStreamForWait(items, options) {
856
+ let index = 0;
857
+ for await (const item of items) {
858
+ const taskMetadata = resourceCatalog.getTask(item.id);
859
+ const parsedPayload = taskMetadata?.fns.parsePayload
860
+ ? await taskMetadata?.fns.parsePayload(item.payload)
861
+ : item.payload;
862
+ const payloadPacket = await stringifyIO(parsedPayload);
863
+ const batchItemIdempotencyKey = await makeIdempotencyKey(flattenIdempotencyKey([options?.idempotencyKey, `${index}`]));
864
+ yield {
865
+ index: index++,
866
+ task: item.id,
867
+ payload: payloadPacket.data,
868
+ options: {
869
+ lockToVersion: taskContext.worker?.version,
870
+ queue: item.options?.queue ? { name: item.options.queue } : undefined,
871
+ concurrencyKey: item.options?.concurrencyKey,
872
+ test: taskContext.ctx?.run.isTest,
873
+ payloadType: payloadPacket.dataType,
874
+ delay: item.options?.delay,
875
+ ttl: item.options?.ttl,
876
+ tags: item.options?.tags,
877
+ maxAttempts: item.options?.maxAttempts,
878
+ metadata: item.options?.metadata,
879
+ maxDuration: item.options?.maxDuration,
880
+ idempotencyKey: (await makeIdempotencyKey(item.options?.idempotencyKey)) ?? batchItemIdempotencyKey,
881
+ idempotencyKeyTTL: item.options?.idempotencyKeyTTL ?? options?.idempotencyKeyTTL,
882
+ machine: item.options?.machine,
883
+ priority: item.options?.priority,
884
+ region: item.options?.region,
885
+ debounce: item.options?.debounce,
886
+ },
887
+ };
888
+ }
889
+ }
890
+ /**
891
+ * Transform a stream of BatchByTaskItem to BatchItemNDJSON format.
651
892
  *
652
- * // Type-safe result handling
653
- * for (const result of results) {
654
- * if (result.ok) {
655
- * switch (result.taskIdentifier) {
656
- * case "child-task-1":
657
- * console.log("Child task 1 output:", result.output); // string type
658
- * break;
659
- * case "child-task-2":
660
- * console.log("Child task 2 output:", result.output); // number type
661
- * break;
662
- * }
663
- * } else {
664
- * console.error("Task failed:", result.error);
665
- * }
666
- * }
667
- * }
668
- * });
669
- * ```
893
+ * @internal
894
+ */
895
+ async function* transformBatchByTaskItemsStream(items, options) {
896
+ let index = 0;
897
+ for await (const item of items) {
898
+ const taskMetadata = resourceCatalog.getTask(item.task.id);
899
+ const parsedPayload = taskMetadata?.fns.parsePayload
900
+ ? await taskMetadata?.fns.parsePayload(item.payload)
901
+ : item.payload;
902
+ const payloadPacket = await stringifyIO(parsedPayload);
903
+ const batchItemIdempotencyKey = await makeIdempotencyKey(flattenIdempotencyKey([options?.idempotencyKey, `${index}`]));
904
+ yield {
905
+ index: index++,
906
+ task: item.task.id,
907
+ payload: payloadPacket.data,
908
+ options: {
909
+ queue: item.options?.queue ? { name: item.options.queue } : undefined,
910
+ concurrencyKey: item.options?.concurrencyKey,
911
+ test: taskContext.ctx?.run.isTest,
912
+ payloadType: payloadPacket.dataType,
913
+ delay: item.options?.delay,
914
+ ttl: item.options?.ttl,
915
+ tags: item.options?.tags,
916
+ maxAttempts: item.options?.maxAttempts,
917
+ metadata: item.options?.metadata,
918
+ maxDuration: item.options?.maxDuration,
919
+ idempotencyKey: (await makeIdempotencyKey(item.options?.idempotencyKey)) ?? batchItemIdempotencyKey,
920
+ idempotencyKeyTTL: item.options?.idempotencyKeyTTL ?? options?.idempotencyKeyTTL,
921
+ machine: item.options?.machine,
922
+ priority: item.options?.priority,
923
+ region: item.options?.region,
924
+ lockToVersion: item.options?.version ?? getEnvVar("TRIGGER_VERSION"),
925
+ debounce: item.options?.debounce,
926
+ },
927
+ };
928
+ }
929
+ }
930
+ /**
931
+ * Transform a stream of BatchByTaskAndWaitItem to BatchItemNDJSON format for triggerAndWait.
670
932
  *
671
- * @description
672
- * Each task item in the array can include:
673
- * - `id`: The task identifier (must match one of the tasks in the union type)
674
- * - `payload`: Strongly-typed payload matching the task's input type
675
- * - `options`: Optional task-specific settings including:
676
- * - `queue`: Specify a queue for the task
677
- * - `concurrencyKey`: Control concurrent execution
678
- * - `delay`: Delay before task execution
679
- * - `ttl`: Time-to-live for the task
680
- * - `tags`: Array of tags for the task
681
- * - `maxAttempts`: Maximum retry attempts
682
- * - `metadata`: Additional metadata
683
- * - `maxDuration`: Maximum execution duration
933
+ * @internal
934
+ */
935
+ async function* transformBatchByTaskItemsStreamForWait(items, options) {
936
+ let index = 0;
937
+ for await (const item of items) {
938
+ const taskMetadata = resourceCatalog.getTask(item.task.id);
939
+ const parsedPayload = taskMetadata?.fns.parsePayload
940
+ ? await taskMetadata?.fns.parsePayload(item.payload)
941
+ : item.payload;
942
+ const payloadPacket = await stringifyIO(parsedPayload);
943
+ const batchItemIdempotencyKey = await makeIdempotencyKey(flattenIdempotencyKey([options?.idempotencyKey, `${index}`]));
944
+ yield {
945
+ index: index++,
946
+ task: item.task.id,
947
+ payload: payloadPacket.data,
948
+ options: {
949
+ lockToVersion: taskContext.worker?.version,
950
+ queue: item.options?.queue ? { name: item.options.queue } : undefined,
951
+ concurrencyKey: item.options?.concurrencyKey,
952
+ test: taskContext.ctx?.run.isTest,
953
+ payloadType: payloadPacket.dataType,
954
+ delay: item.options?.delay,
955
+ ttl: item.options?.ttl,
956
+ tags: item.options?.tags,
957
+ maxAttempts: item.options?.maxAttempts,
958
+ metadata: item.options?.metadata,
959
+ maxDuration: item.options?.maxDuration,
960
+ idempotencyKey: (await makeIdempotencyKey(item.options?.idempotencyKey)) ?? batchItemIdempotencyKey,
961
+ idempotencyKeyTTL: item.options?.idempotencyKeyTTL ?? options?.idempotencyKeyTTL,
962
+ machine: item.options?.machine,
963
+ priority: item.options?.priority,
964
+ region: item.options?.region,
965
+ debounce: item.options?.debounce,
966
+ },
967
+ };
968
+ }
969
+ }
970
+ /**
971
+ * Transform a stream of BatchItem (single task type) to BatchItemNDJSON format.
684
972
  *
685
- * The function provides full type safety for:
686
- * - Task IDs
687
- * - Payload types
688
- * - Return value types
689
- * - Error handling
973
+ * @internal
690
974
  */
691
- export async function batchTriggerAndWaitTasks(items, options, requestOptions) {
692
- const ctx = taskContext.ctx;
693
- if (!ctx) {
694
- throw new Error("batchTriggerAndWait can only be used from inside a task.run()");
975
+ async function* transformSingleTaskBatchItemsStream(taskIdentifier, items, parsePayload, options, queue) {
976
+ let index = 0;
977
+ for await (const item of items) {
978
+ const parsedPayload = parsePayload ? await parsePayload(item.payload) : item.payload;
979
+ const payloadPacket = await stringifyIO(parsedPayload);
980
+ const batchItemIdempotencyKey = await makeIdempotencyKey(flattenIdempotencyKey([options?.idempotencyKey, `${index}`]));
981
+ yield {
982
+ index: index++,
983
+ task: taskIdentifier,
984
+ payload: payloadPacket.data,
985
+ options: {
986
+ queue: item.options?.queue
987
+ ? { name: item.options.queue }
988
+ : queue
989
+ ? { name: queue }
990
+ : undefined,
991
+ concurrencyKey: item.options?.concurrencyKey,
992
+ test: taskContext.ctx?.run.isTest,
993
+ payloadType: payloadPacket.dataType,
994
+ delay: item.options?.delay,
995
+ ttl: item.options?.ttl,
996
+ tags: item.options?.tags,
997
+ maxAttempts: item.options?.maxAttempts,
998
+ metadata: item.options?.metadata,
999
+ maxDuration: item.options?.maxDuration,
1000
+ idempotencyKey: (await makeIdempotencyKey(item.options?.idempotencyKey)) ?? batchItemIdempotencyKey,
1001
+ idempotencyKeyTTL: item.options?.idempotencyKeyTTL ?? options?.idempotencyKeyTTL,
1002
+ machine: item.options?.machine,
1003
+ priority: item.options?.priority,
1004
+ region: item.options?.region,
1005
+ lockToVersion: item.options?.version ?? getEnvVar("TRIGGER_VERSION"),
1006
+ debounce: item.options?.debounce,
1007
+ },
1008
+ };
695
1009
  }
696
- const apiClient = apiClientManager.clientOrThrow(requestOptions?.clientConfig);
697
- return await tracer.startActiveSpan("batch.triggerByTaskAndWait()", async (span) => {
698
- const response = await apiClient.batchTriggerV3({
699
- items: await Promise.all(items.map(async (item, index) => {
700
- const taskMetadata = resourceCatalog.getTask(item.task.id);
701
- const parsedPayload = taskMetadata?.fns.parsePayload
702
- ? await taskMetadata?.fns.parsePayload(item.payload)
703
- : item.payload;
704
- const payloadPacket = await stringifyIO(parsedPayload);
705
- const batchItemIdempotencyKey = await makeIdempotencyKey(flattenIdempotencyKey([options?.idempotencyKey, `${index}`]));
706
- return {
707
- task: item.task.id,
708
- payload: payloadPacket.data,
709
- options: {
710
- lockToVersion: taskContext.worker?.version,
711
- queue: item.options?.queue ? { name: item.options.queue } : undefined,
712
- concurrencyKey: item.options?.concurrencyKey,
713
- test: taskContext.ctx?.run.isTest,
714
- payloadType: payloadPacket.dataType,
715
- delay: item.options?.delay,
716
- ttl: item.options?.ttl,
717
- tags: item.options?.tags,
718
- maxAttempts: item.options?.maxAttempts,
719
- metadata: item.options?.metadata,
720
- maxDuration: item.options?.maxDuration,
721
- idempotencyKey: (await makeIdempotencyKey(item.options?.idempotencyKey)) ??
722
- batchItemIdempotencyKey,
723
- idempotencyKeyTTL: item.options?.idempotencyKeyTTL ?? options?.idempotencyKeyTTL,
724
- machine: item.options?.machine,
725
- priority: item.options?.priority,
726
- region: item.options?.region,
727
- },
728
- };
729
- })),
730
- parentRunId: ctx.run.id,
731
- resumeParentOnCompletion: true,
732
- }, {
733
- processingStrategy: options?.triggerSequentially ? "sequential" : undefined,
734
- }, requestOptions);
735
- span.setAttribute("batchId", response.id);
736
- span.setAttribute("runCount", response.runCount);
737
- const result = await runtime.waitForBatch({
738
- id: response.id,
739
- runCount: response.runCount,
740
- ctx,
741
- });
742
- const runs = await handleBatchTaskRunExecutionResultV2(result.items);
743
- return {
744
- id: result.id,
745
- runs,
1010
+ }
1011
+ /**
1012
+ * Transform a stream of BatchTriggerAndWaitItem (single task type) to BatchItemNDJSON format.
1013
+ *
1014
+ * @internal
1015
+ */
1016
+ async function* transformSingleTaskBatchItemsStreamForWait(taskIdentifier, items, parsePayload, options, queue) {
1017
+ let index = 0;
1018
+ for await (const item of items) {
1019
+ const parsedPayload = parsePayload ? await parsePayload(item.payload) : item.payload;
1020
+ const payloadPacket = await stringifyIO(parsedPayload);
1021
+ const batchItemIdempotencyKey = await makeIdempotencyKey(flattenIdempotencyKey([options?.idempotencyKey, `${index}`]));
1022
+ yield {
1023
+ index: index++,
1024
+ task: taskIdentifier,
1025
+ payload: payloadPacket.data,
1026
+ options: {
1027
+ lockToVersion: taskContext.worker?.version,
1028
+ queue: item.options?.queue
1029
+ ? { name: item.options.queue }
1030
+ : queue
1031
+ ? { name: queue }
1032
+ : undefined,
1033
+ concurrencyKey: item.options?.concurrencyKey,
1034
+ test: taskContext.ctx?.run.isTest,
1035
+ payloadType: payloadPacket.dataType,
1036
+ delay: item.options?.delay,
1037
+ ttl: item.options?.ttl,
1038
+ tags: item.options?.tags,
1039
+ maxAttempts: item.options?.maxAttempts,
1040
+ metadata: item.options?.metadata,
1041
+ maxDuration: item.options?.maxDuration,
1042
+ idempotencyKey: (await makeIdempotencyKey(item.options?.idempotencyKey)) ?? batchItemIdempotencyKey,
1043
+ idempotencyKeyTTL: item.options?.idempotencyKeyTTL ?? options?.idempotencyKeyTTL,
1044
+ machine: item.options?.machine,
1045
+ priority: item.options?.priority,
1046
+ region: item.options?.region,
1047
+ debounce: item.options?.debounce,
1048
+ },
746
1049
  };
747
- }, {
748
- kind: SpanKind.PRODUCER,
749
- attributes: {
750
- [SemanticInternalAttributes.STYLE_ICON]: "trigger",
751
- },
752
- });
1050
+ }
753
1051
  }
754
1052
  async function trigger_internal(name, id, payload, parsePayload, options, requestOptions) {
755
1053
  const apiClient = apiClientManager.clientOrThrow(requestOptions?.clientConfig);
@@ -775,6 +1073,7 @@ async function trigger_internal(name, id, payload, parsePayload, options, reques
775
1073
  priority: options?.priority,
776
1074
  region: options?.region,
777
1075
  lockToVersion: options?.version ?? getEnvVar("TRIGGER_VERSION"),
1076
+ debounce: options?.debounce,
778
1077
  },
779
1078
  }, {
780
1079
  spanParentAsLink: true,
@@ -796,12 +1095,15 @@ async function trigger_internal(name, id, payload, parsePayload, options, reques
796
1095
  async function batchTrigger_internal(name, taskIdentifier, items, options, parsePayload, requestOptions, queue) {
797
1096
  const apiClient = apiClientManager.clientOrThrow(requestOptions?.clientConfig);
798
1097
  const ctx = taskContext.ctx;
799
- const response = await apiClient.batchTriggerV3({
800
- items: await Promise.all(items.map(async (item, index) => {
1098
+ // Check if items is an array or a stream
1099
+ if (Array.isArray(items)) {
1100
+ // Prepare items as BatchItemNDJSON
1101
+ const ndJsonItems = await Promise.all(items.map(async (item, index) => {
801
1102
  const parsedPayload = parsePayload ? await parsePayload(item.payload) : item.payload;
802
1103
  const payloadPacket = await stringifyIO(parsedPayload);
803
1104
  const batchItemIdempotencyKey = await makeIdempotencyKey(flattenIdempotencyKey([options?.idempotencyKey, `${index}`]));
804
1105
  return {
1106
+ index,
805
1107
  task: taskIdentifier,
806
1108
  payload: payloadPacket.data,
807
1109
  options: {
@@ -827,33 +1129,75 @@ async function batchTrigger_internal(name, taskIdentifier, items, options, parse
827
1129
  lockToVersion: item.options?.version ?? getEnvVar("TRIGGER_VERSION"),
828
1130
  },
829
1131
  };
830
- })),
831
- parentRunId: ctx?.run.id,
832
- }, {
833
- spanParentAsLink: true,
834
- processingStrategy: options?.triggerSequentially ? "sequential" : undefined,
835
- }, {
836
- name,
837
- tracer,
838
- icon: "trigger",
839
- onResponseBody(body, span) {
840
- if (body && typeof body === "object" && !Array.isArray(body)) {
841
- if ("id" in body && typeof body.id === "string") {
842
- span.setAttribute("batchId", body.id);
843
- }
844
- if ("runCount" in body && Array.isArray(body.runCount)) {
845
- span.setAttribute("runCount", body.runCount);
846
- }
847
- }
848
- },
849
- ...requestOptions,
850
- });
851
- const handle = {
852
- batchId: response.id,
853
- runCount: response.runCount,
854
- publicAccessToken: response.publicAccessToken,
855
- };
856
- return handle;
1132
+ }));
1133
+ // Execute 2-phase batch
1134
+ const response = await tracer.startActiveSpan(name, async (span) => {
1135
+ const result = await executeBatchTwoPhase(apiClient, ndJsonItems, {
1136
+ parentRunId: ctx?.run.id,
1137
+ idempotencyKey: await makeIdempotencyKey(options?.idempotencyKey),
1138
+ spanParentAsLink: true, // Fire-and-forget: child runs get separate trace IDs
1139
+ }, requestOptions);
1140
+ span.setAttribute("batchId", result.id);
1141
+ span.setAttribute("runCount", result.runCount);
1142
+ return result;
1143
+ }, {
1144
+ kind: SpanKind.PRODUCER,
1145
+ attributes: {
1146
+ [SemanticInternalAttributes.STYLE_ICON]: "trigger",
1147
+ ...accessoryAttributes({
1148
+ items: [
1149
+ {
1150
+ text: taskIdentifier,
1151
+ variant: "normal",
1152
+ },
1153
+ ],
1154
+ style: "codepath",
1155
+ }),
1156
+ },
1157
+ });
1158
+ const handle = {
1159
+ batchId: response.id,
1160
+ runCount: response.runCount,
1161
+ publicAccessToken: response.publicAccessToken,
1162
+ };
1163
+ return handle;
1164
+ }
1165
+ else {
1166
+ // Stream path: convert to AsyncIterable and transform
1167
+ const asyncItems = normalizeToAsyncIterable(items);
1168
+ const transformedItems = transformSingleTaskBatchItemsStream(taskIdentifier, asyncItems, parsePayload, options, queue);
1169
+ // Execute streaming 2-phase batch
1170
+ const response = await tracer.startActiveSpan(name, async (span) => {
1171
+ const result = await executeBatchTwoPhaseStreaming(apiClient, transformedItems, {
1172
+ parentRunId: ctx?.run.id,
1173
+ idempotencyKey: await makeIdempotencyKey(options?.idempotencyKey),
1174
+ spanParentAsLink: true, // Fire-and-forget: child runs get separate trace IDs
1175
+ }, requestOptions);
1176
+ span.setAttribute("batchId", result.id);
1177
+ span.setAttribute("runCount", result.runCount);
1178
+ return result;
1179
+ }, {
1180
+ kind: SpanKind.PRODUCER,
1181
+ attributes: {
1182
+ [SemanticInternalAttributes.STYLE_ICON]: "trigger",
1183
+ ...accessoryAttributes({
1184
+ items: [
1185
+ {
1186
+ text: taskIdentifier,
1187
+ variant: "normal",
1188
+ },
1189
+ ],
1190
+ style: "codepath",
1191
+ }),
1192
+ },
1193
+ });
1194
+ const handle = {
1195
+ batchId: response.id,
1196
+ runCount: response.runCount,
1197
+ publicAccessToken: response.publicAccessToken,
1198
+ };
1199
+ return handle;
1200
+ }
857
1201
  }
858
1202
  async function triggerAndWait_internal(name, id, payload, parsePayload, options, requestOptions) {
859
1203
  const ctx = taskContext.ctx;
@@ -885,6 +1229,7 @@ async function triggerAndWait_internal(name, id, payload, parsePayload, options,
885
1229
  machine: options?.machine,
886
1230
  priority: options?.priority,
887
1231
  region: options?.region,
1232
+ debounce: options?.debounce,
888
1233
  },
889
1234
  }, {}, requestOptions);
890
1235
  span.setAttribute("runId", response.id);
@@ -915,72 +1260,117 @@ async function batchTriggerAndWait_internal(name, id, items, parsePayload, optio
915
1260
  throw new Error("batchTriggerAndWait can only be used from inside a task.run()");
916
1261
  }
917
1262
  const apiClient = apiClientManager.clientOrThrow(requestOptions?.clientConfig);
918
- return await tracer.startActiveSpan(name, async (span) => {
919
- const response = await apiClient.batchTriggerV3({
920
- items: await Promise.all(items.map(async (item, index) => {
921
- const parsedPayload = parsePayload ? await parsePayload(item.payload) : item.payload;
922
- const payloadPacket = await stringifyIO(parsedPayload);
923
- const batchItemIdempotencyKey = await makeIdempotencyKey(flattenIdempotencyKey([options?.idempotencyKey, `${index}`]));
924
- return {
925
- task: id,
926
- payload: payloadPacket.data,
927
- options: {
928
- lockToVersion: taskContext.worker?.version,
929
- queue: item.options?.queue
930
- ? { name: item.options.queue }
931
- : queue
932
- ? { name: queue }
933
- : undefined,
934
- concurrencyKey: item.options?.concurrencyKey,
935
- test: taskContext.ctx?.run.isTest,
936
- payloadType: payloadPacket.dataType,
937
- delay: item.options?.delay,
938
- ttl: item.options?.ttl,
939
- tags: item.options?.tags,
940
- maxAttempts: item.options?.maxAttempts,
941
- metadata: item.options?.metadata,
942
- maxDuration: item.options?.maxDuration,
943
- idempotencyKey: (await makeIdempotencyKey(item.options?.idempotencyKey)) ??
944
- batchItemIdempotencyKey,
945
- idempotencyKeyTTL: item.options?.idempotencyKeyTTL ?? options?.idempotencyKeyTTL,
946
- machine: item.options?.machine,
947
- priority: item.options?.priority,
948
- region: item.options?.region,
949
- },
950
- };
951
- })),
952
- resumeParentOnCompletion: true,
953
- parentRunId: ctx.run.id,
1263
+ // Check if items is an array or a stream
1264
+ if (Array.isArray(items)) {
1265
+ // Prepare items as BatchItemNDJSON
1266
+ const ndJsonItems = await Promise.all(items.map(async (item, index) => {
1267
+ const parsedPayload = parsePayload ? await parsePayload(item.payload) : item.payload;
1268
+ const payloadPacket = await stringifyIO(parsedPayload);
1269
+ const batchItemIdempotencyKey = await makeIdempotencyKey(flattenIdempotencyKey([options?.idempotencyKey, `${index}`]));
1270
+ return {
1271
+ index,
1272
+ task: id,
1273
+ payload: payloadPacket.data,
1274
+ options: {
1275
+ lockToVersion: taskContext.worker?.version,
1276
+ queue: item.options?.queue
1277
+ ? { name: item.options.queue }
1278
+ : queue
1279
+ ? { name: queue }
1280
+ : undefined,
1281
+ concurrencyKey: item.options?.concurrencyKey,
1282
+ test: taskContext.ctx?.run.isTest,
1283
+ payloadType: payloadPacket.dataType,
1284
+ delay: item.options?.delay,
1285
+ ttl: item.options?.ttl,
1286
+ tags: item.options?.tags,
1287
+ maxAttempts: item.options?.maxAttempts,
1288
+ metadata: item.options?.metadata,
1289
+ maxDuration: item.options?.maxDuration,
1290
+ idempotencyKey: (await makeIdempotencyKey(item.options?.idempotencyKey)) ?? batchItemIdempotencyKey,
1291
+ idempotencyKeyTTL: item.options?.idempotencyKeyTTL ?? options?.idempotencyKeyTTL,
1292
+ machine: item.options?.machine,
1293
+ priority: item.options?.priority,
1294
+ region: item.options?.region,
1295
+ },
1296
+ };
1297
+ }));
1298
+ return await tracer.startActiveSpan(name, async (span) => {
1299
+ // Execute 2-phase batch
1300
+ const response = await executeBatchTwoPhase(apiClient, ndJsonItems, {
1301
+ parentRunId: ctx.run.id,
1302
+ resumeParentOnCompletion: true,
1303
+ idempotencyKey: await makeIdempotencyKey(options?.idempotencyKey),
1304
+ spanParentAsLink: false, // Waiting: child runs share parent's trace ID
1305
+ }, requestOptions);
1306
+ span.setAttribute("batchId", response.id);
1307
+ span.setAttribute("runCount", response.runCount);
1308
+ const result = await runtime.waitForBatch({
1309
+ id: response.id,
1310
+ runCount: response.runCount,
1311
+ ctx,
1312
+ });
1313
+ const runs = await handleBatchTaskRunExecutionResult(result.items, id);
1314
+ return {
1315
+ id: result.id,
1316
+ runs,
1317
+ };
954
1318
  }, {
955
- processingStrategy: options?.triggerSequentially ? "sequential" : undefined,
956
- }, requestOptions);
957
- span.setAttribute("batchId", response.id);
958
- span.setAttribute("runCount", response.runCount);
959
- const result = await runtime.waitForBatch({
960
- id: response.id,
961
- runCount: response.runCount,
962
- ctx,
1319
+ kind: SpanKind.PRODUCER,
1320
+ attributes: {
1321
+ [SemanticInternalAttributes.STYLE_ICON]: "trigger",
1322
+ ...accessoryAttributes({
1323
+ items: [
1324
+ {
1325
+ text: id,
1326
+ variant: "normal",
1327
+ },
1328
+ ],
1329
+ style: "codepath",
1330
+ }),
1331
+ },
963
1332
  });
964
- const runs = await handleBatchTaskRunExecutionResult(result.items, id);
965
- return {
966
- id: result.id,
967
- runs,
968
- };
969
- }, {
970
- kind: SpanKind.PRODUCER,
971
- attributes: {
972
- [SemanticInternalAttributes.STYLE_ICON]: "trigger",
973
- ...accessoryAttributes({
974
- items: [
975
- {
976
- text: id,
977
- variant: "normal",
978
- },
979
- ],
980
- style: "codepath",
981
- }),
982
- },
983
- });
1333
+ }
1334
+ else {
1335
+ // Stream path: convert to AsyncIterable and transform
1336
+ const asyncItems = normalizeToAsyncIterable(items);
1337
+ const transformedItems = transformSingleTaskBatchItemsStreamForWait(id, asyncItems, parsePayload, options, queue);
1338
+ return await tracer.startActiveSpan(name, async (span) => {
1339
+ // Execute streaming 2-phase batch
1340
+ const response = await executeBatchTwoPhaseStreaming(apiClient, transformedItems, {
1341
+ parentRunId: ctx.run.id,
1342
+ resumeParentOnCompletion: true,
1343
+ idempotencyKey: await makeIdempotencyKey(options?.idempotencyKey),
1344
+ spanParentAsLink: false, // Waiting: child runs share parent's trace ID
1345
+ }, requestOptions);
1346
+ span.setAttribute("batchId", response.id);
1347
+ span.setAttribute("runCount", response.runCount);
1348
+ const result = await runtime.waitForBatch({
1349
+ id: response.id,
1350
+ runCount: response.runCount,
1351
+ ctx,
1352
+ });
1353
+ const runs = await handleBatchTaskRunExecutionResult(result.items, id);
1354
+ return {
1355
+ id: result.id,
1356
+ runs,
1357
+ };
1358
+ }, {
1359
+ kind: SpanKind.PRODUCER,
1360
+ attributes: {
1361
+ [SemanticInternalAttributes.STYLE_ICON]: "trigger",
1362
+ ...accessoryAttributes({
1363
+ items: [
1364
+ {
1365
+ text: id,
1366
+ variant: "normal",
1367
+ },
1368
+ ],
1369
+ style: "codepath",
1370
+ }),
1371
+ },
1372
+ });
1373
+ }
984
1374
  }
985
1375
  async function handleBatchTaskRunExecutionResult(items, taskIdentifier) {
986
1376
  const someObjectStoreOutputs = items.some((item) => item.ok && item.outputType === "application/store");