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 e7d99f02cb)
This commit is contained in:
SkyZeroZx
2026-01-06 20:56:05 -05:00
committed by kirjs
parent 63b1cdcf70
commit ae1dc16bb0
2 changed files with 40 additions and 2 deletions
@@ -42,8 +42,18 @@ export function debounce<TValue, TPathKind extends PathKind = PathKind.Root>(
function debounceForDuration(durationInMilliseconds: number): Debouncer<unknown> {
return (_context, abortSignal) => {
return new Promise((resolve) => {
const timeoutId = setTimeout(resolve, durationInMilliseconds);
abortSignal.addEventListener('abort', () => clearTimeout(timeoutId));
let timeoutId: ReturnType<typeof setTimeout> | undefined;
const onAbort = () => {
clearTimeout(timeoutId);
};
timeoutId = setTimeout(() => {
abortSignal.removeEventListener('abort', onAbort);
resolve();
}, durationInMilliseconds);
abortSignal.addEventListener('abort', onAbort, {once: true});
});
};
}
@@ -237,6 +237,34 @@ describe('debounce', () => {
expect(abortSpy).toHaveBeenCalledTimes(1);
expect(street.value()).toBe('1600 Amphitheatre Pkwy');
});
it('should remove abort listener when debounce completes', async () => {
const addListenerSpy = spyOn(AbortSignal.prototype, 'addEventListener').and.callThrough();
const removeListenerSpy = spyOn(
AbortSignal.prototype,
'removeEventListener',
).and.callThrough();
const address = signal({street: ''});
const addressForm = form(
address,
(address) => {
debounce(address.street, 1);
},
options(),
);
const street = addressForm.street();
street.setControlValue('1600 Amphitheatre Pkwy');
expect(addListenerSpy).toHaveBeenCalledOnceWith('abort', jasmine.any(Function), {
once: true,
});
expect(removeListenerSpy).not.toHaveBeenCalled();
await timeout(10);
expect(street.value()).toBe('1600 Amphitheatre Pkwy');
expect(removeListenerSpy).toHaveBeenCalledOnceWith('abort', jasmine.any(Function));
});
});
});