From e31c6f75b9dcd201ccdda8392cf5fe2717360517 Mon Sep 17 00:00:00 2001 From: arturovt Date: Sat, 1 Feb 2025 08:08:14 +0200 Subject: [PATCH] fix(transloco): prevent loading translations when injector is destroyed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This avoids potential memory leaks and ensures correct behavior in reactive streams (e.g. switchMap) by returning EMPTY, which completes immediately. Use-case: - If load() is triggered via an observable (like within switchMap), and the service has already been destroyed (e.g. due to app shutdown, route unload, or SSR teardown), continuing execution would: - Create unnecessary subscriptions and pending async operations (like network calls). - Risk caching invalid/partial translation data. - In SSR, could retain memory or even leak state between requests. Returning EMPTY ensures the observable chain completes cleanly without side effects. Here’s a specific leak scenario this fix prevents: ```js componentLanguage$.pipe( switchMap(lang => translocoService.load(lang)), ).subscribe(); ``` --- libs/transloco/src/lib/transloco.service.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/libs/transloco/src/lib/transloco.service.ts b/libs/transloco/src/lib/transloco.service.ts index b9bd2a2b..5fad717f 100644 --- a/libs/transloco/src/lib/transloco.service.ts +++ b/libs/transloco/src/lib/transloco.service.ts @@ -109,6 +109,7 @@ export class TranslocoService { }; private destroyRef = inject(DestroyRef); + private destroyed = false; constructor( @Optional() @Inject(TRANSLOCO_LOADER) private loader: TranslocoLoader, @@ -144,6 +145,7 @@ export class TranslocoService { }); this.destroyRef.onDestroy(() => { + this.destroyed = true; // Complete subjects to release observers if users forget to unsubscribe manually. // This is important in server-side rendering. this.lang.complete(); @@ -193,6 +195,14 @@ export class TranslocoService { } load(path: string, options: LoadOptions = {}): Observable { + // If the application has already been destroyed, return an empty observable. + // We use EMPTY instead of NEVER to ensure the observable completes. + // This is important for operators like switchMap, which rely on the inner observable completing + // before they can subscribe to the next one. NEVER would hang the chain indefinitely. + if (this.destroyed) { + return EMPTY; + } + const cached = this.cache.get(path); if (cached) { return cached;