Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions async/unstable_throttle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,42 +73,63 @@ export interface ThrottledFunction<T extends Array<unknown>> {
* @typeParam T The arguments of the provided function.
* @param fn The function to throttle.
* @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.
* @param options Additional options.
* @returns The throttled function.
*/
// deno-lint-ignore no-explicit-any
export function throttle<T extends Array<any>>(
fn: (this: ThrottledFunction<T>, ...args: T) => void,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type of the function should be updated to match the new async handling, something like void | PromiseLike<void>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that but actually returning a promise doesn't really make sense, as the wrapped function doesn't always run and so the return value should be treated as meaningless and discarded, i.e. should be void/undefined.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, the return value is discarded. So I guess the benefit of adding PromiseLike is to signal/document fn?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, the return value is discarded. So I guess the benefit of adding PromiseLike is to signal/document fn?

Sorry, I don't follow. PromiseLike is not part of the signature, but promise-likes do now have special handling to enable lastExecution to track their time-to-resolve rather than time-to-sync-return.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I don't follow. PromiseLike is not part of the signature, but promise-likes do now have special handling to enable lastExecution to track their time-to-resolve rather than time-to-sync-return.

It was more of a cosmetic thing. Totally fine to keep it like this ☺️

timeframe: number,
timeframe: number | ((previousDuration: number) => number),
options?: ThrottleOptions,
): ThrottledFunction<T> {
const ensureLast = Boolean(options?.ensureLastCall);
let timeout = -1;

let lastExecution = -Infinity;
let flush: (() => void) | null = null;
let throttlingAsync = false;

let tf = typeof timeframe === "function" ? 0 : timeframe;

const throttled = ((...args: T) => {
flush = () => {
const start = Date.now();
let result: unknown;
const done = () => {
throttlingAsync = false;
lastExecution = Date.now();
if (typeof timeframe === "function") {
tf = timeframe(lastExecution - start);
}
};
try {
clearTimeout(timeout);
fn.call(throttled, ...args);
result = fn.call(throttled, ...args);
} finally {
lastExecution = Date.now();
if (isPromiseLike(result)) {
throttlingAsync = true;
Promise.resolve(result).finally(done);
} else {
done();
}
flush = null;
}
};
if (throttled.throttling) {
if (ensureLast) {
clearTimeout(timeout);
timeout = setTimeout(() => flush?.(), timeframe);
timeout = setTimeout(() => flush?.(), tf);
}
return;
}
flush?.();
}) as ThrottledFunction<T>;

throttled.clear = () => {
throttlingAsync = false;
lastExecution = -Infinity;
};

Expand All @@ -117,9 +138,15 @@ export function throttle<T extends Array<any>>(
};

Object.defineProperties(throttled, {
throttling: { get: () => Date.now() - lastExecution <= timeframe },
throttling: {
get: () => Date.now() - lastExecution <= tf || throttlingAsync,
},
lastExecution: { get: () => lastExecution },
});

return throttled;
}

function isPromiseLike(obj: unknown): obj is PromiseLike<unknown> {
return typeof (obj as PromiseLike<unknown>)?.then === "function";
}
26 changes: 26 additions & 0 deletions async/unstable_throttle_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,29 @@ Deno.test("throttle() handles ensureLastCall option", async (t) => {
});
}
});

Deno.test("throttle() handles dynamic timeframe", () => {
using time = new FakeTime();
let calls = 0;
const fn = throttle(
() => {
time.tick(50 * ++calls);
},
(n) => n,
);
fn();
assertEquals(calls, 1);
assertEquals(fn.throttling, true);
time.tick(49);
assertEquals(fn.throttling, true);
time.tick(2);
assertEquals(fn.throttling, false);

fn();
assertEquals(calls, 2);
assertEquals(fn.throttling, true);
time.tick(99);
assertEquals(fn.throttling, true);
time.tick(2);
assertEquals(fn.throttling, false);
});
Loading