Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/core/rxjs-interop/src/take_until_destroyed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ export function takeUntilDestroyed<T>(destroyRef?: DestroyRef): MonoTypeOperator
destroyRef = inject(DestroyRef);
}

const destroyed$ = new Observable<void>((observer) => {
const unregisterFn = destroyRef!.onDestroy(observer.next.bind(observer));
const destroyed$ = new Observable<void>((subscriber) => {
if ((destroyRef as unknown as {destroyed: boolean}).destroyed) {
subscriber.next();
return;
}
const unregisterFn = destroyRef!.onDestroy(subscriber.next.bind(subscriber));
return unregisterFn;
});

Expand Down
47 changes: 45 additions & 2 deletions packages/core/rxjs-interop/test/take_until_destroyed_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@
* found in the LICENSE file at https://angular.dev/license
*/

import {DestroyRef, EnvironmentInjector, Injector, runInInjectionContext} from '../../src/core';
import {BehaviorSubject} from 'rxjs';
import {
Component,
DestroyRef,
EnvironmentInjector,
inject,
Injector,
runInInjectionContext,
} from '../../src/core';
import {BehaviorSubject, finalize} from 'rxjs';

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

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

expect(unregisterFn).toHaveBeenCalled();
});

// https://github.com/angular/angular/issues/54527
it('should unsubscribe after the current context has already been destroyed', async () => {
const recorder: any[] = [];

// Note that we need a "real" view for this test because, in other cases,
// `DestroyRef` would resolve to the root injector rather than to the
// `NodeInjectorDestroyRef`, where `lView` is used.
@Component({template: ''})
class TestComponent {
destroyRef = inject(DestroyRef);

source$ = new BehaviorSubject(0);

ngOnDestroy(): void {
Promise.resolve().then(() => {
this.source$
.pipe(
takeUntilDestroyed(this.destroyRef),
finalize(() => recorder.push('finalize()')),
)
.subscribe((value) => recorder.push(value));
});
}
}

const fixture = TestBed.createComponent(TestComponent);
fixture.destroy();

// Wait until the `ngOnDestroy` microtask is run.
await Promise.resolve();

// Ensure the `value` is not recorded, but unsubscribed immediately.
expect(recorder).toEqual(['finalize()']);
});
});
5 changes: 4 additions & 1 deletion packages/core/src/di/r3_injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ export abstract class EnvironmentInjector implements Injector {

abstract destroy(): void;

/** @internal */
abstract get destroyed(): boolean;

/**
* @internal
*/
Expand All @@ -199,7 +202,7 @@ export class R3Injector extends EnvironmentInjector implements PrimitivesInjecto
/**
* Flag indicating that this injector was previously destroyed.
*/
get destroyed(): boolean {
override get destroyed(): boolean {
return this._destroyed;
}
private _destroyed = false;
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/linker/destroy_ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ export abstract class DestroyRef {
*/
abstract onDestroy(callback: () => void): () => void;

/** @internal */
abstract get destroyed(): boolean;

/**
* @internal
* @nocollapse
Expand All @@ -64,6 +67,10 @@ export class NodeInjectorDestroyRef extends DestroyRef {
super();
}

override get destroyed() {
return isDestroyed(this._lView);
}

override onDestroy(callback: () => void): () => void {
const lView = this._lView;

Expand Down