Skip to content

Commit 0510a72

Browse files
added debounce utility function
Signed-off-by: Benjamin Strasser <bp.strasser@gmail.com>
1 parent ee3b412 commit 0510a72

File tree

5 files changed

+246
-15
lines changed

5 files changed

+246
-15
lines changed

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@
5454
"sveltekit-superforms": "^2.13.0",
5555
"tailwind-merge": "^2.3.0",
5656
"tailwind-variants": "^0.2.1",
57-
"throttle-debounce": "^5.0.2",
5857
"typesafe-utils": "^1.16.2",
5958
"zod": "^3.23.5"
6059
},
@@ -72,7 +71,6 @@
7271
"@types/jsonwebtoken": "^9.0.6",
7372
"@types/minimist": "^1.2.5",
7473
"@types/node": "^20.12.7",
75-
"@types/throttle-debounce": "^5.0.2",
7674
"@typescript-eslint/eslint-plugin": "^7.6.0",
7775
"@typescript-eslint/parser": "^7.6.0",
7876
"autoprefixer": "^10.4.19",

pnpm-lock.yaml

Lines changed: 3 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/container/projects/create-project.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import { page } from '$app/stores'
1010
import { toast } from 'svelte-sonner'
1111
import SlugDisplay from './slug-display.svelte'
12-
import { debounce } from 'throttle-debounce'
12+
import { debounce } from '$lib/utils/debounce'
1313
1414
export let data: SuperValidated<Infer<typeof createProjectSchema>>
1515
@@ -62,7 +62,7 @@
6262
}
6363
}
6464
)
65-
const checkProjectName = debounce(300, submit)
65+
const checkProjectName = debounce(submit, 300)
6666
</script>
6767

6868
<Dialog.Root bind:open>

