diff --git a/packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts b/packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts index 379368088ad0..13b14d9623e7 100644 --- a/packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts +++ b/packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts @@ -78,6 +78,12 @@ interface InlineFileBatchRequest { * The locale specifiers or locale objects that should be used during the inlining process of the file. */ locales: (string | { locale: string; translation?: Blob })[]; + + /** + * Whether the file data should be treated as ephemeral and not cached long-term in the Worker. + * Typically true when all remaining locales for the file are processed in a single batch. + */ + ephemeral?: boolean; } /** @@ -124,23 +130,35 @@ const fileDataCache = new Map>(); const deserializedTranslations = new Map>>(); /** - * Retrieves the cached file data for a filename, loading and extracting it on the first request. + * Retrieves the file data for a filename, loading and extracting localization metadata. + * If `cache` is true, the result is cached in `fileDataCache` across requests in this Worker. + * If `cache` is false (ephemeral), the result is not retained in `fileDataCache`, allowing it + * to be garbage-collected once the batch request finishes. * * @param filename The name of the file to load. - * @returns The cached code and localization metadata. + * @param cache Whether to cache the loaded file data in the Worker's long-term cache. + * @returns The cached or newly extracted code and localization metadata. */ -function getFileData(filename: string): Promise { - let fileDataPromise = fileDataCache.get(filename); - if (!fileDataPromise) { - fileDataPromise = (async () => { - const data = files.get(filename); - assert(data !== undefined, `Invalid inline request for file '${filename}'.`); - - const code = await data.text(); - const metadata = extractLocalizeMetadata(filename, code); - - return { code, metadata }; - })(); +function loadFileData(filename: string, cache = true): Promise { + const existing = fileDataCache.get(filename); + if (existing) { + return existing; + } + + const fileDataPromise = (async () => { + const data = files.get(filename); + assert(data !== undefined, `Invalid inline request for file '${filename}'.`); + + const code = await data.text(); + const metadata = extractLocalizeMetadata(filename, code); + + return { code, metadata }; + })(); + + if (cache) { + fileDataPromise.catch(() => { + fileDataCache.delete(filename); + }); fileDataCache.set(filename, fileDataPromise); } @@ -186,7 +204,7 @@ function loadTranslation( * @returns An object containing the inlined file and optional map content. */ export default async function inlineFile(request: InlineFileRequest) { - const { code, metadata } = await getFileData(request.filename); + const { code, metadata } = await loadFileData(request.filename, true); // Sourcemaps are parsed on demand per request rather than cached long-term to prevent // monotonic memory growth as a worker processes multiple files across the build. @@ -219,7 +237,7 @@ export default async function inlineFile(request: InlineFileRequest) { export async function inlineFileBatch( request: InlineFileBatchRequest, ): Promise { - const { code, metadata } = await getFileData(request.filename); + const { code, metadata } = await loadFileData(request.filename, !request.ephemeral); // Parse the sourcemap once for the entire batch. // It will naturally be garbage-collected after this batch action returns. diff --git a/packages/angular/build/src/tools/esbuild/i18n-inliner.ts b/packages/angular/build/src/tools/esbuild/i18n-inliner.ts index 535aae960b34..983f17956b31 100644 --- a/packages/angular/build/src/tools/esbuild/i18n-inliner.ts +++ b/packages/angular/build/src/tools/esbuild/i18n-inliner.ts @@ -9,7 +9,7 @@ import assert from 'node:assert'; import { extname, join } from 'node:path'; import { serialize } from 'node:v8'; -import { calculateHash, createContentHash } from '../../utils/hash'; +import { calculateHash, createContentHash, initializeHash } from '../../utils/hash'; import { WorkerPool } from '../../utils/worker-pool'; import { type BuildOutputFile, BuildOutputFileType, createOutputFile } from './bundler-files'; import { type PersistentCacheStore, createPersistentCacheStore } from './cache'; @@ -377,6 +377,7 @@ export class I18nInliner { const workerTasks: Promise[] = []; for (const [filename, entries] of uncachedByFile) { + const ephemeral = entries.length <= localesPerBatch; for (let i = 0; i < entries.length; i += localesPerBatch) { const batchEntries = entries.slice(i, i + localesPerBatch); const task = (async () => { @@ -387,6 +388,7 @@ export class I18nInliner { locale: e.locale, translation: e.translation, })), + ephemeral, }, { name: 'inlineFileBatch' }, )) as { @@ -518,7 +520,11 @@ export class I18nInliner { // Initialize a persistent cache for i18n transformations. try { - this.#cache = await createPersistentCacheStore(join(persistentCachePath, 'angular-i18n')); + const [, cache] = await Promise.all([ + initializeHash(), + createPersistentCacheStore(join(persistentCachePath, 'angular-i18n')), + ]); + this.#cache = cache; } catch { this.#cacheInitFailed = true; diff --git a/packages/angular/build/src/tools/esbuild/i18n-inliner_spec.ts b/packages/angular/build/src/tools/esbuild/i18n-inliner_spec.ts index d71f9c0a8830..e8fefeb3a14f 100644 --- a/packages/angular/build/src/tools/esbuild/i18n-inliner_spec.ts +++ b/packages/angular/build/src/tools/esbuild/i18n-inliner_spec.ts @@ -10,6 +10,7 @@ import { transform } from 'esbuild'; import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; +import { initializeHash } from '../../utils/hash'; import { type BuildOutputFile, BuildOutputFileType, createOutputFile } from './bundler-files'; import { I18nInliner } from './i18n-inliner'; @@ -43,6 +44,10 @@ function findFile(outputFiles: BuildOutputFile[], path: string): BuildOutputFile describe('I18nInliner', () => { let inliner: I18nInliner | undefined; + beforeAll(async () => { + await initializeHash(); + }); + // A single thread is used throughout so that every file of every locale is inlined by the same // Worker. Any translation state that a Worker retains between requests is then observable. function createInliner(outputFiles: BuildOutputFile[]): I18nInliner {