refactor(spf): drop the vestigial reschedule retry-on-error path

The RecurringRunner propagates a rejected run as the recurrence's failure
(Promise.all short-circuits before any retry verdict lands), so the
retry-on-error affordance in delayedReschedule and mediaPlaylistReloadDelay
was dead code. Remove it: delayedReschedule awaits the run directly (a
rejection now rejects the reschedule), and mediaPlaylistReloadDelay takes a
non-optional current track. Transient-fetch-failure recovery belongs at the
fetch layer, not in the cadence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Pillsbury
2026-06-25 09:59:25 -07:00
co-authored by Claude Opus 4.8
parent 2520373f2f
commit 5aa99674e9
4 changed files with 26 additions and 47 deletions
@@ -12,21 +12,17 @@ import type { Reschedule } from './task';
* RFC 8216 §6.3.4's "measured from the last time the client began loading"). If
* the run takes longer than the cadence, the next run starts immediately.
*
* A `null` cadence stops the recurrence. An errored run passes `current` as
* `undefined`, so the cadence function can choose to retry (return a delay) or
* stop (return `null`).
* A `null` cadence stops the recurrence. A rejected run rejects this reschedule,
* which the `RecurringRunner` propagates as the recurrence's failure — error
* recovery (e.g. retrying transient fetch failures) belongs below, at the fetch
* layer, not in the cadence.
*/
export function delayedReschedule<TValue>(
cadence: (current: TValue | undefined, previous: TValue | undefined) => number | null
cadence: (current: TValue, previous: TValue | undefined) => number | null
): Reschedule<TValue> {
return async (task) => {
const startedAt = Date.now();
let current: TValue | undefined;
try {
current = await task.run();
} catch {
current = undefined;
}
const current = await task.run();
// `task.previous` is the prior successful value (carried by the runner's
// clone); read-only for the cadence, hence the cast off `DeepReadonly`.
const ms = cadence(current, task.previous as TValue | undefined);
@@ -82,26 +82,18 @@ describe('delayedReschedule', () => {
await expect(reschedule(fakeTask(async () => 1))).resolves.toBe(false);
});
it('passes undefined to the cadence when the run errors (so it can retry)', async () => {
vi.useFakeTimers();
const cadence = vi.fn(() => 50); // retry on error
it('rejects (propagating the run failure) without consulting the cadence', async () => {
const cadence = vi.fn(() => 50);
const reschedule = delayedReschedule<number>(cadence);
let resolved: boolean | undefined;
const done = reschedule(
fakeTask(async () => {
throw new Error('boom');
})
).then((v) => {
resolved = v;
});
await vi.advanceTimersByTimeAsync(0);
expect(cadence).toHaveBeenCalledWith(undefined, undefined);
await vi.advanceTimersByTimeAsync(50);
await done;
expect(resolved).toBe(true);
await expect(
reschedule(
fakeTask(async () => {
throw new Error('boom');
})
)
).rejects.toThrow('boom');
expect(cadence).not.toHaveBeenCalled();
});
it('rejects when aborted during the wait', async () => {