Skip to content

Commit ae1dc16

Browse files
SkyZeroZxkirjs
authored andcommitted
fix(forms): clean up abort listener after timeout
Removes the abort event listener once the debounce timeout completes. This avoids lingering listeners, prevents potential memory leaks, and ensures the abort logic runs at most once. (cherry picked from commit e7d99f0)
1 parent 63b1cdc commit ae1dc16

File tree

2 files changed

+40
-2
lines changed

2 files changed

+40
-2
lines changed

packages/forms/signals/src/api/rules/debounce.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,18 @@ export function debounce<TValue, TPathKind extends PathKind = PathKind.Root>(
4242
function debounceForDuration(durationInMilliseconds: number): Debouncer<unknown> {
4343
return (_context, abortSignal) => {
4444
return new Promise((resolve) => {
45-
const timeoutId = setTimeout(resolve, durationInMilliseconds);
46-
abortSignal.addEventListener('abort', () => clearTimeout(timeoutId));
45+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
46+
47+
const onAbort = () => {
48+
clearTimeout(timeoutId);
49+
};
50+
51+
timeoutId = setTimeout(() => {
52+
abortSignal.removeEventListener('abort', onAbort);
53+
resolve();
54+
}, durationInMilliseconds);
55+
56+
abortSignal.addEventListener('abort', onAbort, {once: true});
4757
});
4858
};
4959
}

packages/forms/signals/test/node/api/debounce.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,34 @@ describe('debounce', () => {
237237
expect(abortSpy).toHaveBeenCalledTimes(1);
238238
expect(street.value()).toBe('1600 Amphitheatre Pkwy');
239239
});
240+
241+
it('should remove abort listener when debounce completes', async () => {
242+
const addListenerSpy = spyOn(AbortSignal.prototype, 'addEventListener').and.callThrough();
243+
const removeListenerSpy = spyOn(
244+
AbortSignal.prototype,
245+
'removeEventListener',
246+
).and.callThrough();
247+
248+
const address = signal({street: ''});
249+
const addressForm = form(
250+
address,
251+
(address) => {
252+
debounce(address.street, 1);
253+
},
254+
options(),
255+
);
256+
const street = addressForm.street();
257+
258+
street.setControlValue('1600 Amphitheatre Pkwy');
259+
expect(addListenerSpy).toHaveBeenCalledOnceWith('abort', jasmine.any(Function), {
260+
once: true,
261+
});
262+
expect(removeListenerSpy).not.toHaveBeenCalled();
263+
264+
await timeout(10);
265+
expect(street.value()).toBe('1600 Amphitheatre Pkwy');
266+
expect(removeListenerSpy).toHaveBeenCalledOnceWith('abort', jasmine.any(Function));
267+
});
240268
});
241269
});
242270

0 commit comments

Comments
 (0)