diff --git a/.github/workflows/ci_ts_latest.yml b/.github/workflows/ci_ts_latest.yml index ce502d7bbf..8ce45c206c 100644 --- a/.github/workflows/ci_ts_latest.yml +++ b/.github/workflows/ci_ts_latest.yml @@ -23,6 +23,6 @@ jobs: run: | npm install -g npm@latest npm ci - npm install --no-save typescript@latest + npm install --no-save typescript@latest tslib@latest @types/node@latest npm run build_all diff --git a/spec/operators/shareReplay-spec.ts b/spec/operators/shareReplay-spec.ts index c81f697766..eaf6867b86 100644 --- a/spec/operators/shareReplay-spec.ts +++ b/spec/operators/shareReplay-spec.ts @@ -262,4 +262,26 @@ describe('shareReplay operator', () => { expectObservable(result).toBe(expected); }); + const FinalizationRegistry = (global as any).FinalizationRegistry; + if (FinalizationRegistry) { + + it('should not leak the subscriber for sync sources', (done) => { + const registry = new FinalizationRegistry((value: any) => { + expect(value).to.equal('callback'); + done(); + }); + let callback: (() => void) | undefined = () => { /* noop */ }; + registry.register(callback, 'callback'); + + const shared = of(42).pipe(shareReplay(1)); + shared.subscribe(callback); + + callback = undefined; + global.gc(); + }); + + } else { + console.warn(`No support for FinalizationRegistry in Node ${process.version}`); + } + }); diff --git a/spec/support/default.opts b/spec/support/default.opts index 32264362e9..7257b818fd 100644 --- a/spec/support/default.opts +++ b/spec/support/default.opts @@ -6,6 +6,7 @@ --reporter dot +--expose-gc --check-leaks --globals WebSocket,FormData,XDomainRequest,ActiveXObject,fetch,AbortController diff --git a/src/internal/operators/shareReplay.ts b/src/internal/operators/shareReplay.ts index e8976b3c7d..6226d2fce8 100644 --- a/src/internal/operators/shareReplay.ts +++ b/src/internal/operators/shareReplay.ts @@ -56,8 +56,14 @@ export interface ShareReplayConfig { * @method shareReplay * @owner Observable */ -export function shareReplay(config: ShareReplayConfig): MonoTypeOperatorFunction; -export function shareReplay(bufferSize?: number, windowTime?: number, scheduler?: SchedulerLike): MonoTypeOperatorFunction; +export function shareReplay( + config: ShareReplayConfig +): MonoTypeOperatorFunction; +export function shareReplay( + bufferSize?: number, + windowTime?: number, + scheduler?: SchedulerLike +): MonoTypeOperatorFunction; export function shareReplay( configOrBufferSize?: ShareReplayConfig | number, windowTime?: number, @@ -71,7 +77,7 @@ export function shareReplay( bufferSize: configOrBufferSize as number | undefined, windowTime, refCount: false, - scheduler + scheduler, }; } return (source: Observable) => source.lift(shareReplayOperator(config)); @@ -81,7 +87,7 @@ function shareReplayOperator({ bufferSize = Number.POSITIVE_INFINITY, windowTime = Number.POSITIVE_INFINITY, refCount: useRefCount, - scheduler + scheduler, }: ShareReplayConfig) { let subject: ReplaySubject | undefined; let refCount = 0; @@ -89,7 +95,10 @@ function shareReplayOperator({ let hasError = false; let isComplete = false; - return function shareReplayOperation(this: Subscriber, source: Observable) { + return function shareReplayOperation( + this: Subscriber, + source: Observable + ) { refCount++; let innerSub: Subscription; if (!subject || hasError) { @@ -97,7 +106,9 @@ function shareReplayOperator({ subject = new ReplaySubject(bufferSize, windowTime, scheduler); innerSub = subject.subscribe(this); subscription = source.subscribe({ - next(value) { subject.next(value); }, + next(value) { + subject.next(value); + }, error(err) { hasError = true; subject.error(err); @@ -108,6 +119,14 @@ function shareReplayOperator({ subject.complete(); }, }); + + // Here we need to check to see if the source synchronously completed. Although + // we're setting `subscription = undefined` in the completion handler, if the source + // is synchronous, that will happen *before* subscription is set by the return of + // the `subscribe` call. + if (isComplete) { + subscription = undefined; + } } else { innerSub = subject.subscribe(this); } @@ -115,6 +134,7 @@ function shareReplayOperator({ this.add(() => { refCount--; innerSub.unsubscribe(); + innerSub = undefined; if (subscription && !isComplete && useRefCount && refCount === 0) { subscription.unsubscribe(); subscription = undefined;