2.2 KB73 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import {nullthrows} from 'shared/utils';
9
10/**
11 * Incrementally increases throttling when an even starts happening too often.
12 * For example, initially there's no throttle
13 * After 10 events without a gap of 10s, there's a 10s throttle.
14 * After 30 events without a gap of 30s, there's a 30s throttle.
15 * After no events for 10s, the throttle is reset to 0.
16 *
17 * These thresholds are configurable.
18 * "Throttling" means dropping events after the first one (unlike debouncing).
19 */
20export function stagedThrottler<P extends Array<unknown>>(
21 stages: Array<{
22 throttleMs: number;
23 /** number of input events needed to advance to the next stage.
24 * Note: it doesn't matter if it was throttled or not. Every input adds to the advancement. */
25 numToNextStage?: number;
26 resetAfterMs: number;
27 /** Called when entering a stage.
28 * Note: 0th stage onEnter is not called "on startup", only if you reset the stage,
29 * and that this stage resets the next time a value IS emitted, not merely once the time passes.
30 */
31 onEnter?: () => unknown;
32 }>,
33 cb: (...args: P) => void,
34) {
35 // Time of the last non-throttled call
36 let lastEmitted = -Infinity;
37 let currentStage = 0;
38 let numSeen = 0;
39
40 return (...args: P) => {
41 const stage = nullthrows(stages[currentStage]);
42 const currentThrottle = stage.throttleMs;
43 const elapsed = Date.now() - lastEmitted;
44
45 // Input always counts towards going to the next stage
46 numSeen++;
47
48 // Maybe go to the next stage
49 if (numSeen > 1 && elapsed > stage.resetAfterMs) {
50 // Reset the throttle
51 numSeen = 0;
52 currentStage = 0;
53 stages[currentStage].onEnter?.();
54 } else if (stage.numToNextStage && numSeen >= stage.numToNextStage) {
55 const nextStage = currentStage + 1;
56 if (nextStage < stages.length) {
57 numSeen = 0;
58 currentStage++;
59 stages[currentStage].onEnter?.();
60 }
61 }
62
63 if (elapsed < currentThrottle) {
64 // Needs to be throttled
65 return;
66 }
67
68 // No need to throttle
69 lastEmitted = Date.now();
70 return cb(...args);
71 };
72}
73