shareReplay with refCount: stale replay on resubscribe versus leaked subscription — which trade-off for cached HTTP streams?
0 reputation · 03 Jun 2023, 11:50 UTC
0 reputation · 03 Jun 2023, 11:50 UTC
I am wrapping HTTP calls in a shared service where multiple components subscribe to the same response stream, and I am using shareReplay to avoid duplicate requests. The documented config-object form, shareReplay({ bufferSize, refCount, windowTime }), forces a decision I cannot resolve from the docs alone.
With refCount: false, the underlying subscription appears to stay alive after the last subscriber unsubscribes, which risks a leaked subscription for long-lived services. With refCount: true, the source resubscribes when a new subscriber arrives, but the internal ReplaySubject reportedly still replays previously buffered values first — so a component expecting a fresh fetch may receive stale data before (or instead of) a new request.
Assume RxJS 7.x; I am aware the overloads and defaults differ from pre-6.4 versions, so answers should target the config-object signature. There also seems to be no built-in "reset buffer on resubscribe" option, leaving custom reset logic as the only middle ground.
refCount: true guaranteed by the documented contract, or an implementation detail that could change?bufferSize, refCount, and windowTime is the intended idiom to get "replay while active, refetch after idle" semantics?32275 reputation · 03 Jun 2023, 18:15 UTC
For a finite HTTP stream, the leak fear behind refCount: false is mostly unfounded: an HttpClient request completes after emitting, so the upstream subscription closes itself even though the shared ReplaySubject never unsubscribes. The real leak risk applies to infinite sources (websockets, intervals). Meanwhile, refCount: true does retain the internal ReplaySubject after teardown, so a resubscriber can see the last buffered value — but windowTime is the built-in bound on that staleness. The intended idiom is shareReplay({ bufferSize: 1, refCount: true, windowTime: ttl }).
It is observable, widely relied-upon behavior in RxJS 7.x, but the documented contract centers on multicasting and replay, not on "freshness on resubscribe." Treat exact resubscribe timing as version-sensitive: shareReplay semantics shifted across 6.4/6.5/7, so verify against your installed version rather than older blog posts about the positional-argument overload. What you can rely on: windowTime bounds how old a replayed value may be — a subscriber arriving more than windowTime after the last emission does not receive the stale buffer.
getData$ = defer(() => this.http.get<T>(url)).pipe(
shareReplay({ bufferSize: 1, refCount: true, windowTime: 30_000 })
);This gives you: concurrent subscribers share one request; late subscribers within the TTL get the cached value; after an idle period exceeding windowTime, the next subscription triggers a fresh request. Always set bufferSize (1 for HTTP) — the default is unbounded. Note the caution for non-idempotent endpoints: every resubscription after refCount teardown issues a real network request, so this pattern is for GET-style reads, not writes.
If your service is long-lived but the source is a single finite HTTP call and you genuinely want "fetch once, cache forever," refCount: false with a windowTime is simpler and not a leak — pick it deliberately rather than by default.
There is no built-in "clear buffer on resubscribe" option, and you should not try to reach into the ReplaySubject. The canonical approach is explicit invalidation: route fetches through a trigger so invalidation produces a new emission.
private refresh$ = new Subject<void>();
data$ = this.refresh$.pipe(
startWith(void 0),
switchMap(() => this.http.get<T>(url)),
shareReplay({ bufferSize: 1, refCount: true })
);
invalidate() { this.refresh$.next(); }Emitting on refresh$ pushes a fresh value into the shared replay, overwriting the stale buffer for everyone — no reference counting re-implemented. Combine with a short windowTime if you also want idle expiry.
Two cheap checks: (1) in Angular, use HttpTestingController — subscribe, unsubscribe all, wait less than and more than windowTime, resubscribe, and assert the request count; (2) add tap({ subscribe, finalize }) on the source inside defer() to log when refetching actually occurs. Also confirm error behavior: an error terminates the shared observable, and recovery depends on refCount teardown — make sure that matches your cache expectations.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.