Commit Graph
19 Commits
Author SHA1 Message Date
Christian PillsburyandClaude Opus 4.8 5aa99674e9 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>
2026-06-25 09:59:25 -07:00
Christian PillsburyandClaude Opus 4.8 2520373f2f refactor(spf): make RecurringRunner a self-recursive schedule(), require reschedule
Replace the imperative `#loop()` with a single chained promise: each cycle's
`.then` returns the next cycle's `schedule(clone())`, so the recurrence is the
method calling itself. The slot is released just before re-scheduling the same-id
clone so the call advances rather than dedup-returning; ownership is tracked by
`#active === task`.

Error handling moves downstream — the runner no longer invents a retry policy:
- A genuine run/reschedule failure rejects schedule()'s promise (propagates to
  the caller); no swallowing.
- The runner's own cancellation (abort/supersede/destroy) is not a failure, so an
  aborted recurrence settles quietly — callers don't `.catch` routine teardown.

Consequences:
- `reschedule` is now required; `runOnce` expresses run-exactly-once explicitly
  (a missing reschedule is a bug, not a silent run-once).
- Reschedule-driven retry-on-transient-error is dropped (a rejected run is
  terminal). The retry logic in delayedReschedule / mediaPlaylistReloadDelay is
  now vestigial — to be cleaned up or relocated to the fetch layer next.
- `resolve-track` catches schedule()'s promise (abort settles quietly; genuine
  resolve failures end the recurrence, TODO surface to state).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 09:59:25 -07:00
Christian PillsburyandClaude Opus 4.8 5154c424f9 refactor(spf): simplify Reschedule to single-arg by carrying previous/signal on Task
Collapse `Reschedule<T>` from `(task, previous, signal) => ...` to
`(task) => PromiseLike<boolean>`. One `delayedReschedule` instance is shared
across the video + audio runners, so per-recurrence state can't live in its
closure — the task is the only per-recurrence carrier, so both dropped params
move onto it:

- `task.signal` exposes the task's composed signal. RecurringRunner drops its
  separate `#abort` AbortController: the task is now the sole cancellation
  channel and loop ownership is task-identity, not an AbortController token.
- `task.previous` is carried by `clone()` as lineage
  (`#previous = this.#value ?? this.#previous`), reproducing the last-*successful*
  value semantics (an errored cycle inherits the prior good value) with no
  bookkeeping in the runner — `#loop` no longer threads previous/result.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 09:59:24 -07:00
Christian PillsburyandClaude Opus 4.8 73f0ef711a refactor(spf): memoize Task.run via a named #execute, fixing orphan rejections
Memoize the run-machinery promise itself (`#promise ??= this.#execute()`) rather
than reassigning `#promise` to fresh `Promise.resolve(value)` / `Promise.reject(error)`
on settle. The reassigned promises were never awaited, so an errored or aborted
task that's not re-run (the norm — the RecurringRunner moves on to a clone) left
an unhandled rejection. The memoized promise is the one callers await, so it's
always handled.

Also closes a sync-throw gap: a `#runFn` that throws synchronously is now captured
as a rejected memoized promise instead of leaving `#promise` unset (which would
re-execute on the next run()).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 09:59:24 -07:00
Christian PillsburyandClaude Opus 4.8 557d8bd72f 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>
2026-06-25 09:59:24 -07:00
d9f9efde88 fix(spf): refactor track switching to rules (#1658)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 14:48:34 -07:00
057f32573e feat(spf): multi-track audio + skills building features and behaviors (#1605)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 09:53:13 -07:00
c112b6248b fix(spf): conventions, per-type specialization, config threading (#1537)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 11:38:47 -07:00
8fa63c6567 feat(spf) discrete signals and behavior objects (#1508)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 08:32:44 -07:00
0cfd3bb395 feat(spf): HLS engine composition walkthrough + doc-driven cleanups (#1512)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 12:07:26 -07:00
Christian PillsburyandGitHub 69852ddeb0 docs/spf fundamental concepts (#1396)
Feedback from @decepulis treated as a fast follow/incremental improvement effort for expediency.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 11:16:56 -07:00
1346d869ae feat(spf): architecture reactors (#1218)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 13:38:24 -07:00
rahimandGitHub 071325de4d fix(utils): stable sort comparator and orphaned JSDoc (#1286) 2026-04-08 17:39:28 +10:00
3fc6cbb9bc poc(spf): migrate reactors from WritableState to TC39 Signals (#1112)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 13:24:39 -07:00
rahimandGitHub c3641c888c fix(utils): polyfill AbortSignal.any for Chromium ≤115 (#1142) 2026-03-26 13:47:48 -07:00
rahimandGitHub 8e2e56e36e chore: upgrade typescript 6, vitest 4, align spf package (#1101) 2026-03-24 01:53:36 -07:00
Christian PillsburyandGitHub 04f81a26e1 fix(spf): implement preload IDL attribute on SpfMedia (#1069) 2026-03-23 07:30:20 -07:00
rahimandGitHub bd4e47b783 chore: fix repo biome lint errors (#804) 2026-03-09 21:10:18 -07:00
27a3993fb2 feat(spf): initial push of SPF (#784)
Co-authored-by: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Wesley Luyten <me@wesleyluyten.com>
2026-03-09 17:32:24 -07:00