mirror of
https://github.com/zoriya/v10.git
synced 2026-08-13 09:30:38 +00:00
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:
co-authored by
Claude Opus 4.8
parent
2520373f2f
commit
5aa99674e9
@@ -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 () => {
|
||||
|
||||
@@ -8,34 +8,30 @@ function snapshotSignature(track: ResolvedTrack): string {
|
||||
return `${getMediaPlaylistMetadata(track)?.mediaSequence ?? 0}:${track.segments.length}`;
|
||||
}
|
||||
|
||||
function targetDurationOf(track: ResolvedTrack | undefined): number {
|
||||
return (track && getMediaPlaylistMetadata(track)?.targetDuration) || FALLBACK_TARGET_DURATION;
|
||||
function targetDurationOf(track: ResolvedTrack): number {
|
||||
return getMediaPlaylistMetadata(track)?.targetDuration || FALLBACK_TARGET_DURATION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Live media-playlist reload cadence, per RFC 8216bis §6.3.4 — a
|
||||
* {@link RecurrencePolicy} for a `RecurringRunner` re-resolving the selected
|
||||
* track. Structurally matches `RecurrencePolicy<ResolvedTrack>` without
|
||||
* importing it (media stays core-free): `current` is the freshly resolved track
|
||||
* (`undefined` if the reload errored), `previous` the prior resolved snapshot.
|
||||
* importing it (media stays core-free): `current` is the freshly resolved track,
|
||||
* `previous` the prior resolved snapshot.
|
||||
*
|
||||
* - Complete playlist (VoD, or live that hit `#EXT-X-ENDLIST`) → `null`: stop.
|
||||
* Keys off `Track.duration` (finite once complete), the single completeness
|
||||
* source of truth.
|
||||
* - Errored reload (`current` undefined) → retry at the last-known target-
|
||||
* duration cadence (fallback when unknown), keeping the loop alive across
|
||||
* transient fetch failures.
|
||||
* - Unchanged window (same media sequence + segment count as `previous`) → poll
|
||||
* at half the target duration; a moved/grown window (or the first reload) →
|
||||
* full target duration.
|
||||
*
|
||||
* A failed reload doesn't reach here — the rejection propagates through the
|
||||
* `RecurringRunner`; transient-failure recovery belongs at the fetch layer.
|
||||
*
|
||||
* Returned delays are milliseconds.
|
||||
*/
|
||||
export function mediaPlaylistReloadDelay(
|
||||
current: ResolvedTrack | undefined,
|
||||
previous: ResolvedTrack | undefined
|
||||
): number | null {
|
||||
if (!current) return targetDurationOf(previous) * 1000;
|
||||
export function mediaPlaylistReloadDelay(current: ResolvedTrack, previous: ResolvedTrack | undefined): number | null {
|
||||
if (Number.isFinite(current.duration)) return null;
|
||||
|
||||
const target = targetDurationOf(current);
|
||||
|
||||
@@ -43,12 +43,7 @@ describe('mediaPlaylistReloadDelay', () => {
|
||||
expect(mediaPlaylistReloadDelay(slid, prev)).toBe(4000);
|
||||
});
|
||||
|
||||
it('retries at the last-known cadence when a reload errors (current undefined)', () => {
|
||||
const prev = track({ targetDuration: 8 });
|
||||
expect(mediaPlaylistReloadDelay(undefined, prev)).toBe(8000);
|
||||
});
|
||||
|
||||
it('falls back to 6s when no target duration / prior snapshot is available', () => {
|
||||
expect(mediaPlaylistReloadDelay(undefined, undefined)).toBe(6000);
|
||||
it('falls back to 6s when the playlist carries no usable target duration', () => {
|
||||
expect(mediaPlaylistReloadDelay(track({ targetDuration: 0 }), undefined)).toBe(6000);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user