forked from denoland/std
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(async/unstable): add
throttle()
function (denoland#6110)
- Loading branch information
1 parent
d6b5612
commit 0f4649d
Showing
3 changed files
with
197 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. | ||
// This module is browser compatible. | ||
|
||
/** | ||
* A throttled function that will be executed at most once during the | ||
* specified `timeframe` in milliseconds. | ||
*/ | ||
export interface ThrottledFunction<T extends Array<unknown>> { | ||
(...args: T): void; | ||
/** | ||
* Clears the throttling state. | ||
* {@linkcode ThrottledFunction.lastExecution} will be reset to `NaN` and | ||
* {@linkcode ThrottledFunction.throttling} will be reset to `false`. | ||
*/ | ||
clear(): void; | ||
/** | ||
* Execute the last throttled call (if any) and clears the throttling state. | ||
*/ | ||
flush(): void; | ||
/** | ||
* Returns a boolean indicating whether the function is currently being throttled. | ||
*/ | ||
readonly throttling: boolean; | ||
/** | ||
* Returns the timestamp of the last execution of the throttled function. | ||
* It is set to `NaN` if it has not been called yet. | ||
*/ | ||
readonly lastExecution: number; | ||
} | ||
|
||
/** | ||
* Creates a throttled function that prevents the given `func` | ||
* from being called more than once within a given `timeframe` in milliseconds. | ||
* | ||
* @experimental **UNSTABLE**: New API, yet to be vetted. | ||
* | ||
* @example Usage | ||
* ```ts | ||
* import { throttle } from "./unstable_throttle.ts" | ||
* import { retry } from "@std/async/retry" | ||
* import { assert } from "@std/assert" | ||
* | ||
* let called = 0; | ||
* await using server = Deno.serve({ port: 0, onListen:() => null }, () => new Response(`${called++}`)); | ||
* | ||
* // A throttled function will be executed at most once during a specified ms timeframe | ||
* const timeframe = 100 | ||
* const func = throttle<[string]>((url) => fetch(url).then(r => r.body?.cancel()), timeframe); | ||
* for (let i = 0; i < 10; i++) { | ||
* func(`http://localhost:${server.addr.port}/api`); | ||
* } | ||
* | ||
* await retry(() => assert(!func.throttling)) | ||
* assert(called === 1) | ||
* assert(!Number.isNaN(func.lastExecution)) | ||
* ``` | ||
* | ||
* @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. | ||
* @returns The throttled function. | ||
*/ | ||
// deno-lint-ignore no-explicit-any | ||
export function throttle<T extends Array<any>>( | ||
fn: (this: ThrottledFunction<T>, ...args: T) => void, | ||
timeframe: number, | ||
): ThrottledFunction<T> { | ||
let lastExecution = NaN; | ||
let flush: (() => void) | null = null; | ||
|
||
const throttled = ((...args: T) => { | ||
flush = () => { | ||
try { | ||
fn.call(throttled, ...args); | ||
} finally { | ||
lastExecution = Date.now(); | ||
flush = null; | ||
} | ||
}; | ||
if (throttled.throttling) { | ||
return; | ||
} | ||
flush?.(); | ||
}) as ThrottledFunction<T>; | ||
|
||
throttled.clear = () => { | ||
lastExecution = NaN; | ||
}; | ||
|
||
throttled.flush = () => { | ||
lastExecution = NaN; | ||
flush?.(); | ||
throttled.clear(); | ||
}; | ||
|
||
Object.defineProperties(throttled, { | ||
throttling: { get: () => Date.now() - lastExecution <= timeframe }, | ||
lastExecution: { get: () => lastExecution }, | ||
}); | ||
|
||
return throttled; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. | ||
import { | ||
assertEquals, | ||
assertGreater, | ||
assertLess, | ||
assertNotEquals, | ||
assertStrictEquals, | ||
} from "@std/assert"; | ||
import { throttle, type ThrottledFunction } from "./unstable_throttle.ts"; | ||
import { delay } from "./delay.ts"; | ||
|
||
Deno.test("throttle() handles called", async () => { | ||
let called = 0; | ||
const t = throttle(() => called++, 100); | ||
assertEquals(t.throttling, false); | ||
assertEquals(t.lastExecution, NaN); | ||
t(); | ||
const { lastExecution } = t; | ||
t(); | ||
t(); | ||
assertLess(Math.abs(t.lastExecution - Date.now()), 100); | ||
assertEquals(called, 1); | ||
assertEquals(t.throttling, true); | ||
assertEquals(t.lastExecution, lastExecution); | ||
await delay(200); | ||
assertEquals(called, 1); | ||
assertEquals(t.throttling, false); | ||
assertEquals(t.lastExecution, lastExecution); | ||
t(); | ||
assertEquals(called, 2); | ||
assertEquals(t.throttling, true); | ||
assertGreater(t.lastExecution, lastExecution); | ||
}); | ||
|
||
Deno.test("throttle() handles cancelled", () => { | ||
let called = 0; | ||
const t = throttle(() => called++, 100); | ||
t(); | ||
t(); | ||
t(); | ||
assertEquals(called, 1); | ||
assertEquals(t.throttling, true); | ||
assertNotEquals(t.lastExecution, NaN); | ||
t.clear(); | ||
assertEquals(called, 1); | ||
assertEquals(t.throttling, false); | ||
assertEquals(t.lastExecution, NaN); | ||
}); | ||
|
||
Deno.test("debounce() handles flush", () => { | ||
let called = 0; | ||
let arg = ""; | ||
const t = throttle((_arg) => { | ||
arg = _arg; | ||
called++; | ||
}, 100); | ||
t("foo"); | ||
t("bar"); | ||
t("baz"); | ||
assertEquals(called, 1); | ||
assertEquals(arg, "foo"); | ||
assertEquals(t.throttling, true); | ||
assertNotEquals(t.lastExecution, NaN); | ||
for (const _ of [1, 2]) { | ||
t.flush(); | ||
assertEquals(called, 2); | ||
assertEquals(arg, "baz"); | ||
assertEquals(t.throttling, false); | ||
assertEquals(t.lastExecution, NaN); | ||
} | ||
}); | ||
|
||
Deno.test("throttle() handles params and context", async () => { | ||
const params: Array<string | number> = []; | ||
const t: ThrottledFunction<[string, number]> = throttle( | ||
function (param1: string, param2: number) { | ||
params.push(param1); | ||
params.push(param2); | ||
assertStrictEquals(t, this); | ||
}, | ||
100, | ||
); | ||
t("foo", 1); | ||
t("bar", 1); | ||
t("baz", 1); | ||
// @ts-expect-error Argument of type 'number' is not assignable to parameter of type 'string'. | ||
t(1, 1); | ||
assertEquals(params, ["foo", 1]); | ||
assertEquals(t.throttling, true); | ||
await delay(200); | ||
assertEquals(params, ["foo", 1]); | ||
assertEquals(t.throttling, false); | ||
}); |