Skip to content

Commit 8c60cbf

Browse files
atscottkirjs
authored andcommitted
fix(core): takeUntilDestroyed completes immediately if DestroyRef already destroyed (#61847)
Adds fix directly for `takeUntilDestroyed` to unsubscribe when already destroyed instead of putting synchronous behavior on `DestroyRef.onDestroyed` callback as in #58008 fixes #54527 PR Close #61847
1 parent 3c64468 commit 8c60cbf

File tree

4 files changed

+62
-5
lines changed

4 files changed

+62
-5
lines changed

packages/core/rxjs-interop/src/take_until_destroyed.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@ export function takeUntilDestroyed<T>(destroyRef?: DestroyRef): MonoTypeOperator
2626
destroyRef = inject(DestroyRef);
2727
}
2828

29-
const destroyed$ = new Observable<void>((observer) => {
30-
const unregisterFn = destroyRef!.onDestroy(observer.next.bind(observer));
29+
const destroyed$ = new Observable<void>((subscriber) => {
30+
if ((destroyRef as unknown as {destroyed: boolean}).destroyed) {
31+
subscriber.next();
32+
return;
33+
}
34+
const unregisterFn = destroyRef!.onDestroy(subscriber.next.bind(subscriber));
3135
return unregisterFn;
3236
});
3337

packages/core/rxjs-interop/test/take_until_destroyed_spec.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,18 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {DestroyRef, EnvironmentInjector, Injector, runInInjectionContext} from '../../src/core';
10-
import {BehaviorSubject} from 'rxjs';
9+
import {
10+
Component,
11+
DestroyRef,
12+
EnvironmentInjector,
13+
inject,
14+
Injector,
15+
runInInjectionContext,
16+
} from '../../src/core';
17+
import {BehaviorSubject, finalize} from 'rxjs';
1118

1219
import {takeUntilDestroyed} from '../src/take_until_destroyed';
20+
import {TestBed} from '@angular/core/testing';
1321

1422
describe('takeUntilDestroyed', () => {
1523
it('should complete an observable when the current context is destroyed', () => {
@@ -76,4 +84,39 @@ describe('takeUntilDestroyed', () => {
7684

7785
expect(unregisterFn).toHaveBeenCalled();
7886
});
87+
88+
// https://github.com/angular/angular/issues/54527
89+
it('should unsubscribe after the current context has already been destroyed', async () => {
90+
const recorder: any[] = [];
91+
92+
// Note that we need a "real" view for this test because, in other cases,
93+
// `DestroyRef` would resolve to the root injector rather than to the
94+
// `NodeInjectorDestroyRef`, where `lView` is used.
95+
@Component({template: ''})
96+
class TestComponent {
97+
destroyRef = inject(DestroyRef);
98+
99+
source$ = new BehaviorSubject(0);
100+
101+
ngOnDestroy(): void {
102+
Promise.resolve().then(() => {
103+
this.source$
104+
.pipe(
105+
takeUntilDestroyed(this.destroyRef),
106+
finalize(() => recorder.push('finalize()')),
107+
)
108+
.subscribe((value) => recorder.push(value));
109+
});
110+
}
111+
}
112+
113+
const fixture = TestBed.createComponent(TestComponent);
114+
fixture.destroy();
115+
116+
// Wait until the `ngOnDestroy` microtask is run.
117+
await Promise.resolve();
118+
119+
// Ensure the `value` is not recorded, but unsubscribed immediately.
120+
expect(recorder).toEqual(['finalize()']);
121+
});
79122
});

packages/core/src/di/r3_injector.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ export abstract class EnvironmentInjector implements Injector {
175175

176176
abstract destroy(): void;
177177

178+
/** @internal */
179+
abstract get destroyed(): boolean;
180+
178181
/**
179182
* @internal
180183
*/
@@ -199,7 +202,7 @@ export class R3Injector extends EnvironmentInjector implements PrimitivesInjecto
199202
/**
200203
* Flag indicating that this injector was previously destroyed.
201204
*/
202-
get destroyed(): boolean {
205+
override get destroyed(): boolean {
203206
return this._destroyed;
204207
}
205208
private _destroyed = false;

packages/core/src/linker/destroy_ref.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ export abstract class DestroyRef {
4646
*/
4747
abstract onDestroy(callback: () => void): () => void;
4848

49+
/** @internal */
50+
abstract get destroyed(): boolean;
51+
4952
/**
5053
* @internal
5154
* @nocollapse
@@ -64,6 +67,10 @@ export class NodeInjectorDestroyRef extends DestroyRef {
6467
super();
6568
}
6669

70+
override get destroyed() {
71+
return isDestroyed(this._lView);
72+
}
73+
6774
override onDestroy(callback: () => void): () => void {
6875
const lView = this._lView;
6976

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy