@@ -47,18 +47,17 @@ export interface InlineCodeRequest {
4747 */
4848 translation ?: Blob | SharedArrayBuffer ;
4949
50+ translationKey ?: string ;
51+
5052 /**
5153 * How to handle missing translations.
5254 */
5355 missingTranslation ?: 'error' | 'warning' | 'ignore' ;
5456}
5557
56- /**
57- * The response returned from a code request.
58- */
59- export interface InlineCodeResult {
60- output : string ;
61- messages : { type : 'error' | 'warning' ; message : string } [ ] ;
58+ export interface InlineFileBatchLocaleEntry {
59+ translation ?: Blob | SharedArrayBuffer ;
60+ translationKey ?: string ;
6261}
6362
6463/**
@@ -83,7 +82,7 @@ export interface InlineFileBatchRequest {
8382 /**
8483 * The locale specifiers and optional translations to use during the inlining process of the file.
8584 */
86- locales : ReadonlyMap < string , Blob | SharedArrayBuffer | undefined > ;
85+ locales : ReadonlyMap < string , InlineFileBatchLocaleEntry | Blob | SharedArrayBuffer | undefined > ;
8786
8887 /**
8988 * How to handle missing translations.
@@ -107,6 +106,38 @@ export interface InlineFileBatchRequest {
107106 * all long-term worker caches are cleared.
108107 */
109108 generation ?: number ;
109+
110+ /**
111+ * Optional file contents Blob when dispatched via the shared worker pool.
112+ */
113+ fileBlob ?: Blob ;
114+
115+ /**
116+ * Optional cache key uniquely identifying the file content and AST metadata.
117+ */
118+ fileKey ?: string ;
119+
120+ /**
121+ * Optional sourcemap Blob for the file when dispatched via the shared worker pool.
122+ */
123+ mapBlob ?: Blob ;
124+ }
125+
126+ export interface InlineDiagnosticMessage {
127+ type : 'error' | 'warning' ;
128+ message : string ;
129+ }
130+
131+ export interface InlineFileResult {
132+ file : string ;
133+ code : string ;
134+ map ?: string ;
135+ messages : InlineDiagnosticMessage [ ] ;
136+ }
137+
138+ export interface InlineCodeResult {
139+ output : string ;
140+ messages : InlineDiagnosticMessage [ ] ;
110141}
111142
112143/**
@@ -116,7 +147,7 @@ export interface InlineLocaleResult {
116147 locale : string ;
117148 code ?: string ;
118149 map ?: string ;
119- messages : { type : 'error' | 'warning' ; message : string } [ ] ;
150+ messages : InlineDiagnosticMessage [ ] ;
120151}
121152
122153/**
@@ -134,6 +165,17 @@ export type InlineFileBatchResult =
134165 results : InlineLocaleResult [ ] ;
135166 } ;
136167
168+ /**
169+ * Maximum number of AST metadata structures cached in memory per worker isolate.
170+ * Bounding capacity prevents unbounded memory growth across watch rebuilds.
171+ */
172+ const MAX_CACHED_FILES = 256 ;
173+
174+ /**
175+ * Maximum number of deserialized translation dictionaries cached in memory per worker isolate.
176+ */
177+ const MAX_CACHED_TRANSLATIONS = 32 ;
178+
137179/**
138180 * Cached file data including code and extracted localization metadata.
139181 */
@@ -143,12 +185,12 @@ interface CachedFileData {
143185}
144186
145187/**
146- * Cache of file data promises keyed by filename.
188+ * Cache of file data promises keyed by `${filename}\0${hash}` or filename.
147189 */
148190const fileDataCache = new Map < string , Promise < CachedFileData > > ( ) ;
149191
150192/**
151- * Cache of deserialized translation messages keyed by locale.
193+ * Deserialized translation message dictionary cache keyed by `${locale}\0${translationKey}` or locale.
152194 */
153195const deserializedTranslations = new Map < string , Promise < Record < string , ɵParsedTranslation > > > ( ) ;
154196
@@ -160,72 +202,106 @@ let currentGeneration: number | undefined;
160202let remapping : typeof import ( '@ampproject/remapping' ) . default | undefined ;
161203
162204/**
163- * Retrieves the file data for a filename, loading and extracting localization metadata.
164- * If `cache` is true, the result is cached in `fileDataCache` across requests in this Worker .
165- * If `cache` is false (ephemeral), the result is not retained in `fileDataCache`, allowing it
166- * to be garbage-collected once the batch request finishes.
205+ * Retrieves the code and extracted localization metadata for a file .
206+ * Caches the metadata promise in memory to avoid reparsing the AST across locales .
207+ * If `cache` is false (ephemeral), the result is not retained in `fileDataCache`,
208+ * allowing it to be garbage-collected once the batch request finishes.
167209 *
168- * @param filename The name of the file to load .
210+ * @param filename The name of the file.
169211 * @param codeBlob The source code file as a Blob.
212+ * @param fileKey Optional cache key uniquely identifying the file content.
170213 * @param cache Whether to cache the loaded file data in the Worker's long-term cache.
171- * @returns The cached or newly extracted code and localization metadata .
214+ * @returns The cached file data .
172215 */
173- function loadFileData ( filename : string , codeBlob : Blob , cache = true ) : Promise < CachedFileData > {
174- const existing = fileDataCache . get ( filename ) ;
175- if ( existing ) {
176- if ( ! cache ) {
177- fileDataCache . delete ( filename ) ;
178- }
179-
180- return existing ;
181- }
182-
183- const fileDataPromise = ( async ( ) => {
184- const code = await codeBlob . text ( ) ;
185- const metadata = extractLocalizeMetadata ( filename , code ) ;
216+ function getFileData (
217+ filename : string ,
218+ codeBlob : Blob ,
219+ fileKey ?: string ,
220+ cache = true ,
221+ ) : Promise < CachedFileData > {
222+ const cacheKey = fileKey ?? filename ;
223+ let dataPromise = fileDataCache . get ( cacheKey ) ;
224+ if ( ! dataPromise ) {
225+ dataPromise = ( async ( ) => {
226+ const code = await codeBlob . text ( ) ;
227+
228+ return {
229+ code,
230+ metadata : extractLocalizeMetadata ( filename , code ) ,
231+ } ;
232+ } ) ( ) . catch ( ( error ) => {
233+ if ( fileDataCache . get ( cacheKey ) === dataPromise ) {
234+ fileDataCache . delete ( cacheKey ) ;
235+ }
236+ throw error ;
237+ } ) ;
186238
187- return { code, metadata } ;
188- } ) ( ) ;
239+ if ( cache ) {
240+ if ( fileDataCache . size >= MAX_CACHED_FILES ) {
241+ const oldestKey = fileDataCache . keys ( ) . next ( ) . value ;
242+ if ( oldestKey !== undefined ) {
243+ fileDataCache . delete ( oldestKey ) ;
244+ }
245+ }
189246
190- if ( cache ) {
191- fileDataPromise . catch ( ( ) => {
192- fileDataCache . delete ( filename ) ;
193- } ) ;
194- fileDataCache . set ( filename , fileDataPromise ) ;
247+ fileDataCache . set ( cacheKey , dataPromise ) ;
248+ }
249+ } else if ( cache ) {
250+ fileDataCache . delete ( cacheKey ) ;
251+ fileDataCache . set ( cacheKey , dataPromise ) ;
252+ } else {
253+ fileDataCache . delete ( cacheKey ) ;
195254 }
196255
197- return fileDataPromise ;
256+ return dataPromise ;
198257}
199258
200259/**
201260 * Deserializes or wraps the translation messages for a locale, reusing the result for any
202- * subsequent request that targets the same locale.
203- * @param locale The locale identifier.
204- * @param translation Optional serialized translation messages (SharedArrayBuffer or Blob) .
205- * @returns The translation messages, or undefined if the locale has no translations .
261+ * subsequent request that targets the same locale and translation payload .
262+ *
263+ * @param request The translation request object containing locale, translation payload, and optional key .
264+ * @param explicitTranslation Optional fallback translation payload if request is a string .
206265 */
207266function loadTranslation (
208267 locale : string ,
209268 translation ?: Blob | SharedArrayBuffer ,
269+ translationKey ?: string ,
210270) : Promise < Record < string , ɵParsedTranslation > > | undefined {
211271 if ( ! translation ) {
212272 return undefined ;
213273 }
214274
215- let messagesPromise = deserializedTranslations . get ( locale ) ;
275+ const cacheKey = translationKey ? `${ locale } \0${ translationKey } ` : undefined ;
276+ let messagesPromise = cacheKey ? deserializedTranslations . get ( cacheKey ) : undefined ;
216277 if ( ! messagesPromise ) {
217278 if ( translation instanceof Blob ) {
218279 messagesPromise = translation
219280 . arrayBuffer ( )
220281 . then ( ( buffer ) => deserialize ( new Uint8Array ( buffer ) ) as Record < string , ɵParsedTranslation > )
221282 . catch ( ( error ) => {
222- deserializedTranslations . delete ( locale ) ;
283+ if ( cacheKey && deserializedTranslations . get ( cacheKey ) === messagesPromise ) {
284+ deserializedTranslations . delete ( cacheKey ) ;
285+ }
223286 throw error ;
224287 } ) ;
225288 } else {
226289 messagesPromise = Promise . resolve ( createSharedTranslationProxy ( translation ) ) ;
227290 }
228- deserializedTranslations . set ( locale , messagesPromise ) ;
291+
292+ if ( cacheKey ) {
293+ if ( deserializedTranslations . size >= MAX_CACHED_TRANSLATIONS ) {
294+ const oldestKey = deserializedTranslations . keys ( ) . next ( ) . value ;
295+ if ( oldestKey !== undefined ) {
296+ deserializedTranslations . delete ( oldestKey ) ;
297+ }
298+ }
299+
300+ deserializedTranslations . set ( cacheKey , messagesPromise ) ;
301+ }
302+ } else if ( cacheKey ) {
303+ deserializedTranslations . delete ( cacheKey ) ;
304+ deserializedTranslations . set ( cacheKey , messagesPromise ) ;
229305 }
230306
231307 return messagesPromise ;
@@ -248,14 +324,25 @@ export async function inlineFileBatch(
248324
249325 if ( request . activeLocales ) {
250326 const activeSet = new Set ( request . activeLocales ) ;
251- for ( const locale of deserializedTranslations . keys ( ) ) {
252- if ( ! activeSet . has ( locale ) ) {
253- deserializedTranslations . delete ( locale ) ;
327+ for ( const key of deserializedTranslations . keys ( ) ) {
328+ const keyLocale = key . includes ( '\0' ) ? key . split ( '\0' , 1 ) [ 0 ] : key ;
329+ if ( ! activeSet . has ( keyLocale ) ) {
330+ deserializedTranslations . delete ( key ) ;
254331 }
255332 }
256333 }
257334
258- const { code, metadata } = await loadFileData ( request . filename , request . code , ! request . ephemeral ) ;
335+ const codeBlob = request . code ?? request . fileBlob ;
336+ if ( ! codeBlob ) {
337+ throw new Error ( `File content not provided for: ${ request . filename } ` ) ;
338+ }
339+
340+ const { code, metadata } = await getFileData (
341+ request . filename ,
342+ codeBlob ,
343+ request . fileKey ,
344+ ! request . ephemeral ,
345+ ) ;
259346
260347 // Fast path: file has no $localize call sites or locale insert sites
261348 if ( metadata . callSites . length === 0 && metadata . localeInsertSites . length === 0 ) {
@@ -271,20 +358,31 @@ export async function inlineFileBatch(
271358
272359 // Parse the sourcemap once for the entire batch if provided.
273360 // It will naturally be garbage-collected after this batch action returns.
361+ const rawMapBlob = request . map ?? request . mapBlob ;
274362 let map : SourceMapInput | undefined ;
275- if ( request . map ) {
276- const rawMap = await request . map . text ( ) ;
363+ let rawMap : string | undefined ;
364+ if ( rawMapBlob ) {
365+ rawMap = await rawMapBlob . text ( ) ;
277366 map = rawMap ? ( JSON . parse ( rawMap ) as SourceMapInput ) : undefined ;
278367 }
279368
280369 const results = [ ] ;
281- for ( const [ locale , translation ] of request . locales ) {
370+ for ( const [ locale , entry ] of request . locales ) {
371+ const translation =
372+ entry && typeof entry === 'object' && 'translation' in entry
373+ ? entry . translation
374+ : ( entry as Blob | SharedArrayBuffer | undefined ) ;
375+ const translationKey =
376+ entry && typeof entry === 'object' && 'translationKey' in entry
377+ ? entry . translationKey
378+ : undefined ;
379+
282380 const result = await inlineLocalize (
283381 code ,
284382 map ,
285383 metadata ,
286384 locale ,
287- await loadTranslation ( locale , translation ) ,
385+ await loadTranslation ( locale , translation , translationKey ) ,
288386 request . filename ,
289387 request . missingTranslation ,
290388 ) ;
@@ -307,7 +405,7 @@ export async function inlineFileBatch(
307405 * Inlines the provided locale and translation into JavaScript code that contains `$localize` usage.
308406 * This function is a secondary entry primarily for use with component HMR update modules.
309407 *
310- * @param request An InlineRequest object representing the options for inlining
408+ * @param request An InlineCodeRequest object representing the options for inlining
311409 * @returns An object containing the inlined code.
312410 */
313411export async function inlineCode ( request : InlineCodeRequest ) : Promise < InlineCodeResult > {
@@ -317,7 +415,7 @@ export async function inlineCode(request: InlineCodeRequest): Promise<InlineCode
317415 undefined ,
318416 metadata ,
319417 request . locale ,
320- await loadTranslation ( request . locale , request . translation ) ,
418+ await loadTranslation ( request . locale , request . translation , request . translationKey ) ,
321419 request . filename ,
322420 request . missingTranslation ,
323421 ) ;
@@ -559,7 +657,7 @@ async function inlineLocalize(
559657 }
560658
561659 const outputCode = magicString . toString ( ) ;
562- let outputMap ;
660+ let outputMap : string | undefined ;
563661 if ( map ) {
564662 // A decoded map is generated here rather than an encoded one because remapping decodes its
565663 // inputs. Encoding the mappings only for remapping to immediately decode them again doubles
@@ -570,12 +668,14 @@ async function inlineLocalize(
570668 hires : 'boundary' ,
571669 } ) ;
572670 remapping ??= ( await import ( '@ampproject/remapping' ) ) . default ;
573- outputMap = remapping ( [ { ...rawMap , version : 3 } satisfies DecodedSourceMap , map ] , ( ) => null ) ;
671+ outputMap = JSON . stringify (
672+ remapping ( [ { ...rawMap , version : 3 } satisfies DecodedSourceMap , map ] , ( ) => null ) ,
673+ ) ;
574674 }
575675
576676 return {
577677 code : outputCode ,
578- map : outputMap && JSON . stringify ( outputMap ) ,
678+ map : outputMap ,
579679 diagnostics,
580680 } ;
581681}
0 commit comments