Created
March 5, 2021 09:26
-
-
Save xsahil03x/0b3d34af4e9f71fa89c1ee389b894195 to your computer and use it in GitHub Desktop.
Rate Limiter
This file contains hidden or 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
import 'dart:async'; | |
import 'dart:math' as math; | |
/// Creates a debounced function that delays invoking `func` until after `wait` | |
/// milliseconds have elapsed since the last time the debounced function was | |
/// invoked, or until the next browser frame is drawn. The debounced function | |
/// comes with a `cancel` method to cancel delayed `func` invocations and a | |
/// `flush` method to immediately invoke them. Provide `options` to indicate | |
/// whether `func` should be invoked on the leading and/or trailing edge of the | |
/// `wait` timeout. The `func` is invoked with the last arguments provided to the | |
/// debounced function. Subsequent calls to the debounced function return the | |
/// result of the last `func` invocation. | |
/// | |
/// **Note:** If `leading` and `trailing` options are `true`, `func` is | |
/// invoked on the trailing edge of the timeout only if the debounced function | |
/// is invoked more than once during the `wait` timeout. | |
/// | |
/// If `wait` is `0` and `leading` is `false`, `func` invocation is deferred | |
/// until the next tick, similar to `setTimeout` with a timeout of `0`. | |
/// | |
/// If `wait` is omitted in an environment with `requestAnimationFrame`, `func` | |
/// invocation will be deferred until the next frame is drawn (typically about | |
/// 16ms). | |
/// | |
/// See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) | |
/// for details over the differences between `debounce` and `throttle`. | |
/// | |
/// @since 0.1.0 | |
/// @category Function | |
/// @param {Function} func The function to debounce. | |
/// @param {number} [wait=0] | |
/// The number of milliseconds to delay; if omitted, `requestAnimationFrame` is | |
/// used (if available). | |
/// @param {Object} [options={}] The options object. | |
/// @param {boolean} [options.leading=false] | |
/// Specify invoking on the leading edge of the timeout. | |
/// @param {number} [options.maxWait] | |
/// The maximum time `func` is allowed to be delayed before it's invoked. | |
/// @param {boolean} [options.trailing=true] | |
/// Specify invoking on the trailing edge of the timeout. | |
/// @returns {Function} Returns the new debounced function. | |
/// @example | |
/// | |
/// // Avoid costly calculations while the window size is in flux. | |
/// jQuery(window).on('resize', debounce(calculateLayout, 150)) | |
/// | |
/// // Invoke `sendMail` when clicked, debouncing subsequent calls. | |
/// jQuery(element).on('click', debounce(sendMail, 300, { | |
/// 'leading': true, | |
/// 'trailing': false | |
/// })) | |
/// | |
/// // Ensure `batchLog` is invoked once after 1 second of debounced calls. | |
/// const debounced = debounce(batchLog, 250, { 'maxWait': 1000 }) | |
/// const source = new EventSource('/stream') | |
/// jQuery(source).on('message', debounced) | |
/// | |
/// // Cancel the trailing debounced invocation. | |
/// jQuery(window).on('popstate', debounced.cancel) | |
/// | |
/// // Check for pending invocations. | |
/// const status = debounced.pending() ? "Pending..." : "Ready" | |
class Debounce { | |
final Function func; | |
final Duration timeout; | |
final bool leading; | |
final bool trailing; | |
final Duration maxWaitTimeout; | |
final int wait; | |
final bool maxing; | |
int maxWait; | |
List<dynamic> lastArgs; | |
Function lastThis; | |
Timer timer; | |
int lastCallTime; | |
dynamic result; | |
int lastInvokeTime = 0; | |
/// | |
Debounce( | |
this.func, | |
this.timeout, { | |
this.leading = false, | |
this.trailing = true, | |
this.maxWaitTimeout, | |
}) : wait = timeout?.inMilliseconds ?? 0, | |
maxing = maxWaitTimeout != null { | |
if (maxing) { | |
maxWait = math.max(maxWaitTimeout.inMilliseconds, wait); | |
} | |
} | |
dynamic _invokeFunc(int time) { | |
final args = lastArgs; | |
final thisArg = lastThis; | |
lastArgs = lastThis = null; | |
lastInvokeTime = time; | |
return result = Function.apply(func, args); | |
} | |
Timer _startTimer(Function pendingFunc, int wait) => | |
Timer(Duration(milliseconds: wait), pendingFunc); | |
bool _shouldInvoke(int time) { | |
final timeSinceLastCall = time - (lastCallTime ?? time); | |
final timeSinceLastInvoke = time - lastInvokeTime; | |
// Either this is the first call, activity has stopped and we're at the | |
// trailing edge, the system time has gone backwards and we're treating | |
// it as the trailing edge, or we've hit the `maxWait` limit. | |
return lastCallTime == null || | |
(timeSinceLastCall >= wait) || | |
(timeSinceLastCall < 0) || | |
(maxing && timeSinceLastInvoke >= maxWait); | |
} | |
dynamic _trailingEdge(int time) { | |
timer = null; | |
// Only invoke if we have `lastArgs` which means `func` has been | |
// debounced at least once. | |
if (trailing && lastArgs != null) { | |
return _invokeFunc(time); | |
} | |
lastArgs = lastThis = null; | |
return result; | |
} | |
int _remainingWait(int time) { | |
final timeSinceLastCall = time - lastCallTime; | |
final timeSinceLastInvoke = time - lastInvokeTime; | |
final timeWaiting = wait - timeSinceLastCall; | |
return maxing | |
? math.min(timeWaiting, maxWait - timeSinceLastInvoke) | |
: timeWaiting; | |
} | |
void _timerExpired() { | |
final time = DateTime.now().millisecondsSinceEpoch; | |
if (_shouldInvoke(time)) { | |
return _trailingEdge(time); | |
} | |
// Restart the timer. | |
timer = _startTimer(_timerExpired, _remainingWait(time)); | |
} | |
dynamic _leadingEdge(int time) { | |
// Reset any `maxWait` timer. | |
lastInvokeTime = time; | |
// Start the timer for the trailing edge. | |
timer = _startTimer(_timerExpired, wait); | |
// Invoke the leading edge. | |
return leading ? _invokeFunc(time) : result; | |
} | |
/// | |
void cancel() { | |
timer?.cancel(); | |
lastInvokeTime = 0; | |
lastArgs = lastCallTime = lastThis = timer = null; | |
} | |
/// | |
dynamic flush() { | |
final now = DateTime.now().millisecondsSinceEpoch; | |
return timer == null ? result : _trailingEdge(now); | |
} | |
/// | |
bool get hasPending => timer != null; | |
/// | |
dynamic call(List<dynamic> args) { | |
final time = DateTime.now().millisecondsSinceEpoch; | |
final isInvoking = _shouldInvoke(time); | |
lastArgs = args; | |
lastThis = this; | |
lastCallTime = time; | |
if (isInvoking) { | |
if (timer == null) { | |
return _leadingEdge(lastCallTime); | |
} | |
if (maxing) { | |
// Handle invocations in a tight loop. | |
timer = _startTimer(_timerExpired, wait); | |
return _invokeFunc(lastCallTime); | |
} | |
} | |
timer ??= _startTimer(_timerExpired, wait); | |
return result; | |
} | |
} | |
/// Creates a throttled function that only invokes `func` at most once per | |
/// every `wait` milliseconds (or once per browser frame). The throttled function | |
/// comes with a `cancel` method to cancel delayed `func` invocations and a | |
/// `flush` method to immediately invoke them. Provide `options` to indicate | |
/// whether `func` should be invoked on the leading and/or trailing edge of the | |
/// `wait` timeout. The `func` is invoked with the last arguments provided to the | |
/// throttled function. Subsequent calls to the throttled function return the | |
/// result of the last `func` invocation. | |
/// | |
/// **Note:** If `leading` and `trailing` options are `true`, `func` is | |
/// invoked on the trailing edge of the timeout only if the throttled function | |
/// is invoked more than once during the `wait` timeout. | |
/// | |
/// If `wait` is `0` and `leading` is `false`, `func` invocation is deferred | |
/// until the next tick, similar to `setTimeout` with a timeout of `0`. | |
/// | |
/// If `wait` is omitted in an environment with `requestAnimationFrame`, `func` | |
/// invocation will be deferred until the next frame is drawn (typically about | |
/// 16ms). | |
/// | |
/// See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) | |
/// for details over the differences between `throttle` and `debounce`. | |
/// | |
/// @since 0.1.0 | |
/// @category Function | |
/// @param {Function} func The function to throttle. | |
/// @param {number} [wait=0] | |
/// The number of milliseconds to throttle invocations to; if omitted, | |
/// `requestAnimationFrame` is used (if available). | |
/// @param {Object} [options={}] The options object. | |
/// @param {boolean} [options.leading=true] | |
/// Specify invoking on the leading edge of the timeout. | |
/// @param {boolean} [options.trailing=true] | |
/// Specify invoking on the trailing edge of the timeout. | |
/// @returns {Function} Returns the new throttled function. | |
/// @example | |
/// | |
/// // Avoid excessively updating the position while scrolling. | |
/// jQuery(window).on('scroll', throttle(updatePosition, 100)) | |
/// | |
/// // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes. | |
/// const throttled = throttle(renewToken, 300000, { 'trailing': false }) | |
/// jQuery(element).on('click', throttled) | |
/// | |
/// // Cancel the trailing throttled invocation. | |
/// jQuery(window).on('popstate', throttled.cancel) | |
/// | |
class Throttle extends Debounce { | |
/// | |
Throttle( | |
Function func, | |
Duration timeout, { | |
bool leading = true, | |
bool trailing = true, | |
}) : super( | |
func, | |
timeout, | |
leading: leading, | |
trailing: trailing, | |
maxWaitTimeout: timeout, | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment