Skip to content

Commit 424d7a0

Browse files
committed
feat: implement throttle
1 parent 53f11e7 commit 424d7a0

File tree

2 files changed

+77
-1
lines changed

2 files changed

+77
-1
lines changed

packages/core/src/shared/utilities/functionUtils.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,29 @@ export function keyedDebounce<T, U extends any[], K extends string = string>(
156156
return promise
157157
}
158158
}
159+
160+
/**
161+
* Wraps the target function such that it will only execute after {@link delay} milliseconds have passed
162+
* since the last invocation. Omitting {@link delay} will not execute the function for
163+
* a single event loop.
164+
*
165+
* Multiple calls made during the throttle window will return the last returned result.
166+
*/
167+
export function throttle<Input extends any[], Output>(
168+
fn: (...args: Input) => Output | Promise<Output>,
169+
delay: number = 0
170+
): (...args: Input) => Promise<Output> {
171+
let lastResult: Output
172+
let timeout: Timeout | undefined
173+
174+
return async (...args: Input) => {
175+
if (timeout) {
176+
return lastResult
177+
}
178+
179+
timeout = new Timeout(delay)
180+
timeout.onCompletion(() => (timeout = undefined))
181+
182+
return (lastResult = (await fn(...args)) as Output)
183+
}
184+
}

packages/core/src/test/shared/utilities/functionUtils.test.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import assert from 'assert'
7-
import { once, onceChanged, debounce } from '../../../shared/utilities/functionUtils'
7+
import { once, onceChanged, debounce, throttle } from '../../../shared/utilities/functionUtils'
88
import { installFakeClock } from '../../testUtil'
99

1010
describe('functionUtils', function () {
@@ -107,3 +107,53 @@ describe('debounce', function () {
107107
})
108108
})
109109
})
110+
111+
describe('throttle', function () {
112+
let counter: number
113+
let fn: () => Promise<number>
114+
let clock: ReturnType<typeof installFakeClock>
115+
116+
const callAndSleep = async (delayInMs: number) => {
117+
const r = await fn()
118+
await clock.tickAsync(delayInMs)
119+
return r
120+
}
121+
122+
const callAndSleepN = async (delayInMs: number, n: number) => {
123+
const results = []
124+
for (const _ of Array.from({ length: n })) {
125+
results.push(await callAndSleep(delayInMs))
126+
}
127+
return results
128+
}
129+
130+
beforeEach(function () {
131+
clock = installFakeClock()
132+
counter = 0
133+
fn = throttle(() => ++counter, 10)
134+
})
135+
136+
afterEach(function () {
137+
clock.uninstall()
138+
})
139+
140+
it('prevents a function from executing more than once in the `delay` window', async function () {
141+
await callAndSleepN(3, 3)
142+
assert.strictEqual(counter, 1, 'total calls should be 1')
143+
})
144+
145+
it('returns cached value on subsequent calls within window', async function () {
146+
const result = await callAndSleepN(3, 3)
147+
assert.deepStrictEqual(result, [1, 1, 1], 'all calls in window should return cached value')
148+
})
149+
150+
it('updates cache for next window', async function () {
151+
const result = await callAndSleepN(10, 3)
152+
assert.deepStrictEqual(result, [1, 2, 3], 'each call should return a new value')
153+
})
154+
155+
it('properly manages rolling cache window', async function () {
156+
const result = await callAndSleepN(5, 10)
157+
assert.deepStrictEqual(result, [1, 1, 2, 2, 3, 3, 4, 4, 5, 5])
158+
})
159+
})

0 commit comments

Comments
 (0)