takeUntil subscription still retaining observers after component teardown — diagnosis needed
0 reputation · 08 Feb 2021, 18:30 UTC
0 reputation · 08 Feb 2021, 18:30 UTC
I have an Angular component that subscribes to a long-lived observable (an interval-based stream) and uses takeUntil with a dedicated Subject that emits in ngOnDestroy. The intent is that destroying the component releases the subscription and everything it references.
The uncertainty is whether this pattern alone guarantees teardown in my setup. The same destroy subject is also passed to a second subscription, and the source stream is wrapped in shareReplay without an explicit refCount configuration. I am unsure whether a shared takeUntil notifier can cause premature or incomplete unsubscription, or whether shareReplay keeps an internal subscription alive even after all downstream observers unsubscribe.
Assume RxJS 7.x and a current Angular LTS. I plan to verify with repeated mount/destroy cycles and heap snapshots in Chrome DevTools, comparing retained size against a version without takeUntil.
shareReplay without refCount: true retain the source subscription after the last takeUntil-terminated subscriber unsubscribes?35275 reputation · 09 Feb 2021, 00:31 UTC
shareReplay without refCount: true (the default in RxJS 7.0–7.3) retains its internal ReplaySubject and the upstream subscription indefinitely once the first subscriber arrives. When the last downstream subscriber (terminated by takeUntil) unsubscribes, the inner subscription to the source (e.g., interval) is not released.Subject across multiple takeUntil operators is safe. Each takeUntil creates its own inner subscription to the notifier; the notifier’s next/complete only tears down the specific chain it’s piped into. The risk is incomplete teardown if the shared notifier never emits (e.g., ngOnDestroy not called), not premature completion.ReplaySubject and Subscriber (or SafeSubscriber) instances. After each destroy cycle, a heap snapshot filtered for “ReplaySubject” should show a constant instance count. A growing count — or a Subscriber still linked to the source observable (e.g., the interval timer closure) — confirms the leak lives in shareReplay’s internal subscription, not in the destroy subject.With shareReplay({ bufferSize: 1, refCount: false }) (the RxJS 7.0–7.3 default), the first subscription connects to the source and creates a ReplaySubject that replays the buffered value to late subscribers. That connection is never reference-counted. When your component’s takeUntil(destroy$) unsubscribes, the downstream chain tears down, but the shareReplay’s inner subscription to interval remains active, holding the timer callback and any buffered values in memory.
source$.pipe(
shareReplay({ bufferSize: 1, refCount: true }), // or shareReplay(1) in RxJS 7.4+
takeUntil(this.destroy$)
).subscribe(...);
With refCount: true, the shared observable becomes reference-counted: when the last downstream subscriber unsubscribes (via takeUntil), the inner subscription to the source is torn down.
npm list rxjs | grep rxjs@. RxJS 7.4+ defaults refCount to true when windowTime is omitted; earlier versions default to false.finalize log before shareReplay to see when the source subscription actually tears down:
source$.pipe(
tap({ subscribe: () => console.log('sub'), finalize: () => console.log('unsub') }),
shareReplay({ refCount: true, bufferSize: 1 }),
takeUntil(this.destroy$)
).subscribe(...);
refCount: true.SubscriptionTracker service that increments a counter in subscribe and decrements in finalize; assert zero after destroy.What is your exact RxJS patch version (e.g., 7.2.3 vs 7.8.1)? If you’re on 7.4+, shareReplay(1) already enables refCount by default and the leak may not exist; if you’re on 7.0–7.3, the explicit { refCount: true } is required.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.