@@ -20,6 +20,12 @@ import { type PersistentCacheStore, createPersistentCacheStore } from './cache';
2020 */
2121const LOCALIZE_KEYWORD = '$localize' ;
2222
23+ /**
24+ * The maximum number of locales to process concurrently in a single sliding window.
25+ * This caps peak worker memory while maintaining multi-locale batching throughput.
26+ */
27+ const DEFAULT_LOCALE_WINDOW_SIZE = 8 ;
28+
2329/**
2430 * Serializes the translation messages for a locale for transfer to an inliner Worker.
2531 *
@@ -218,101 +224,114 @@ export class I18nInliner {
218224 fileResultsByLocale . set ( locale , new Map ( ) ) ;
219225 }
220226
221- // Pre-calculate cache key bases and serialized Blobs for each requested locale
222- const localeCacheBases = new Map < string , string > ( ) ;
223- const localeBlobs = new Map < string , Blob | undefined > ( ) ;
224-
225- for ( const { locale, translation, translationIntegrity } of localeList ) {
226- localeBlobs . set ( locale , serializeTranslation ( translation ) ) ;
227-
228- if ( this . #cache) {
229- localeCacheBases . set (
230- locale ,
231- calculateHash (
232- JSON . stringify ( {
233- locale,
234- translation : translationIntegrity || translation ,
235- missingTranslation,
236- shouldOptimize,
237- localizeVersion,
238- } ) ,
239- ) ,
240- ) ;
241- }
242- }
243-
244227 const filenames = Array . from ( this . #localizeFiles. keys ( ) ) . filter (
245228 ( name ) => ! name . endsWith ( '.map' ) ,
246229 ) ;
247230
248- const cacheChecks : CacheCheckItem [ ] = [ ] ;
231+ // Process locales in sliding windows to cap peak worker memory
232+ for ( let i = 0 ; i < localeList . length ; i += DEFAULT_LOCALE_WINDOW_SIZE ) {
233+ const windowLocales = localeList . slice ( i , i + DEFAULT_LOCALE_WINDOW_SIZE ) ;
234+ const activeLocales = windowLocales . map ( ( item ) => item . locale ) ;
235+ const isLastWindow = i + DEFAULT_LOCALE_WINDOW_SIZE >= localeList . length ;
249236
250- for ( const filename of filenames ) {
251- const file = this . #localizeFiles . get ( filename ) ;
252- assert ( file !== undefined , 'Localize file must exist: ' + filename ) ;
237+ // Pre-calculate cache key bases and serialized Blobs for each locale in this window
238+ const localeCacheBases = new Map < string , string > ( ) ;
239+ const localeBlobs = new Map < string , Blob | undefined > ( ) ;
253240
254- for ( const { locale } of localeList ) {
255- let cacheKey : string | undefined ;
256- let cachedResultPromise : Promise < TransformedFileResult | null > = Promise . resolve ( null ) ;
241+ for ( const { locale, translation, translationIntegrity } of windowLocales ) {
242+ localeBlobs . set ( locale , serializeTranslation ( translation ) ) ;
257243
258244 if ( this . #cache) {
259- const fileCacheKeyBase = localeCacheBases . get ( locale ) ;
260- assert ( fileCacheKeyBase !== undefined , 'Cache base must exist for locale: ' + locale ) ;
245+ localeCacheBases . set (
246+ locale ,
247+ calculateHash (
248+ JSON . stringify ( {
249+ locale,
250+ translation : translationIntegrity || translation ,
251+ missingTranslation,
252+ shouldOptimize,
253+ localizeVersion,
254+ } ) ,
255+ ) ,
256+ ) ;
257+ }
258+ }
261259
262- const hasher = createContentHash ( ) ;
263- hasher . update ( file . hash ) ;
264- hasher . update ( filename ) ;
265- hasher . update ( fileCacheKeyBase ) ;
266- cacheKey = hasher . digest ( ) ;
260+ const cacheChecks : CacheCheckItem [ ] = [ ] ;
267261
268- cachedResultPromise = this . #cache. get ( cacheKey ) . catch ( ( ) => null ) ;
269- }
262+ for ( const filename of filenames ) {
263+ const file = this . #localizeFiles. get ( filename ) ;
264+ assert ( file !== undefined , 'Localize file must exist: ' + filename ) ;
270265
271- cacheChecks . push ( {
272- filename,
273- locale,
274- cacheKey,
275- cachedResult : cachedResultPromise ,
276- } ) ;
277- }
278- }
266+ for ( const { locale } of windowLocales ) {
267+ let cacheKey : string | undefined ;
268+ let cachedResultPromise : Promise < TransformedFileResult | null > = Promise . resolve ( null ) ;
279269
280- // Await all cache checks
281- const resolvedChecks = await Promise . all (
282- cacheChecks . map ( async ( item ) => ( {
283- ...item ,
284- result : await item . cachedResult ,
285- } ) ) ,
286- ) ;
270+ if ( this . #cache) {
271+ const fileCacheKeyBase = localeCacheBases . get ( locale ) ;
272+ assert ( fileCacheKeyBase !== undefined , 'Cache base must exist for locale: ' + locale ) ;
287273
288- // Group uncached items by filename
289- const uncachedByFile = new Map <
290- string ,
291- Array < { locale : string ; cacheKey ?: string ; translation ?: Blob } >
292- > ( ) ;
274+ const hasher = createContentHash ( ) ;
275+ hasher . update ( file . hash ) ;
276+ hasher . update ( filename ) ;
277+ hasher . update ( fileCacheKeyBase ) ;
278+ cacheKey = hasher . digest ( ) ;
293279
294- for ( const item of resolvedChecks ) {
295- if ( item . result ) {
296- // Cache hit: store directly in locale file results
297- fileResultsByLocale . get ( item . locale ) ?. set ( item . filename , item . result ) ;
298- } else {
299- // Cache miss: needs worker processing
300- let fileEntries = uncachedByFile . get ( item . filename ) ;
301- if ( ! fileEntries ) {
302- fileEntries = [ ] ;
303- uncachedByFile . set ( item . filename , fileEntries ) ;
280+ cachedResultPromise = this . #cache. get ( cacheKey ) . catch ( ( ) => null ) ;
281+ }
282+
283+ cacheChecks . push ( {
284+ filename,
285+ locale,
286+ cacheKey,
287+ cachedResult : cachedResultPromise ,
288+ } ) ;
304289 }
305- fileEntries . push ( {
306- locale : item . locale ,
307- cacheKey : item . cacheKey ,
308- translation : localeBlobs . get ( item . locale ) ,
309- } ) ;
310290 }
311- }
312291
313- // Adaptive 2D Sharding for uncached tasks
314- if ( uncachedByFile . size > 0 ) {
315- await this . #processUncachedBatches( uncachedByFile , localeList . length , fileResultsByLocale ) ;
292+ // Await all cache checks for this window
293+ const resolvedChecks = await Promise . all (
294+ cacheChecks . map ( async ( item ) => ( {
295+ ...item ,
296+ result : await item . cachedResult ,
297+ } ) ) ,
298+ ) ;
299+
300+ // Group uncached items by filename for this window
301+ const uncachedByFile = new Map <
302+ string ,
303+ Array < { locale : string ; cacheKey ?: string ; translation ?: Blob } >
304+ > ( ) ;
305+
306+ for ( const item of resolvedChecks ) {
307+ if ( item . result ) {
308+ // Cache hit: store directly in locale file results
309+ fileResultsByLocale . get ( item . locale ) ?. set ( item . filename , item . result ) ;
310+ } else {
311+ // Cache miss: needs worker processing
312+ let fileEntries = uncachedByFile . get ( item . filename ) ;
313+ if ( ! fileEntries ) {
314+ fileEntries = [ ] ;
315+ uncachedByFile . set ( item . filename , fileEntries ) ;
316+ }
317+ fileEntries . push ( {
318+ locale : item . locale ,
319+ cacheKey : item . cacheKey ,
320+ translation : localeBlobs . get ( item . locale ) ,
321+ } ) ;
322+ }
323+ }
324+
325+ // Adaptive 2D Sharding for uncached tasks in this window
326+ if ( uncachedByFile . size > 0 ) {
327+ await this . #processUncachedBatches(
328+ uncachedByFile ,
329+ windowLocales . length ,
330+ fileResultsByLocale ,
331+ activeLocales ,
332+ isLastWindow ,
333+ ) ;
334+ }
316335 }
317336
318337 // Assemble final results in deterministic order per locale
@@ -366,6 +385,8 @@ export class I18nInliner {
366385 uncachedByFile : Map < string , Array < { locale : string ; cacheKey ?: string ; translation ?: Blob } > > ,
367386 localeCount : number ,
368387 fileResultsByLocale : Map < string , Map < string , TransformedFileResult > > ,
388+ activeLocales ?: string [ ] ,
389+ isLastWindow = true ,
369390 ) : Promise < void > {
370391 const workerCount = this . #workerPool. maxThreads || 1 ;
371392 const targetTaskCount = Math . max ( uncachedByFile . size , workerCount * 2 ) ;
@@ -377,7 +398,7 @@ export class I18nInliner {
377398 const workerTasks : Promise < void > [ ] = [ ] ;
378399
379400 for ( const [ filename , entries ] of uncachedByFile ) {
380- const ephemeral = entries . length <= localesPerBatch ;
401+ const ephemeral = isLastWindow && entries . length <= localesPerBatch ;
381402 for ( let i = 0 ; i < entries . length ; i += localesPerBatch ) {
382403 const batchEntries = entries . slice ( i , i + localesPerBatch ) ;
383404 const task = ( async ( ) => {
@@ -389,6 +410,7 @@ export class I18nInliner {
389410 translation : e . translation ,
390411 } ) ) ,
391412 ephemeral,
413+ activeLocales,
392414 } ,
393415 { name : 'inlineFileBatch' } ,
394416 ) ) as {
0 commit comments