mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
refactor(spf): drive live reload via a RecurringRunner instead of an epoch signal (WIP)
Replace the signal-as-event live-reload scheduler with a runner-driven model. The `resolveTrack` loader schedules its resolve work on a new `RecurringRunner` that re-runs the task on an injected `reschedule` policy; the separate `scheduleTrackReload` behavior and its per-type reload-epoch signals are deleted. Core (`core/tasks`): - `Task.run()` is now memoized (runs once, shares the result across calls) and gains `clone()` (fresh, pending, structurally identical) — added to `TaskLike`. - `RecurringRunner`: single-slot, id-keyed (dedup same id / abort-and-replace on new id), time-free. Each cycle runs `Promise.all([task.run(), reschedule(task, previous, signal)])` and re-runs a `clone()` while reschedule resolves `true`. - `Reschedule<T> = (task, previous, signal) => PromiseLike<boolean>` — invoked concurrently with the run, observes it via the memoized `run()`, owns its delay. - `delayedReschedule(cadence)` builds a Reschedule from a pure ms-cadence fn, start-anchored (subtracts the run's elapsed) so reloads are measured from load-start per RFC 8216 §6.3.4, preserving half-on-unchanged. Supporting: - `@videojs/utils/time`: add cancellable `sleep(ms, signal)`. - `media/hls/reload-policy`: `mediaPlaylistReloadDelay` (pure cadence; relocated scheduler logic — target-duration, half-on-unchanged, stop-on-ENDLIST, retry). - `resolve-track`: baked universal completeness gate + injected `reschedule`; engine composes `delayedReschedule(mediaPlaylistReloadDelay)`. WIP: not fully validated end-to-end against a live stream through this path. 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
1d51582d8c
commit
557d8bd72f
@@ -1 +1,2 @@
|
||||
export * from './format';
|
||||
export * from './sleep';
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Resolve after `ms` milliseconds. Pass a `signal` to make it cancellable: the
|
||||
* timer is cleared and the promise rejects with the signal's reason as soon as
|
||||
* the signal aborts (including if it's already aborted).
|
||||
*/
|
||||
export function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (signal?.aborted) {
|
||||
reject(signal.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
clearTimeout(timer);
|
||||
reject(signal?.reason);
|
||||
};
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
signal?.removeEventListener('abort', onAbort);
|
||||
resolve();
|
||||
}, ms);
|
||||
|
||||
signal?.addEventListener('abort', onAbort, { once: true });
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { sleep } from '../sleep';
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('sleep', () => {
|
||||
it('resolves after the given delay', async () => {
|
||||
vi.useFakeTimers();
|
||||
const resolved = vi.fn();
|
||||
const promise = sleep(100).then(resolved);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(99);
|
||||
expect(resolved).not.toHaveBeenCalled();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
await promise;
|
||||
expect(resolved).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('rejects with the signal reason and clears the timer when aborted mid-wait', async () => {
|
||||
vi.useFakeTimers();
|
||||
const controller = new AbortController();
|
||||
const reason = new DOMException('Aborted', 'AbortError');
|
||||
const promise = sleep(100, controller.signal);
|
||||
|
||||
controller.abort(reason);
|
||||
await expect(promise).rejects.toBe(reason);
|
||||
|
||||
// Timer was cleared — advancing past the delay does nothing further.
|
||||
await vi.advanceTimersByTimeAsync(200);
|
||||
});
|
||||
|
||||
it('rejects immediately when the signal is already aborted', async () => {
|
||||
const controller = new AbortController();
|
||||
const reason = new DOMException('Aborted', 'AbortError');
|
||||
controller.abort(reason);
|
||||
|
||||
await expect(sleep(100, controller.signal)).rejects.toBe(reason);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user