addons/isl-server/src/StagedThrottler.tsblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import {nullthrows} from 'shared/utils';
b69ab319
b69ab3110/**
b69ab3111 * Incrementally increases throttling when an even starts happening too often.
b69ab3112 * For example, initially there's no throttle
b69ab3113 * After 10 events without a gap of 10s, there's a 10s throttle.
b69ab3114 * After 30 events without a gap of 30s, there's a 30s throttle.
b69ab3115 * After no events for 10s, the throttle is reset to 0.
b69ab3116 *
b69ab3117 * These thresholds are configurable.
b69ab3118 * "Throttling" means dropping events after the first one (unlike debouncing).
b69ab3119 */
b69ab3120export function stagedThrottler<P extends Array<unknown>>(
b69ab3121 stages: Array<{
b69ab3122 throttleMs: number;
b69ab3123 /** number of input events needed to advance to the next stage.
b69ab3124 * Note: it doesn't matter if it was throttled or not. Every input adds to the advancement. */
b69ab3125 numToNextStage?: number;
b69ab3126 resetAfterMs: number;
b69ab3127 /** Called when entering a stage.
b69ab3128 * Note: 0th stage onEnter is not called "on startup", only if you reset the stage,
b69ab3129 * and that this stage resets the next time a value IS emitted, not merely once the time passes.
b69ab3130 */
b69ab3131 onEnter?: () => unknown;
b69ab3132 }>,
b69ab3133 cb: (...args: P) => void,
b69ab3134) {
b69ab3135 // Time of the last non-throttled call
b69ab3136 let lastEmitted = -Infinity;
b69ab3137 let currentStage = 0;
b69ab3138 let numSeen = 0;
b69ab3139
b69ab3140 return (...args: P) => {
b69ab3141 const stage = nullthrows(stages[currentStage]);
b69ab3142 const currentThrottle = stage.throttleMs;
b69ab3143 const elapsed = Date.now() - lastEmitted;
b69ab3144
b69ab3145 // Input always counts towards going to the next stage
b69ab3146 numSeen++;
b69ab3147
b69ab3148 // Maybe go to the next stage
b69ab3149 if (numSeen > 1 && elapsed > stage.resetAfterMs) {
b69ab3150 // Reset the throttle
b69ab3151 numSeen = 0;
b69ab3152 currentStage = 0;
b69ab3153 stages[currentStage].onEnter?.();
b69ab3154 } else if (stage.numToNextStage && numSeen >= stage.numToNextStage) {
b69ab3155 const nextStage = currentStage + 1;
b69ab3156 if (nextStage < stages.length) {
b69ab3157 numSeen = 0;
b69ab3158 currentStage++;
b69ab3159 stages[currentStage].onEnter?.();
b69ab3160 }
b69ab3161 }
b69ab3162
b69ab3163 if (elapsed < currentThrottle) {
b69ab3164 // Needs to be throttled
b69ab3165 return;
b69ab3166 }
b69ab3167
b69ab3168 // No need to throttle
b69ab3169 lastEmitted = Date.now();
b69ab3170 return cb(...args);
b69ab3171 };
b69ab3172}