Skip to content

Commit 6bf6dbc

Browse files
arturovtthePunderWoman
authored andcommitted
fix(core): cleanup testability subscriptions (#61261)
This commit prevents leaking memory when the application is destroyed and subscriptions are still alive. PR Close #61261
1 parent a2fdb49 commit 6bf6dbc

File tree

4 files changed

+28
-11
lines changed

4 files changed

+28
-11
lines changed

goldens/size-tracking/integration-payloads.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"cli-hello-world": {
33
"uncompressed": {
4-
"main": 132425,
5-
"polyfills": 33792
4+
"main": 137461,
5+
"polyfills": 34579
66
}
77
},
88
"cli-hello-world-ivy-i18n": {

packages/core/src/testability/testability.ts

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

9-
import {Inject, Injectable, InjectionToken} from '../di';
9+
import {inject, Inject, Injectable, InjectionToken} from '../di';
10+
import {isInInjectionContext} from '../di/contextual';
11+
import {DestroyRef} from '../linker/destroy_ref';
1012
import {NgZone} from '../zone/ng_zone';
1113

1214
/**
@@ -84,13 +86,21 @@ export class Testability implements PublicTestability {
8486
private _isZoneStable: boolean = true;
8587
private _callbacks: WaitCallback[] = [];
8688

87-
private taskTrackingZone: {macroTasks: Task[]} | null = null;
89+
private _taskTrackingZone: {macroTasks: Task[]} | null = null;
90+
91+
private _destroyRef?: DestroyRef;
8892

8993
constructor(
9094
private _ngZone: NgZone,
9195
private registry: TestabilityRegistry,
9296
@Inject(TESTABILITY_GETTER) testabilityGetter: GetTestability,
9397
) {
98+
// Attempt to retrieve a `DestroyRef` optionally.
99+
// For backwards compatibility reasons, this cannot be required.
100+
if (isInInjectionContext()) {
101+
this._destroyRef = inject(DestroyRef, {optional: true}) ?? undefined;
102+
}
103+
94104
// If there was no Testability logic registered in the global scope
95105
// before, register the current testability getter as a global one.
96106
if (!_testabilityGetter) {
@@ -99,19 +109,19 @@ export class Testability implements PublicTestability {
99109
}
100110
this._watchAngularEvents();
101111
_ngZone.run(() => {
102-
this.taskTrackingZone =
112+
this._taskTrackingZone =
103113
typeof Zone == 'undefined' ? null : Zone.current.get('TaskTrackingZone');
104114
});
105115
}
106116

107117
private _watchAngularEvents(): void {
108-
this._ngZone.onUnstable.subscribe({
118+
const onUnstableSubscription = this._ngZone.onUnstable.subscribe({
109119
next: () => {
110120
this._isZoneStable = false;
111121
},
112122
});
113123

114-
this._ngZone.runOutsideAngular(() => {
124+
const onStableSubscription = this._ngZone.runOutsideAngular(() =>
115125
this._ngZone.onStable.subscribe({
116126
next: () => {
117127
NgZone.assertNotInAngularZone();
@@ -120,7 +130,12 @@ export class Testability implements PublicTestability {
120130
this._runCallbacksIfReady();
121131
});
122132
},
123-
});
133+
}),
134+
);
135+
136+
this._destroyRef?.onDestroy(() => {
137+
onUnstableSubscription.unsubscribe();
138+
onStableSubscription.unsubscribe();
124139
});
125140
}
126141

@@ -156,12 +171,12 @@ export class Testability implements PublicTestability {
156171
}
157172

158173
private getPendingTasks(): PendingMacrotask[] {
159-
if (!this.taskTrackingZone) {
174+
if (!this._taskTrackingZone) {
160175
return [];
161176
}
162177

163178
// Copy the tasks data so that we don't leak tasks.
164-
return this.taskTrackingZone.macroTasks.map((t: Task) => {
179+
return this._taskTrackingZone.macroTasks.map((t: Task) => {
165180
return {
166181
source: t.source,
167182
// From TaskTrackingZone:
@@ -196,7 +211,7 @@ export class Testability implements PublicTestability {
196211
* and no further updates will be issued.
197212
*/
198213
whenStable(doneCb: Function, timeout?: number, updateCb?: Function): void {
199-
if (updateCb && !this.taskTrackingZone) {
214+
if (updateCb && !this._taskTrackingZone) {
200215
throw new Error(
201216
'Task tracking zone is required when passing an update callback to ' +
202217
'whenStable(). Is "zone.js/plugins/task-tracking" loaded?',

packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,7 @@
459459
"isFormControlState",
460460
"isForwardRef",
461461
"isFunction",
462+
"isInInjectionContext",
462463
"isInlineTemplate",
463464
"isInputBinding",
464465
"isInteropObservable",

packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@
446446
"isFormControlState",
447447
"isForwardRef",
448448
"isFunction",
449+
"isInInjectionContext",
449450
"isInlineTemplate",
450451
"isInputBinding",
451452
"isInteropObservable",

0 commit comments

Comments
 (0)