Skip to content

Commit ef76344

Browse files
committed
feat(async/unstable): allow setting dynamic timeframe for throttle
1 parent 91d1d55 commit ef76344

File tree

1 file changed

+61
-15
lines changed

1 file changed

+61
-15
lines changed

async/unstable_throttle.ts

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ export type ThrottleOptions = {
1515
* A throttled function that will be executed at most once during the
1616
* specified `timeframe` in milliseconds.
1717
*/
18-
export interface ThrottledFunction<T extends Array<unknown>> {
19-
(...args: T): void;
18+
export interface ThrottledFunction<T extends Array<unknown>, R = void> {
19+
(...args: T): R extends Promise<unknown> ? Promise<void> : void;
2020
/**
2121
* Clears the throttling state.
2222
* {@linkcode ThrottledFunction.lastExecution} will be reset to `-Infinity` and
@@ -26,7 +26,7 @@ export interface ThrottledFunction<T extends Array<unknown>> {
2626
/**
2727
* Execute the last throttled call (if any) and clears the throttling state.
2828
*/
29-
flush(): void;
29+
flush(): R extends Promise<unknown> ? Promise<void> : void;
3030
/**
3131
* Returns a boolean indicating whether the function is currently being throttled.
3232
*/
@@ -70,56 +70,102 @@ export interface ThrottledFunction<T extends Array<unknown>> {
7070
* assert(func.lastExecution > 0);
7171
* ```
7272
*
73+
* @example Using a dynamic timeframe
74+
* ```ts
75+
* import { throttle } from "@std/async/unstable-throttle";
76+
* import { delay } from "./delay.ts";
77+
* import { assertEquals } from "@std/assert";
78+
*
79+
* let timesCalled = 0;
80+
* const fn = throttle(async (ms: number) => {
81+
* await delay(ms);
82+
* ++timesCalled;
83+
* }, (previousExecution) => previousExecution * 2);
84+
* await fn(50); // takes ~50ms to execute, after which it will be throttled for 50ms * 2 = 100ms
85+
* assertEquals(timesCalled, 1);
86+
* await delay(50);
87+
* await fn(50);
88+
* assertEquals(timesCalled, 1); // still throttled
89+
* await delay(30);
90+
* await fn(50);
91+
* assertEquals(timesCalled, 1); // still throttled
92+
* await delay(30);
93+
* await fn(50);
94+
* assertEquals(timesCalled, 2); // not throttled this time
95+
* ```
96+
*
7397
* @typeParam T The arguments of the provided function.
7498
* @param fn The function to throttle.
75-
* @param timeframe The timeframe in milliseconds in which the function should be called at most once.
99+
* @param timeframe The timeframe in milliseconds in which the function should be called at most once. If a callback function is supplied, it will be called with the duration of the previous execution and should return the next timeframe to use in milliseconds.
76100
* @param options Additional options.
77101
* @returns The throttled function.
78102
*/
79103
// deno-lint-ignore no-explicit-any
80-
export function throttle<T extends Array<any>>(
81-
fn: (this: ThrottledFunction<T>, ...args: T) => void,
82-
timeframe: number,
104+
export function throttle<T extends Array<any>, R = void>(
105+
fn: (this: ThrottledFunction<T, R>, ...args: T) => R,
106+
timeframe: number | ((previousExecution: number) => number),
83107
options?: ThrottleOptions,
84-
): ThrottledFunction<T> {
108+
): ThrottledFunction<T, R> {
85109
const ensureLast = Boolean(options?.ensureLastCall);
86110
let timeout = -1;
87111

88112
let lastExecution = -Infinity;
89-
let flush: (() => void) | null = null;
113+
let flush: (() => void | Promise<void>) | null = null;
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;
93121
try {
94122
clearTimeout(timeout);
95-
fn.call(throttled, ...args);
123+
result = fn.call(throttled, ...args);
96124
} finally {
97125
lastExecution = Date.now();
126+
if (isPromiseLike(result)) {
127+
result = result.finally(() => {
128+
lastExecution = Date.now();
129+
if (typeof timeframe === "function") {
130+
tf = timeframe(lastExecution - start);
131+
}
132+
});
133+
} else {
134+
// lastExecution = Date.now();
135+
if (typeof timeframe === "function") {
136+
tf = timeframe(lastExecution - start);
137+
}
138+
}
98139
flush = null;
99140
}
141+
if (isPromiseLike(result)) return result.then(() => {});
100142
};
101143
if (throttled.throttling) {
102144
if (ensureLast) {
103145
clearTimeout(timeout);
104-
timeout = setTimeout(() => flush?.(), timeframe);
146+
timeout = setTimeout(() => flush?.(), tf);
105147
}
106148
return;
107149
}
108-
flush?.();
109-
}) as ThrottledFunction<T>;
150+
return flush?.();
151+
}) as ThrottledFunction<T, R>;
110152

111153
throttled.clear = () => {
112154
lastExecution = -Infinity;
113155
};
114156

115157
throttled.flush = () => {
116-
flush?.();
158+
return flush?.() as ReturnType<ThrottledFunction<T, R>["flush"]>;
117159
};
118160

119161
Object.defineProperties(throttled, {
120-
throttling: { get: () => Date.now() - lastExecution <= timeframe },
162+
throttling: { get: () => Date.now() - lastExecution <= tf },
121163
lastExecution: { get: () => lastExecution },
122164
});
123165

124166
return throttled;
125167
}
168+
169+
function isPromiseLike(obj: unknown): obj is Promise<unknown> {
170+
return typeof (obj as Promise<unknown>)?.then === "function";
171+
}

0 commit comments

Comments
 (0)