Files
advanced-camera-card/src/utils/retry-timer.ts
T

102 lines
3.4 KiB
TypeScript

import {
ExponentialBackoff,
type ExponentialBackoffOptions,
} from './exponential-backoff';
import { Timer } from './timer';
// Expand a plain `number` (fixed delay in seconds) into the equivalent
// `ExponentialBackoffOptions`: base = max so growth flattens, jitter pinned to
// 1.0 so the delay is exactly N every time.
const convertToBackoffOptions = (
options: ExponentialBackoffOptions | number,
): ExponentialBackoffOptions => {
if (typeof options === 'number') {
return {
baseSeconds: options,
maxSeconds: options,
jitterMin: 1,
jitterMax: 1,
};
}
return options;
};
/**
* Pairs an `ExponentialBackoff` with a `Timer` for retry scheduling. Each
* `schedule(...)` call fires after the current backoff delay; `advance()`
* bumps the counter for next time.
*
* Constructor and `setOptions` accept either `ExponentialBackoffOptions`
* (growth + jitter) or a plain `number` (fixed delay in seconds, no growth,
* no jitter). Internally a number expands to `{ baseSeconds: N, maxSeconds:
* N, jitterMin: 1, jitterMax: 1 }` so all methods behave uniformly -- callers
* never branch.
*
* Typical patterns:
* - "Failure happened, retry later, count this failure": `schedule(cb)`.
* - "Retry deferred for an unrelated reason; don't compound":
* `schedule(cb, { advance: false })` (re-arms at the current delay).
* - "Attempt happened, count it separately from scheduling": `advance()`.
* - "Operation succeeded": `reset()`.
* - "Caller going away": `cancel()`.
*/
export class RetryTimer {
private readonly _backoff: ExponentialBackoff;
private readonly _timer = new Timer();
constructor(options: ExponentialBackoffOptions | number) {
this._backoff = new ExponentialBackoff(convertToBackoffOptions(options));
}
/**
* Replace the backoff configuration. Cheap and idempotent: doesn't cancel
* pending callbacks or reset the attempt counter, so it's safe to call on
* every scheduling pass regardless of whether the options actually changed.
* Call `reset()` separately if zeroing the counter is desired (e.g. on a
* semantically distinct mode switch).
*/
public setOptions(options: ExponentialBackoffOptions | number): void {
this._backoff.setOptions(convertToBackoffOptions(options));
}
/**
* Schedule `callback` to fire after the current delay, then advance the
* attempt counter so the next schedule uses a longer delay. Pass
* `{ advance: false }` to re-arm at the current delay without counting it
* (e.g. a retry that may be gated and re-scheduled). Any pending callback
* is canceled before scheduling.
*/
public schedule(callback: () => void, options?: { advance?: boolean }): void {
this._timer.start(this._backoff.peek(), callback);
if (options?.advance !== false) {
this._backoff.next();
}
}
/**
* Bump the attempt counter without scheduling. For flows where the schedule
* call and the "attempt happened, count it" event are separate (e.g. the
* scheduled callback may or may not actually retry, depending on a gate).
*/
public advance(): void {
this._backoff.next();
}
public cancel(): void {
this._timer.stop();
}
public reset(): void {
this._timer.stop();
this._backoff.reset();
}
public isRunning(): boolean {
return this._timer.isRunning();
}
public getAttempts(): number {
return this._backoff.getAttempts();
}
}