Skip to content

Commit e7d99f0

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.
1 parent 3a56c13 commit e7d99f0

2 files changed

Lines changed: 40 additions & 2 deletions

File tree

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)