@@ -23,6 +23,7 @@ import {
2323 getSchemaParseFn ,
2424 lifecycleHooks ,
2525 makeIdempotencyKey ,
26+ packetRequiresOffloading ,
2627 parsePacket ,
2728 RateLimitError ,
2829 resourceCatalog ,
@@ -1655,6 +1656,21 @@ async function executeBatchTwoPhase(
16551656 } ,
16561657 requestOptions ?: TriggerApiRequestOptions
16571658) : Promise < { id : string ; runCount : number ; publicAccessToken : string ; taskIdentifiers : string [ ] } > {
1659+ // Offload any oversized per-item payloads to object storage before streaming, so a batch
1660+ // of large items keeps the request body small — the same treatment single triggers get.
1661+ // Both the array and streaming batch paths funnel through here, so this is the one place
1662+ // batch offloading needs to happen. Wrap failures so a presign/upload error carries the
1663+ // same batch context (phase, itemCount, rate-limit info) as the create/stream phases.
1664+ try {
1665+ items = await offloadBatchItemPayloads ( items , apiClient ) ;
1666+ } catch ( error ) {
1667+ throw new BatchTriggerError ( `Failed to offload payloads for batch with ${ items . length } items` , {
1668+ cause : error ,
1669+ phase : "offload" ,
1670+ itemCount : items . length ,
1671+ } ) ;
1672+ }
1673+
16581674 let batch : Awaited < ReturnType < typeof apiClient . createBatch > > | undefined ;
16591675
16601676 try {
@@ -1701,6 +1717,81 @@ async function executeBatchTwoPhase(
17011717 } ;
17021718}
17031719
1720+ /**
1721+ * Offload any oversized per-item payloads in a batch to object storage, mirroring the
1722+ * single-trigger offload path. Small payloads pass through untouched. Every returned item
1723+ * carries `options.payloadSize` (the pre-offload serialized byte size) so the pipeline can
1724+ * reason about payload size without downloading an "application/store" reference.
1725+ *
1726+ * Uploads run with bounded concurrency so a batch of large items doesn't fire an unbounded
1727+ * number of simultaneous presigned PUTs.
1728+ *
1729+ * @internal Exported for testing.
1730+ */
1731+ export async function offloadBatchItemPayloads (
1732+ items : BatchItemNDJSON [ ] ,
1733+ apiClient : ApiClient ,
1734+ concurrency = 10
1735+ ) : Promise < BatchItemNDJSON [ ] > {
1736+ if ( items . length === 0 ) {
1737+ return items ;
1738+ }
1739+
1740+ const results = new Array < BatchItemNDJSON > ( items . length ) ;
1741+ let cursor = 0 ;
1742+
1743+ async function worker ( ) {
1744+ while ( cursor < items . length ) {
1745+ const index = cursor ++ ;
1746+ results [ index ] = await offloadBatchItemPayload ( items [ index ] ! , apiClient ) ;
1747+ }
1748+ }
1749+
1750+ await Promise . all ( Array . from ( { length : Math . min ( concurrency , items . length ) } , ( ) => worker ( ) ) ) ;
1751+
1752+ return results ;
1753+ }
1754+
1755+ async function offloadBatchItemPayload (
1756+ item : BatchItemNDJSON ,
1757+ apiClient : ApiClient
1758+ ) : Promise < BatchItemNDJSON > {
1759+ // The batch builders serialize each payload to a string via stringifyIO before we get
1760+ // here; anything else has no inline body to offload or measure.
1761+ if ( typeof item . payload !== "string" || item . payload . length === 0 ) {
1762+ return item ;
1763+ }
1764+
1765+ const dataType = item . options ?. payloadType ?? "application/json" ;
1766+
1767+ // Already an object-store reference — the caller pre-offloaded it, nothing to do.
1768+ if ( dataType === "application/store" ) {
1769+ return item ;
1770+ }
1771+
1772+ const packet : IOPacket = { data : item . payload , dataType } ;
1773+ // Measure before offload so the size reflects the real payload, not the small reference
1774+ // we may replace it with.
1775+ const { size : payloadSize } = packetRequiresOffloading ( packet ) ;
1776+
1777+ const exported = await conditionallyExportPacket (
1778+ packet ,
1779+ createTriggerPayloadPathPrefix ( item . task ) ,
1780+ undefined ,
1781+ apiClient
1782+ ) ;
1783+
1784+ return {
1785+ ...item ,
1786+ payload : exported . data ,
1787+ options : {
1788+ ...item . options ,
1789+ payloadType : exported . dataType ,
1790+ payloadSize,
1791+ } ,
1792+ } ;
1793+ }
1794+
17041795/**
17051796 * Error thrown when batch trigger operations fail.
17061797 * Includes context about which phase failed and the batch details.
@@ -1710,7 +1801,7 @@ async function executeBatchTwoPhase(
17101801 * - `retryAfterMs`: milliseconds until the rate limit resets
17111802 */
17121803export class BatchTriggerError extends Error {
1713- readonly phase : "create" | "stream" ;
1804+ readonly phase : "offload" | " create" | "stream" ;
17141805 readonly batchId ?: string ;
17151806 readonly itemCount : number ;
17161807
@@ -1730,7 +1821,7 @@ export class BatchTriggerError extends Error {
17301821 message : string ,
17311822 options : {
17321823 cause ?: unknown ;
1733- phase : "create" | "stream" ;
1824+ phase : "offload" | " create" | "stream" ;
17341825 batchId ?: string ;
17351826 itemCount : number ;
17361827 }
@@ -2205,7 +2296,11 @@ async function trigger_internal<TRunTypes extends AnyRunTypes>(
22052296 const apiClient = apiClientManager . clientOrThrow ( requestOptions ?. clientConfig ) ;
22062297
22072298 const parsedPayload = parsePayload ? await parsePayload ( payload ) : payload ;
2208- const triggerPayloadPacket = await prepareTriggerPayload ( parsedPayload , apiClient , id ) ;
2299+ const { packet : triggerPayloadPacket , payloadSize } = await prepareTriggerPayload (
2300+ parsedPayload ,
2301+ apiClient ,
2302+ id
2303+ ) ;
22092304
22102305 // Process idempotency key and extract options for storage
22112306 const processedIdempotencyKey = await makeIdempotencyKey ( options ?. idempotencyKey ) ;
@@ -2222,6 +2317,7 @@ async function trigger_internal<TRunTypes extends AnyRunTypes>(
22222317 concurrencyKey : options ?. concurrencyKey ,
22232318 test : taskContext . ctx ?. run . isTest ,
22242319 payloadType : triggerPayloadPacket . dataType ,
2320+ payloadSize,
22252321 idempotencyKey : processedIdempotencyKey ?. toString ( ) ,
22262322 idempotencyKeyTTL : options ?. idempotencyKeyTTL ,
22272323 idempotencyKeyOptions,
@@ -2460,7 +2556,11 @@ async function triggerAndWait_internal<TIdentifier extends string, TPayload, TOu
24602556 const apiClient = apiClientManager . clientOrThrow ( requestOptions ?. clientConfig ) ;
24612557
24622558 const parsedPayload = parsePayload ? await parsePayload ( payload ) : payload ;
2463- const triggerPayloadPacket = await prepareTriggerPayload ( parsedPayload , apiClient , id ) ;
2559+ const { packet : triggerPayloadPacket , payloadSize } = await prepareTriggerPayload (
2560+ parsedPayload ,
2561+ apiClient ,
2562+ id
2563+ ) ;
24642564
24652565 // Process idempotency key and extract options for storage
24662566 const processedIdempotencyKey = await makeIdempotencyKey ( options ?. idempotencyKey ) ;
@@ -2481,6 +2581,7 @@ async function triggerAndWait_internal<TIdentifier extends string, TPayload, TOu
24812581 concurrencyKey : options ?. concurrencyKey ,
24822582 test : taskContext . ctx ?. run . isTest ,
24832583 payloadType : triggerPayloadPacket . dataType ,
2584+ payloadSize,
24842585 delay : options ?. delay ,
24852586 ttl : options ?. ttl ,
24862587 tags : options ?. tags ,
@@ -2546,7 +2647,11 @@ async function triggerAndSubscribe_internal<TIdentifier extends string, TPayload
25462647 const apiClient = apiClientManager . clientOrThrow ( requestOptions ?. clientConfig ) ;
25472648
25482649 const parsedPayload = parsePayload ? await parsePayload ( payload ) : payload ;
2549- const triggerPayloadPacket = await prepareTriggerPayload ( parsedPayload , apiClient , id ) ;
2650+ const { packet : triggerPayloadPacket , payloadSize } = await prepareTriggerPayload (
2651+ parsedPayload ,
2652+ apiClient ,
2653+ id
2654+ ) ;
25502655
25512656 const processedIdempotencyKey = await makeIdempotencyKey ( options ?. idempotencyKey ) ;
25522657 const idempotencyKeyOptions = processedIdempotencyKey
@@ -2566,6 +2671,7 @@ async function triggerAndSubscribe_internal<TIdentifier extends string, TPayload
25662671 concurrencyKey : options ?. concurrencyKey ,
25672672 test : taskContext . ctx ?. run . isTest ,
25682673 payloadType : triggerPayloadPacket . dataType ,
2674+ payloadSize,
25692675 delay : options ?. delay ,
25702676 ttl : options ?. ttl ,
25712677 tags : options ?. tags ,
@@ -3070,14 +3176,18 @@ async function prepareTriggerPayload(
30703176 payload : unknown ,
30713177 apiClient : ApiClient ,
30723178 taskId : string
3073- ) : Promise < IOPacket > {
3179+ ) : Promise < { packet : IOPacket ; payloadSize : number } > {
30743180 const payloadPacket = await stringifyIO ( payload ) ;
3075- return await conditionallyExportPacket (
3181+ // Measure the serialized size before any offload, so it reflects the real payload
3182+ // size rather than the small "application/store" reference we may send instead.
3183+ const { size : payloadSize } = packetRequiresOffloading ( payloadPacket ) ;
3184+ const packet = await conditionallyExportPacket (
30763185 payloadPacket ,
30773186 createTriggerPayloadPathPrefix ( taskId ) ,
30783187 undefined ,
30793188 apiClient
30803189 ) ;
3190+ return { packet, payloadSize } ;
30813191}
30823192
30833193function createTriggerPayloadPathPrefix ( taskId : string ) : string {
0 commit comments