@@ -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