208 lines
6.1 KiB
TypeScript
208 lines
6.1 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { RetryTimer } from '../../src/utils/retry-timer';
|
|
|
|
// @vitest-environment jsdom
|
|
describe('RetryTimer', () => {
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('should schedule a callback after the current backoff delay', () => {
|
|
vi.useFakeTimers();
|
|
vi.spyOn(Math, 'random').mockReturnValue(1);
|
|
const timer = new RetryTimer({ baseSeconds: 1, maxSeconds: 60 });
|
|
const cb = vi.fn();
|
|
|
|
timer.schedule(cb);
|
|
vi.advanceTimersByTime(999);
|
|
expect(cb).not.toHaveBeenCalled();
|
|
vi.advanceTimersByTime(1);
|
|
expect(cb).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should advance the counter on schedule by default', () => {
|
|
const timer = new RetryTimer({ baseSeconds: 1, maxSeconds: 60 });
|
|
timer.schedule(() => {});
|
|
expect(timer.getAttempts()).toBe(1);
|
|
timer.schedule(() => {});
|
|
expect(timer.getAttempts()).toBe(2);
|
|
});
|
|
|
|
it('should not advance the counter when schedule is called with advance: false', () => {
|
|
const timer = new RetryTimer({ baseSeconds: 1, maxSeconds: 60 });
|
|
timer.schedule(() => {}, { advance: false });
|
|
expect(timer.getAttempts()).toBe(0);
|
|
timer.schedule(() => {}, { advance: false });
|
|
expect(timer.getAttempts()).toBe(0);
|
|
});
|
|
|
|
it('should advance the counter via advance()', () => {
|
|
const timer = new RetryTimer({ baseSeconds: 1, maxSeconds: 60 });
|
|
timer.advance();
|
|
expect(timer.getAttempts()).toBe(1);
|
|
timer.advance();
|
|
expect(timer.getAttempts()).toBe(2);
|
|
});
|
|
|
|
it('should use a longer delay after advance()', () => {
|
|
vi.useFakeTimers();
|
|
vi.spyOn(Math, 'random').mockReturnValue(1);
|
|
const timer = new RetryTimer({ baseSeconds: 1, maxSeconds: 60 });
|
|
const cb = vi.fn();
|
|
|
|
timer.advance();
|
|
timer.schedule(cb);
|
|
|
|
// Counter is 1, delay should be base * 2^1 = 2 seconds.
|
|
vi.advanceTimersByTime(1999);
|
|
expect(cb).not.toHaveBeenCalled();
|
|
vi.advanceTimersByTime(1);
|
|
expect(cb).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should cancel a pending callback', () => {
|
|
vi.useFakeTimers();
|
|
vi.spyOn(Math, 'random').mockReturnValue(1);
|
|
const timer = new RetryTimer({ baseSeconds: 1, maxSeconds: 60 });
|
|
const cb = vi.fn();
|
|
|
|
timer.schedule(cb);
|
|
timer.cancel();
|
|
vi.advanceTimersByTime(10_000);
|
|
expect(cb).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reset both the timer and the counter', () => {
|
|
vi.useFakeTimers();
|
|
vi.spyOn(Math, 'random').mockReturnValue(1);
|
|
const timer = new RetryTimer({ baseSeconds: 1, maxSeconds: 60 });
|
|
const cb = vi.fn();
|
|
|
|
timer.advance();
|
|
timer.advance();
|
|
timer.schedule(cb, { advance: false });
|
|
expect(timer.getAttempts()).toBe(2);
|
|
expect(timer.isRunning()).toBe(true);
|
|
|
|
timer.reset();
|
|
expect(timer.getAttempts()).toBe(0);
|
|
expect(timer.isRunning()).toBe(false);
|
|
|
|
vi.advanceTimersByTime(10_000);
|
|
expect(cb).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should report running state', () => {
|
|
vi.useFakeTimers();
|
|
vi.spyOn(Math, 'random').mockReturnValue(1);
|
|
const timer = new RetryTimer({ baseSeconds: 1, maxSeconds: 60 });
|
|
|
|
expect(timer.isRunning()).toBe(false);
|
|
timer.schedule(() => {});
|
|
expect(timer.isRunning()).toBe(true);
|
|
vi.advanceTimersByTime(1000);
|
|
expect(timer.isRunning()).toBe(false);
|
|
});
|
|
|
|
it('should advance the counter when schedule is called with advance: true', () => {
|
|
vi.useFakeTimers();
|
|
vi.spyOn(Math, 'random').mockReturnValue(1);
|
|
const timer = new RetryTimer({ baseSeconds: 1, maxSeconds: 60 });
|
|
|
|
timer.schedule(() => {}, { advance: true });
|
|
expect(timer.getAttempts()).toBe(1);
|
|
timer.schedule(() => {}, { advance: true });
|
|
expect(timer.getAttempts()).toBe(2);
|
|
});
|
|
|
|
it('should produce a fixed delay when configured with base=max and jitter=1', () => {
|
|
// The "static delay" idiom: callers wanting a non-growing delay configure
|
|
// the backoff to flatten out. No special mode in the class.
|
|
vi.useFakeTimers();
|
|
const timer = new RetryTimer({
|
|
baseSeconds: 30,
|
|
maxSeconds: 30,
|
|
jitterMin: 1,
|
|
jitterMax: 1,
|
|
});
|
|
const cb = vi.fn();
|
|
|
|
timer.advance();
|
|
timer.advance();
|
|
timer.schedule(cb);
|
|
vi.advanceTimersByTime(29_999);
|
|
expect(cb).not.toHaveBeenCalled();
|
|
vi.advanceTimersByTime(1);
|
|
expect(cb).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should accept a plain number as shorthand for a fixed delay', () => {
|
|
vi.useFakeTimers();
|
|
const timer = new RetryTimer(30);
|
|
const cb = vi.fn();
|
|
|
|
timer.advance();
|
|
timer.schedule(cb);
|
|
vi.advanceTimersByTime(29_999);
|
|
expect(cb).not.toHaveBeenCalled();
|
|
vi.advanceTimersByTime(1);
|
|
expect(cb).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
describe('setOptions', () => {
|
|
it('should apply the new backoff config to the next schedule', () => {
|
|
vi.useFakeTimers();
|
|
vi.spyOn(Math, 'random').mockReturnValue(1);
|
|
const timer = new RetryTimer({
|
|
baseSeconds: 1,
|
|
maxSeconds: 60,
|
|
jitterMin: 1,
|
|
jitterMax: 1,
|
|
});
|
|
const cb = vi.fn();
|
|
|
|
timer.setOptions({
|
|
baseSeconds: 10,
|
|
maxSeconds: 10,
|
|
jitterMin: 1,
|
|
jitterMax: 1,
|
|
});
|
|
timer.schedule(cb);
|
|
vi.advanceTimersByTime(9_999);
|
|
expect(cb).not.toHaveBeenCalled();
|
|
vi.advanceTimersByTime(1);
|
|
expect(cb).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should preserve the attempt counter and any pending callback', () => {
|
|
vi.useFakeTimers();
|
|
vi.spyOn(Math, 'random').mockReturnValue(1);
|
|
const timer = new RetryTimer({
|
|
baseSeconds: 1,
|
|
maxSeconds: 60,
|
|
jitterMin: 1,
|
|
jitterMax: 1,
|
|
});
|
|
const cb = vi.fn();
|
|
|
|
timer.advance();
|
|
timer.advance();
|
|
timer.schedule(cb, { advance: false });
|
|
expect(timer.getAttempts()).toBe(2);
|
|
expect(timer.isRunning()).toBe(true);
|
|
|
|
// Idempotent setOptions doesn't touch counter or pending timer.
|
|
timer.setOptions({
|
|
baseSeconds: 1,
|
|
maxSeconds: 60,
|
|
jitterMin: 1,
|
|
jitterMax: 1,
|
|
});
|
|
expect(timer.getAttempts()).toBe(2);
|
|
expect(timer.isRunning()).toBe(true);
|
|
});
|
|
});
|
|
});
|