mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +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
|
* 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.
|
* the run takes longer than the cadence, the next run starts immediately.
|
||||||
*
|
*
|
||||||
* A `null` cadence stops the recurrence. An errored run passes `current` as
|
* A `null` cadence stops the recurrence. A rejected run rejects this reschedule,
|
||||||
* `undefined`, so the cadence function can choose to retry (return a delay) or
|
* which the `RecurringRunner` propagates as the recurrence's failure — error
|
||||||
* stop (return `null`).
|
* recovery (e.g. retrying transient fetch failures) belongs below, at the fetch
|
||||||
|
* layer, not in the cadence.
|
||||||
*/
|
*/
|
||||||
export function delayedReschedule<TValue>(
|
export function delayedReschedule<TValue>(
|
||||||
cadence: (current: TValue | undefined, previous: TValue | undefined) => number | null
|
cadence: (current: TValue, previous: TValue | undefined) => number | null
|
||||||
): Reschedule<TValue> {
|
): Reschedule<TValue> {
|
||||||
return async (task) => {
|
return async (task) => {
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
let current: TValue | undefined;
|
const current = await task.run();
|
||||||
try {
|
|
||||||
current = await task.run();
|
|
||||||
} catch {
|
|
||||||
current = undefined;
|
|
||||||
}
|
|
||||||
// `task.previous` is the prior successful value (carried by the runner's
|
// `task.previous` is the prior successful value (carried by the runner's
|
||||||
// clone); read-only for the cadence, hence the cast off `DeepReadonly`.
|
// clone); read-only for the cadence, hence the cast off `DeepReadonly`.
|
||||||
const ms = cadence(current, task.previous as TValue | undefined);
|
const ms = cadence(current, task.previous as TValue | undefined);
|
||||||
|
|||||||
@@ -82,26 +82,18 @@ describe('delayedReschedule', () => {
|
|||||||
await expect(reschedule(fakeTask(async () => 1))).resolves.toBe(false);
|
await expect(reschedule(fakeTask(async () => 1))).resolves.toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes undefined to the cadence when the run errors (so it can retry)', async () => {
|
it('rejects (propagating the run failure) without consulting the cadence', async () => {
|
||||||
vi.useFakeTimers();
|
const cadence = vi.fn(() => 50);
|
||||||
const cadence = vi.fn(() => 50); // retry on error
|
|
||||||
const reschedule = delayedReschedule<number>(cadence);
|
const reschedule = delayedReschedule<number>(cadence);
|
||||||
|
|
||||||
let resolved: boolean | undefined;
|
await expect(
|
||||||
const done = reschedule(
|
reschedule(
|
||||||
fakeTask(async () => {
|
fakeTask(async () => {
|
||||||
throw new Error('boom');
|
throw new Error('boom');
|
||||||
})
|
})
|
||||||
).then((v) => {
|
)
|
||||||
resolved = v;
|
).rejects.toThrow('boom');
|
||||||
});
|
expect(cadence).not.toHaveBeenCalled();
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(0);
|
|
||||||
expect(cadence).toHaveBeenCalledWith(undefined, undefined);
|
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(50);
|
|
||||||
await done;
|
|
||||||
expect(resolved).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects when aborted during the wait', async () => {
|
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}`;
|
return `${getMediaPlaylistMetadata(track)?.mediaSequence ?? 0}:${track.segments.length}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function targetDurationOf(track: ResolvedTrack | undefined): number {
|
function targetDurationOf(track: ResolvedTrack): number {
|
||||||
return (track && getMediaPlaylistMetadata(track)?.targetDuration) || FALLBACK_TARGET_DURATION;
|
return getMediaPlaylistMetadata(track)?.targetDuration || FALLBACK_TARGET_DURATION;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Live media-playlist reload cadence, per RFC 8216bis §6.3.4 — a
|
* Live media-playlist reload cadence, per RFC 8216bis §6.3.4 — a
|
||||||
* {@link RecurrencePolicy} for a `RecurringRunner` re-resolving the selected
|
* {@link RecurrencePolicy} for a `RecurringRunner` re-resolving the selected
|
||||||
* track. Structurally matches `RecurrencePolicy<ResolvedTrack>` without
|
* track. Structurally matches `RecurrencePolicy<ResolvedTrack>` without
|
||||||
* importing it (media stays core-free): `current` is the freshly resolved track
|
* importing it (media stays core-free): `current` is the freshly resolved track,
|
||||||
* (`undefined` if the reload errored), `previous` the prior resolved snapshot.
|
* `previous` the prior resolved snapshot.
|
||||||
*
|
*
|
||||||
* - Complete playlist (VoD, or live that hit `#EXT-X-ENDLIST`) → `null`: stop.
|
* - Complete playlist (VoD, or live that hit `#EXT-X-ENDLIST`) → `null`: stop.
|
||||||
* Keys off `Track.duration` (finite once complete), the single completeness
|
* Keys off `Track.duration` (finite once complete), the single completeness
|
||||||
* source of truth.
|
* 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
|
* - Unchanged window (same media sequence + segment count as `previous`) → poll
|
||||||
* at half the target duration; a moved/grown window (or the first reload) →
|
* at half the target duration; a moved/grown window (or the first reload) →
|
||||||
* full target duration.
|
* 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.
|
* Returned delays are milliseconds.
|
||||||
*/
|
*/
|
||||||
export function mediaPlaylistReloadDelay(
|
export function mediaPlaylistReloadDelay(current: ResolvedTrack, previous: ResolvedTrack | undefined): number | null {
|
||||||
current: ResolvedTrack | undefined,
|
|
||||||
previous: ResolvedTrack | undefined
|
|
||||||
): number | null {
|
|
||||||
if (!current) return targetDurationOf(previous) * 1000;
|
|
||||||
if (Number.isFinite(current.duration)) return null;
|
if (Number.isFinite(current.duration)) return null;
|
||||||
|
|
||||||
const target = targetDurationOf(current);
|
const target = targetDurationOf(current);
|
||||||
|
|||||||
@@ -43,12 +43,7 @@ describe('mediaPlaylistReloadDelay', () => {
|
|||||||
expect(mediaPlaylistReloadDelay(slid, prev)).toBe(4000);
|
expect(mediaPlaylistReloadDelay(slid, prev)).toBe(4000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('retries at the last-known cadence when a reload errors (current undefined)', () => {
|
it('falls back to 6s when the playlist carries no usable target duration', () => {
|
||||||
const prev = track({ targetDuration: 8 });
|
expect(mediaPlaylistReloadDelay(track({ targetDuration: 0 }), undefined)).toBe(6000);
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user