Signal-Desktop/ts/util/timeout.ts

90 lines
2.2 KiB
TypeScript

// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { createLogger } from '../logging/log';
const logging = createLogger('timeout');
const MAX_SAFE_TIMEOUT_DELAY = 2147483647; // max 32-bit signed integer
// Prefer using this function over setTimeout in any circumstances where
// the delay is not hardcoded to < MAX_SAFE_TIMEOUT_DELAY. Sets and returns a
// timeout if the delay is safe, otherwise does not set a timeout.
export function safeSetTimeout(
callback: VoidFunction,
providedDelayMs: number,
options?: {
clampToMax: boolean;
}
): NodeJS.Timeout | null {
let delayMs = providedDelayMs;
if (delayMs < 0) {
logging.warn('safeSetTimeout: timeout is less than zero');
delayMs = 0;
}
if (delayMs > MAX_SAFE_TIMEOUT_DELAY) {
if (options?.clampToMax) {
delayMs = MAX_SAFE_TIMEOUT_DELAY;
} else {
logging.warn(
'safeSetTimeout: timeout is larger than maximum setTimeout delay'
);
return null;
}
}
return setTimeout(callback, delayMs);
}
// Set timeout for a delay that might be longer than MAX_SAFE_TIMEOUT_DELAY. The
// callback is guaranteed to execute after desired delay.
export class LongTimeout {
#callback: VoidFunction;
#fireTime: number;
#timer: NodeJS.Timeout | undefined;
constructor(callback: VoidFunction, providedDelayMs: number) {
let delayMs = providedDelayMs;
if (delayMs < 0) {
logging.warn('safeSetTimeout: timeout is less than zero');
delayMs = 0;
}
if (Number.isNaN(delayMs)) {
throw new Error('NaN delayMs');
}
if (!Number.isFinite(delayMs)) {
throw new Error('Infinite delayMs');
}
this.#callback = callback;
this.#fireTime = Date.now() + delayMs;
this.#schedule();
}
clear(): void {
if (this.#timer != null) {
clearTimeout(this.#timer);
}
this.#timer = undefined;
}
#schedule(): void {
const remainingMs = this.#fireTime - Date.now();
if (remainingMs <= MAX_SAFE_TIMEOUT_DELAY) {
this.#timer = setTimeout(() => this.#fire(), remainingMs);
return;
}
this.#timer = setTimeout(() => {
this.#schedule();
}, MAX_SAFE_TIMEOUT_DELAY);
}
#fire(): void {
this.clear();
this.#callback();
}
}