Skip to content

Commit 6976349

Browse files
arturovtAndrewKushnir
authored andcommitted
fix(zone.js): remove abort listener once fetch is settled (angular#57882)
This commit updates the `fetch` patch for zone.js. Currently, we're attaching an `abort` event listener on the signal (when it's provided) and never removing it. We should be good citizens and remove event listeners whenever objects need to be properly collected. In Firefox, when saving a heap snapshot and running it through `fxsnapshot`, querying `AbortSignal` will print a so-called "CaptureMap" with a list of "lambdas," indicating that the signal is not garbage collected because of the event listener lambda function. PR Close angular#57882
1 parent 8d8c03a commit 6976349

File tree

2 files changed

+27
-9
lines changed

2 files changed

+27
-9
lines changed

packages/zone.js/lib/common/fetch.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -99,22 +99,31 @@ export function patchFetch(Zone: ZoneType): void {
9999
options.signal = fetchSignal;
100100
args[1] = options;
101101

102+
let onAbort: () => void;
102103
if (signal) {
103104
const nativeAddEventListener =
104105
signal[Zone.__symbol__('addEventListener') as 'addEventListener'] ||
105106
signal.addEventListener;
106107

107-
nativeAddEventListener.call(
108-
signal,
109-
'abort',
110-
function () {
111-
ac!.abort();
112-
},
113-
{once: true},
114-
);
108+
onAbort = () => ac!.abort();
109+
nativeAddEventListener.call(signal, 'abort', onAbort, {once: true});
115110
}
116111

117-
return createFetchTask('fetch', {fetchArgs: args} as FetchTaskData, fetch, this, args, ac);
112+
return createFetchTask(
113+
'fetch',
114+
{fetchArgs: args} as FetchTaskData,
115+
fetch,
116+
this,
117+
args,
118+
ac,
119+
).finally(() => {
120+
// We need to be good citizens and remove the `abort` listener once
121+
// the fetch is settled. The `abort` listener may not be called at all,
122+
// which means the event listener closure would retain a reference to
123+
// the `ac` object even if it goes out of scope. Since browser's garbage
124+
// collectors work differently, some may not be smart enough to collect a signal.
125+
signal?.removeEventListener('abort', onAbort);
126+
});
118127
};
119128

120129
if (OriginalResponse?.prototype) {

packages/zone.js/test/common/fetch.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,10 @@ describe(
169169
'invokeTask:fetch:macroTask',
170170
'scheduleTask:Promise.then:microTask',
171171
'invokeTask:Promise.then:microTask',
172+
173+
// This is the `finally` task, which is used for cleanup.
174+
'scheduleTask:Promise.then:microTask',
175+
'invokeTask:Promise.then:microTask',
172176
]);
173177
done();
174178
},
@@ -194,6 +198,11 @@ describe(
194198
'invokeTask:fetch:macroTask',
195199
'scheduleTask:Promise.then:microTask',
196200
'invokeTask:Promise.then:microTask',
201+
202+
// This is the `finally` task, which is used for cleanup.
203+
'scheduleTask:Promise.then:microTask',
204+
'invokeTask:Promise.then:microTask',
205+
197206
// Please refer to the issue link above. Previously, `Response` methods were not
198207
// patched by zone.js, and their return values were considered only as
199208
// microtasks (not macrotasks). The Angular zone stabilized prematurely,

0 commit comments

Comments
 (0)