mirror of
https://github.com/denoland/std.git
synced 2024-11-21 12:40:03 +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",
|
||||
"./pool": "./pool.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