src/lib/utils/debounce.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2+
type AnyFunction = (...args: any[]) => any
3+
4+
export function debounce<T extends AnyFunction>(
5+
func: T,
6+
wait: number
7+
): (...args: Parameters<T>) => void {
8+
let timeoutId: ReturnType<typeof setTimeout> | null = null
9+
10+
return function (this: unknown, ...args: Parameters<T>) {
11+
if (timeoutId !== null) {
12+
clearTimeout(timeoutId)
13+
}
14+
15+
timeoutId = setTimeout(() => {
16+
func.apply(this, args)
17+
timeoutId = null
18+
}, wait)
19+
}
20+
}
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2+
import { debounce } from './debounce'
3+
4+
describe('debounce', () => {
5+
beforeEach(() => {
6+
vi.useFakeTimers()
7+
})
8+
9+
afterEach(() => {
10+
vi.useRealTimers()
11+
})
12+
13+
it('should delay function execution', () => {
14+
const mockFn = vi.fn()
15+
const debouncedFn = debounce(mockFn, 1000)
16+
17+
debouncedFn()
18+
expect(mockFn).not.toBeCalled()
19+
20+
vi.advanceTimersByTime(500)
21+
debouncedFn()
22+
expect(mockFn).not.toBeCalled()
23+
24+
vi.advanceTimersByTime(999)
25+
expect(mockFn).not.toBeCalled()
26+
27+
vi.advanceTimersByTime(1)
28+
expect(mockFn).toBeCalledTimes(1)
29+
})
30+
31+
it('should handle errors', () => {
32+
const mockError = new Error('Sync error')
33+
const mockFn = vi.fn(() => {
34+
throw mockError
35+
})
36+
const debouncedFn = debounce(mockFn, 1000)
37+
38+
expect(() => {
39+
debouncedFn()
40+
vi.advanceTimersByTime(1000)
41+
}).toThrow(mockError)
42+
})
43+
44+
it('should call the function only once for multiple calls within the wait time', () => {
45+
const mockFn = vi.fn()
46+
const debouncedFn = debounce(mockFn, 1000)
47+
48+
debouncedFn()
49+
debouncedFn()
50+
debouncedFn()
51+
52+
vi.advanceTimersByTime(1000)
53+
expect(mockFn).toBeCalledTimes(1)
54+
})
55+
56+
it('should pass arguments to the debounced function', () => {
57+
const mockFn = vi.fn()
58+
const debouncedFn = debounce(mockFn, 1000)
59+
60+
debouncedFn('test', 123)
61+
62+
vi.advanceTimersByTime(1000)
63+
expect(mockFn).toBeCalledWith('test', 123)
64+
})
65+
66+
it('should use the latest arguments for delayed execution', () => {
67+
const mockFn = vi.fn()
68+
const debouncedFn = debounce(mockFn, 1000)
69+
70+
debouncedFn('first', 1)
71+
debouncedFn('second', 2)
72+
debouncedFn('third', 3)
73+
74+
vi.advanceTimersByTime(1000)
75+
expect(mockFn).toBeCalledTimes(1)
76+
expect(mockFn).toBeCalledWith('third', 3)
77+
})
78+
79+
it('should maintain the correct context', () => {
80+
const obj = {
81+
value: 'test',
82+
method: vi.fn(function (this: { value: string }) {
83+
return this.value
84+
})
85+
}
86+
87+
const debouncedMethod = debounce(obj.method, 1000)
88+
89+
debouncedMethod.call(obj)
90+
vi.advanceTimersByTime(1000)
91+
92+
expect(obj.method).toBeCalledTimes(1)
93+
expect(obj.method.mock.results[0]?.value).toBe('test')
94+
})
95+
96+
it('should allow immediate execution after the wait time', () => {
97+
const mockFn = vi.fn()
98+
const debouncedFn = debounce(mockFn, 1000)
99+
100+
debouncedFn()
101+
vi.advanceTimersByTime(1000)
102+
expect(mockFn).toBeCalledTimes(1)
103+
104+
debouncedFn()
105+
vi.advanceTimersByTime(1000)
106+
expect(mockFn).toBeCalledTimes(2)
107+
})
108+
109+
it('should work with a wait time of 0', () => {
110+
const mockFn = vi.fn()
111+
const debouncedFn = debounce(mockFn, 0)
112+
113+
debouncedFn()
114+
vi.advanceTimersByTime(0)
115+
expect(mockFn).toBeCalledTimes(1)
116+
})
117+
118+
it('should cancel pending executions', () => {
119+
const mockFn = vi.fn()
120+
const debouncedFn = debounce(mockFn, 1000)
121+
122+
debouncedFn()
123+
vi.advanceTimersByTime(500)
124+
debouncedFn()
125+
vi.advanceTimersByTime(500)
126+
expect(mockFn).not.toBeCalled()
127+
128+
vi.advanceTimersByTime(500)
129+
expect(mockFn).toBeCalledTimes(1)
130+
})
131+
132+
it('should handle multiple debounced functions independently', () => {
133+
const mockFn1 = vi.fn()
134+
const mockFn2 = vi.fn()
135+
const debouncedFn1 = debounce(mockFn1, 1000)
136+
const debouncedFn2 = debounce(mockFn2, 500)
137+
138+
debouncedFn1()
139+
debouncedFn2()
140+
141+
vi.advanceTimersByTime(500)
142+
expect(mockFn1).not.toBeCalled()
143+
expect(mockFn2).toBeCalledTimes(1)
144+
145+
vi.advanceTimersByTime(500)
146+
expect(mockFn1).toBeCalledTimes(1)
147+
expect(mockFn2).toBeCalledTimes(1)
148+
})
149+
150+
it('should work with async functions', async () => {
151+
const mockFn = vi.fn().mockResolvedValue('async result')
152+
const debouncedFn = debounce(mockFn, 1000)
153+
154+
debouncedFn()
155+
vi.advanceTimersByTime(1000)
156+
157+
await vi.runAllTimersAsync()
158+
159+
expect(mockFn).toHaveBeenCalledTimes(1)
160+
})
161+
162+
it('should debounce multiple calls to async functions', async () => {
163+
const mockFn = vi.fn().mockResolvedValue('async result')
164+
const debouncedFn = debounce(mockFn, 1000)
165+
166+
debouncedFn()
167+
debouncedFn()
168+
debouncedFn()
169+
170+
vi.advanceTimersByTime(1000)
171+
172+
await vi.runAllTimersAsync()
173+
174+
expect(mockFn).toHaveBeenCalledTimes(1)
175+
})
176+
177+
it('should pass arguments to async functions', async () => {
178+
const mockFn = vi.fn().mockResolvedValue('async result')
179+
const debouncedFn = debounce(mockFn, 1000)
180+
181+
debouncedFn('arg1', 'arg2')
182+
vi.advanceTimersByTime(1000)
183+
184+
await vi.runAllTimersAsync()
185+
186+
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2')
187+
})
188+
189+
it('should maintain the correct context for async methods', async () => {
190+
const obj = {
191+
value: 'test',
192+
method: vi.fn(async function (this: { value: string }) {
193+
return this.value
194+
})
195+
}
196+
197+
const debouncedMethod = debounce(obj.method, 1000)
198+
199+
debouncedMethod.call(obj)
200+
vi.advanceTimersByTime(1000)
201+
202+
await vi.runAllTimersAsync()
203+
204+
expect(obj.method).toHaveBeenCalledTimes(1)
205+
expect(await obj.method.mock.results[0]?.value).toBe('test')
206+
})
207+
208+
it('should cancel pending async function calls', async () => {
209+
const mockFn = vi.fn().mockResolvedValue('async result')
210+
const debouncedFn = debounce(mockFn, 1000)
211+
212+
debouncedFn()
213+
vi.advanceTimersByTime(500)
214+
debouncedFn()
215+
vi.advanceTimersByTime(500)
216+
217+
await vi.runAllTimersAsync()
218+
219+
expect(mockFn).toHaveBeenCalledTimes(1)
220+
})
221+
})

0 commit comments

Comments
 (0)