mirror of
https://github.com/denoland/std.git
synced 2024-11-21 20:50:22 +00:00
feat(async/unstable): add throttle()
function (#6110)
This commit is contained in:
parent
d6b5612c9f
commit
0f4649d4b7
@ -11,6 +11,7 @@
|
|||||||
"./unstable-mux-async-iterator": "./unstable_mux_async_iterator.ts",
|
"./unstable-mux-async-iterator": "./unstable_mux_async_iterator.ts",
|
||||||
"./pool": "./pool.ts",
|
"./pool": "./pool.ts",
|
||||||
"./retry": "./retry.ts",
|
"./retry": "./retry.ts",
|
||||||
"./tee": "./tee.ts"
|
"./tee": "./tee.ts",
|
||||||
|
"./unstable-throttle": "./unstable_throttle.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
102
async/unstable_throttle.ts
Normal file
102
async/unstable_throttle.ts
Normal file
@ -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;
|
||||||
|
}
|
93
async/unstable_throttle_test.ts
Normal file
93
async/unstable_throttle_test.ts
Normal file
@ -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);
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user