@@ -146,6 +146,310 @@ describe('BatchBuilder', () => {
146146 } )
147147 } )
148148
149+ describe ( 'chunked batch execution' , ( ) => {
150+ type ChunkingTestCase = {
151+ requestCount : number
152+ expectedChunks : number
153+ expectedChunkSizes : number [ ]
154+ idOffset : number
155+ description : string
156+ verifyOrder : boolean
157+ }
158+
159+ type MockUser = {
160+ id : number
161+ name : string
162+ email : string
163+ user_type : string
164+ short_name : string
165+ timezone : string
166+ removed : boolean
167+ bot : boolean
168+ version : number
169+ }
170+
171+ const basicChunkingTests : ChunkingTestCase [ ] = [
172+ {
173+ requestCount : 10 ,
174+ expectedChunks : 1 ,
175+ expectedChunkSizes : [ 10 ] ,
176+ idOffset : 400 ,
177+ description : 'should handle exactly 10 requests in a single batch' ,
178+ verifyOrder : true ,
179+ } ,
180+ {
181+ requestCount : 11 ,
182+ expectedChunks : 2 ,
183+ expectedChunkSizes : [ 10 , 1 ] ,
184+ idOffset : 500 ,
185+ description : 'should chunk 11 requests into two parallel batches' ,
186+ verifyOrder : true ,
187+ } ,
188+ {
189+ requestCount : 25 ,
190+ expectedChunks : 3 ,
191+ expectedChunkSizes : [ 10 , 10 , 5 ] ,
192+ idOffset : 600 ,
193+ description : 'should chunk 25 requests into three parallel batches' ,
194+ verifyOrder : false , // Parallel execution may not preserve exact order due to MSW limitations
195+ } ,
196+ {
197+ requestCount : 33 ,
198+ expectedChunks : 4 ,
199+ expectedChunkSizes : [ 10 , 10 , 10 , 3 ] ,
200+ idOffset : 1000 ,
201+ description : 'should chunk 33 requests into four parallel batches' ,
202+ verifyOrder : false ,
203+ } ,
204+ ]
205+
206+ test . each ( basicChunkingTests ) (
207+ '$description' ,
208+ async ( { requestCount, expectedChunks, expectedChunkSizes, idOffset, verifyOrder } ) => {
209+ // Create mock users
210+ const mockUsers : MockUser [ ] = Array . from ( { length : requestCount } , ( _ , i ) => ( {
211+ id : idOffset + i ,
212+ name : `User ${ i + 1 } ` ,
213+ email : `user${ i + 1 } @example.com` ,
214+ user_type : 'USER' ,
215+ short_name : `U${ i + 1 } ` ,
216+ timezone : 'UTC' ,
217+ removed : false ,
218+ bot : false ,
219+ version : 1 ,
220+ } ) )
221+
222+ let batchCount = 0
223+
224+ server . use (
225+ http . post ( 'https://api.twist.com/api/v3/batch' , async ( { request } ) => {
226+ const body = await request . text ( )
227+ const params = new URLSearchParams ( body )
228+ const requestsStr = params . get ( 'requests' )
229+
230+ expect ( requestsStr ) . toBeDefined ( )
231+ const requests = JSON . parse ( requestsStr || '[]' )
232+
233+ batchCount ++
234+
235+ // Verify chunk size matches expected
236+ const expectedSize = expectedChunkSizes . find (
237+ ( size ) => size === requests . length ,
238+ )
239+ expect ( expectedSize ) . toBeDefined ( )
240+
241+ // Extract user IDs from the requests to return the correct users
242+ const responseUsers = requests . map ( ( req : { url : string } ) => {
243+ // Extract user_id from URL query params
244+ const url = new URL ( req . url )
245+ const userId = Number . parseInt (
246+ url . searchParams . get ( 'user_id' ) || '0' ,
247+ 10 ,
248+ )
249+ return mockUsers . find ( ( user ) => user . id === userId ) || mockUsers [ 0 ]
250+ } )
251+
252+ // Return appropriate responses based on the actual requested user IDs
253+ return HttpResponse . json (
254+ responseUsers . map ( ( user : MockUser ) => ( {
255+ code : 200 ,
256+ headers : '' ,
257+ body : JSON . stringify ( user ) ,
258+ } ) ) ,
259+ )
260+ } ) ,
261+ )
262+
263+ const results = await api . batch (
264+ ...mockUsers . map ( ( user ) =>
265+ api . workspaceUsers . getUserById (
266+ { workspaceId : 123 , userId : user . id } ,
267+ { batch : true } ,
268+ ) ,
269+ ) ,
270+ )
271+
272+ // Verify batch count and result length
273+ expect ( batchCount ) . toBe ( expectedChunks )
274+ expect ( results ) . toHaveLength ( requestCount )
275+
276+ // Verify all results are successful
277+ results . forEach ( ( result ) => {
278+ expect ( result . code ) . toBe ( 200 )
279+ expect ( result . data . id ) . toBeGreaterThanOrEqual ( idOffset )
280+ expect ( result . data . id ) . toBeLessThan ( idOffset + requestCount )
281+ } )
282+
283+ // Verify order preservation if expected
284+ if ( verifyOrder ) {
285+ results . forEach ( ( result , index ) => {
286+ expect ( result . data . id ) . toBe ( idOffset + index )
287+ expect ( result . data . name ) . toBe ( `User ${ index + 1 } ` )
288+ } )
289+ }
290+ } ,
291+ )
292+
293+ it ( 'should handle mixed success and failure across multiple chunks' , async ( ) => {
294+ // Create 15 requests to trigger chunking into 2 batches
295+ const mockUsers = Array . from ( { length : 15 } , ( _ , i ) => ( {
296+ id : 700 + i ,
297+ name : `User ${ i + 1 } ` ,
298+ email : `user${ i + 1 } @example.com` ,
299+ user_type : 'USER' ,
300+ short_name : `U${ i + 1 } ` ,
301+ timezone : 'UTC' ,
302+ removed : false ,
303+ bot : false ,
304+ version : 1 ,
305+ } ) )
306+
307+ let batchCount = 0
308+
309+ server . use (
310+ http . post ( 'https://api.twist.com/api/v3/batch' , async ( { request } ) => {
311+ const body = await request . text ( )
312+ const params = new URLSearchParams ( body )
313+ const requestsStr = params . get ( 'requests' )
314+
315+ expect ( requestsStr ) . toBeDefined ( )
316+ const requests = JSON . parse ( requestsStr || '[]' )
317+
318+ batchCount ++
319+
320+ if ( requests . length === 10 ) {
321+ // First batch: mix of success and error
322+ return HttpResponse . json ( [
323+ ...Array . from ( { length : 8 } , ( _ , i ) => ( {
324+ code : 200 ,
325+ headers : '' ,
326+ body : JSON . stringify ( mockUsers [ i ] ) ,
327+ } ) ) ,
328+ {
329+ code : 404 ,
330+ headers : '' ,
331+ body : JSON . stringify ( { error : 'User not found' } ) ,
332+ } ,
333+ {
334+ code : 500 ,
335+ headers : '' ,
336+ body : JSON . stringify ( { error : 'Internal server error' } ) ,
337+ } ,
338+ ] )
339+ } else if ( requests . length === 5 ) {
340+ // Second batch: all successful
341+ return HttpResponse . json (
342+ Array . from ( { length : 5 } , ( _ , i ) => ( {
343+ code : 200 ,
344+ headers : '' ,
345+ body : JSON . stringify ( mockUsers [ 10 + i ] ) ,
346+ } ) ) ,
347+ )
348+ } else {
349+ return HttpResponse . error ( )
350+ }
351+ } ) ,
352+ )
353+
354+ const results = await api . batch (
355+ ...mockUsers . map ( ( user ) =>
356+ api . workspaceUsers . getUserById (
357+ { workspaceId : 123 , userId : user . id } ,
358+ { batch : true } ,
359+ ) ,
360+ ) ,
361+ )
362+
363+ expect ( batchCount ) . toBe ( 2 )
364+ expect ( results ) . toHaveLength ( 15 )
365+
366+ // Count successful and error responses
367+ let successCount = 0
368+ let errorCount = 0
369+
370+ results . forEach ( ( result ) => {
371+ if ( result . code === 200 ) {
372+ successCount ++
373+ expect ( result . data . id ) . toBeGreaterThanOrEqual ( 700 )
374+ } else if ( result . code === 404 || result . code === 500 ) {
375+ errorCount ++
376+ }
377+ } )
378+
379+ expect ( successCount ) . toBe ( 13 ) // 8 from first batch + 5 from second batch
380+ expect ( errorCount ) . toBe ( 2 ) // 2 errors from first batch
381+ } )
382+
383+ it ( 'should handle chunk-level failures gracefully' , async ( ) => {
384+ // Create 15 requests to trigger chunking
385+ const mockUsers = Array . from ( { length : 15 } , ( _ , i ) => ( {
386+ id : 800 + i ,
387+ name : `User ${ i + 1 } ` ,
388+ email : `user${ i + 1 } @example.com` ,
389+ user_type : 'USER' ,
390+ short_name : `U${ i + 1 } ` ,
391+ timezone : 'UTC' ,
392+ removed : false ,
393+ bot : false ,
394+ version : 1 ,
395+ } ) )
396+
397+ let batchCount = 0
398+
399+ server . use (
400+ http . post ( 'https://api.twist.com/api/v3/batch' , async ( { request } ) => {
401+ batchCount ++
402+ const body = await request . text ( )
403+ const params = new URLSearchParams ( body )
404+ const requestsStr = params . get ( 'requests' )
405+
406+ expect ( requestsStr ) . toBeDefined ( )
407+ const requests = JSON . parse ( requestsStr || '[]' )
408+
409+ if ( batchCount === 1 ) {
410+ // First batch fails completely
411+ expect ( requests ) . toHaveLength ( 10 )
412+ return HttpResponse . error ( )
413+ } else {
414+ // Second batch succeeds
415+ expect ( requests ) . toHaveLength ( 5 )
416+ return HttpResponse . json (
417+ mockUsers . slice ( 10 ) . map ( ( user ) => ( {
418+ code : 200 ,
419+ headers : '' ,
420+ body : JSON . stringify ( user ) ,
421+ } ) ) ,
422+ )
423+ }
424+ } ) ,
425+ )
426+
427+ const results = await api . batch (
428+ ...mockUsers . map ( ( user ) =>
429+ api . workspaceUsers . getUserById (
430+ { workspaceId : 123 , userId : user . id } ,
431+ { batch : true } ,
432+ ) ,
433+ ) ,
434+ )
435+
436+ expect ( batchCount ) . toBe ( 2 )
437+ expect ( results ) . toHaveLength ( 15 )
438+
439+ // Verify first batch requests have error responses
440+ for ( let i = 0 ; i < 10 ; i ++ ) {
441+ expect ( results [ i ] . code ) . toBe ( 500 )
442+ expect ( results [ i ] . data ) . toBe ( null )
443+ }
444+
445+ // Verify second batch requests are successful
446+ for ( let i = 10 ; i < 15 ; i ++ ) {
447+ expect ( results [ i ] . code ) . toBe ( 200 )
448+ expect ( results [ i ] . data . id ) . toBe ( 800 + i )
449+ }
450+ } )
451+ } )
452+
149453 describe ( 'getUserById with batch option' , ( ) => {
150454 it ( 'should return descriptor when batch: true' , ( ) => {
151455 const descriptor = api . workspaceUsers . getUserById (
0 commit comments