Skip to content

Commit 25aa81f

Browse files
authored
feat(async/unstable): allow setting dynamic timeframe for throttle (#7002)
1 parent 46a16b7 commit 25aa81f

File tree

2 files changed

+78
-5
lines changed

2 files changed

+78
-5
lines changed

async/unstable_throttle.ts

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,45 +70,86 @@ export interface ThrottledFunction<T extends Array<unknown>> {
7070
* assert(func.lastExecution > 0);
7171
* ```
7272
*
73+
* @example With dynamic timeframe
74+
*
75+
* ```ts no-assert
76+
* import { throttle } from "@std/async/unstable-throttle";
77+
*
78+
* function processUserInput(input: string) {
79+
* // Do some expensive computation with user input that changes on each
80+
* // keypress, which takes a variable amount of time depending on the length
81+
* // or complexity of input.
82+
* }
83+
*
84+
* const processUserInputThrottled = throttle(
85+
* processUserInput,
86+
* // Throttle dynamically, waiting twice as long as the previous execution
87+
* // took to complete before starting the next call.
88+
* (n) => n * 2,
89+
* { ensureLastCall: true },
90+
* );
91+
* ```
92+
*
7393
* @typeParam T The arguments of the provided function.
7494
* @param fn The function to throttle.
7595
* @param timeframe The timeframe in milliseconds in which the function should be called at most once.
96+
* If a callback function is supplied, it will be called with the duration of
97+
* the previous execution and should return the
98+
* next timeframe to use in milliseconds.
7699
* @param options Additional options.
77100
* @returns The throttled function.
78101
*/
79102
// deno-lint-ignore no-explicit-any
80103
export function throttle<T extends Array<any>>(
81104
fn: (this: ThrottledFunction<T>, ...args: T) => void,
82-
timeframe: number,
105+
timeframe: number | ((previousDuration: number) => number),
83106
options?: ThrottleOptions,
84107
): ThrottledFunction<T> {
85108
const ensureLast = Boolean(options?.ensureLastCall);
86109
let timeout = -1;
87110

88111
let lastExecution = -Infinity;
89112
let flush: (() => void) | null = null;
113+
let throttlingAsync = false;
114+
115+
let tf = typeof timeframe === "function" ? 0 : timeframe;
90116

91117
const throttled = ((...args: T) => {
92118
flush = () => {
119+
const start = Date.now();
120+
let result: unknown;
121+
const done = () => {
122+
throttlingAsync = false;
123+
lastExecution = Date.now();
124+
if (typeof timeframe === "function") {
125+
tf = timeframe(lastExecution - start);
126+
}
127+
};
93128
try {
94129
clearTimeout(timeout);
95-
fn.call(throttled, ...args);
130+
result = fn.call(throttled, ...args);
96131
} finally {
97-
lastExecution = Date.now();
132+
if (isPromiseLike(result)) {
133+
throttlingAsync = true;
134+
Promise.resolve(result).finally(done);
135+
} else {
136+
done();
137+
}
98138
flush = null;
99139
}
100140
};
101141
if (throttled.throttling) {
102142
if (ensureLast) {
103143
clearTimeout(timeout);
104-
timeout = setTimeout(() => flush?.(), timeframe);
144+
timeout = setTimeout(() => flush?.(), tf);
105145
}
106146
return;
107147
}
108148
flush?.();
109149
}) as ThrottledFunction<T>;
110150

111151
throttled.clear = () => {
152+
throttlingAsync = false;
112153
lastExecution = -Infinity;
113154
};
114155

@@ -117,9 +158,15 @@ export function throttle<T extends Array<any>>(
117158
};
118159

119160
Object.defineProperties(throttled, {
120-
throttling: { get: () => Date.now() - lastExecution <= timeframe },
161+
throttling: {
162+
get: () => Date.now() - lastExecution <= tf || throttlingAsync,
163+
},
121164
lastExecution: { get: () => lastExecution },
122165
});
123166

124167
return throttled;
125168
}
169+
170+
function isPromiseLike(obj: unknown): obj is PromiseLike<unknown> {
171+
return typeof (obj as PromiseLike<unknown>)?.then === "function";
172+
}

async/unstable_throttle_test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,29 @@ Deno.test("throttle() handles ensureLastCall option", async (t) => {
153153
});
154154
}
155155
});
156+
157+
Deno.test("throttle() handles dynamic timeframe", () => {
158+
using time = new FakeTime();
159+
let calls = 0;
160+
const fn = throttle(
161+
() => {
162+
time.tick(50 * ++calls);
163+
},
164+
(n) => n,
165+
);
166+
fn();
167+
assertEquals(calls, 1);
168+
assertEquals(fn.throttling, true);
169+
time.tick(49);
170+
assertEquals(fn.throttling, true);
171+
time.tick(2);
172+
assertEquals(fn.throttling, false);
173+
174+
fn();
175+
assertEquals(calls, 2);
176+
assertEquals(fn.throttling, true);
177+
time.tick(99);
178+
assertEquals(fn.throttling, true);
179+
time.tick(2);
180+
assertEquals(fn.throttling, false);
181+
});

0 commit comments

Comments
 (0)