@@ -2028,7 +2028,7 @@ export class BitGoAPI implements BitGoBase {
20282028 await this . processKeychainPasswordUpdatesInBatches (
20292029 updatePasswordParams . keychains ,
20302030 updatePasswordParams . v2_keychains ,
2031- batchingFlowCheck . noOfBatches ,
2031+ batchingFlowCheck . maxBatchSizeKB ,
20322032 3
20332033 ) ;
20342034 // Call changepassword API without keychains for batching flow
@@ -2287,30 +2287,80 @@ export class BitGoAPI implements BitGoBase {
22872287 }
22882288
22892289 /**
2290- * Process keychain password updates in batches with retry logic
2290+ * Pack keychains into batches using First Fit Decreasing (FFD) algorithm.
2291+ *
2292+ * @param keychains - V1 keychains
2293+ * @param v2Keychains - V2 keychains
2294+ * @param maxBatchSizeBytes - Maximum byte size per batch
2295+ * @private
2296+ */
2297+ private packKeychainsFFD (
2298+ keychains : Record < string , string > ,
2299+ v2Keychains : Record < string , string > ,
2300+ maxBatchSizeBytes : number
2301+ ) : Array < { v1Batch : Record < string , string > ; v2Batch : Record < string , string > ; sizeBytes : number } > {
2302+ const entrySize = ( id : string , value : string ) => Buffer . byteLength ( id , 'utf8' ) + Buffer . byteLength ( value , 'utf8' ) ;
2303+
2304+ const items = [
2305+ ...Object . entries ( keychains ) . map ( ( [ id , value ] ) => ( { id, value, sizeBytes : entrySize ( id , value ) , isV2 : false } ) ) ,
2306+ ...Object . entries ( v2Keychains ) . map ( ( [ id , value ] ) => ( { id, value, sizeBytes : entrySize ( id , value ) , isV2 : true } ) ) ,
2307+ ] . sort ( ( a , b ) => b . sizeBytes - a . sizeBytes ) ;
2308+
2309+ const bins : Array < { v1Batch : Record < string , string > ; v2Batch : Record < string , string > ; sizeBytes : number } > = [ ] ;
2310+
2311+ for ( const item of items ) {
2312+ if ( item . sizeBytes > maxBatchSizeBytes ) {
2313+ throw new Error ( `Keychain with id ${ item . id } exceeds the maximum batch size and cannot be processed` ) ;
2314+ }
2315+
2316+ const target = bins . find ( ( bin ) => bin . sizeBytes + item . sizeBytes <= maxBatchSizeBytes ) ;
2317+ if ( target ) {
2318+ if ( item . isV2 ) {
2319+ target . v2Batch [ item . id ] = item . value ;
2320+ } else {
2321+ target . v1Batch [ item . id ] = item . value ;
2322+ }
2323+ target . sizeBytes += item . sizeBytes ;
2324+ } else {
2325+ const newBin = {
2326+ v1Batch : { } as Record < string , string > ,
2327+ v2Batch : { } as Record < string , string > ,
2328+ sizeBytes : item . sizeBytes ,
2329+ } ;
2330+ if ( item . isV2 ) {
2331+ newBin . v2Batch [ item . id ] = item . value ;
2332+ } else {
2333+ newBin . v1Batch [ item . id ] = item . value ;
2334+ }
2335+ bins . push ( newBin ) ;
2336+ }
2337+ }
2338+
2339+ return bins ;
2340+ }
2341+
2342+ /**
2343+ * Process keychain password updates in batches with retry logic.
2344+ * Uses First Fit Decreasing (FFD) bin packing to ensure no batch exceeds
2345+ * maxBatchSizeKB
2346+ *
22912347 * @param keychains - The v1 keychains to update
22922348 * @param v2Keychains - The v2 keychains to update
2293- * @param noOfBatches - Number of batches to split the keychains into
2349+ * @param maxBatchSizeKB - Maximum payload size per batch in kilobytes
22942350 * @param maxRetries - Maximum number of retries per batch
22952351 * @private
22962352 */
22972353 private async processKeychainPasswordUpdatesInBatches (
22982354 keychains : Record < string , string > ,
22992355 v2Keychains : Record < string , string > ,
2300- noOfBatches : number ,
2356+ maxBatchSizeKB : number ,
23012357 maxRetries : number
23022358 ) : Promise < void > {
2303- // Split keychains into batches
2304- const v1KeychainEntries = Object . entries ( keychains ) ;
2305- const v2KeychainEntries = Object . entries ( v2Keychains ) ;
2306-
2307- const v1BatchSize = Math . ceil ( v1KeychainEntries . length / noOfBatches ) ;
2308- const v2BatchSize = Math . ceil ( v2KeychainEntries . length / noOfBatches ) ;
2359+ const maxBatchSizeBytes = maxBatchSizeKB * 1024 ;
2360+ const bins = this . packKeychainsFFD ( keychains , v2Keychains , maxBatchSizeBytes ) ;
23092361
2310- // Call batching API for each batch with retry logic
2311- for ( let i = 0 ; i < noOfBatches ; i ++ ) {
2312- const v1Batch = Object . fromEntries ( v1KeychainEntries . slice ( i * v1BatchSize , ( i + 1 ) * v1BatchSize ) ) ;
2313- const v2Batch = Object . fromEntries ( v2KeychainEntries . slice ( i * v2BatchSize , ( i + 1 ) * v2BatchSize ) ) ;
2362+ for ( let i = 0 ; i < bins . length ; i ++ ) {
2363+ const { v1Batch, v2Batch } = bins [ i ] ;
23142364
23152365 let retryCount = 0 ;
23162366 let success = false ;
0 commit comments