refactor(producer): make sequencer the sole writer of job.duration/totalFrames

The probe stage previously assigned `job.duration` and `job.totalFrames`
inside its body AND the sequencer re-asserted them after the call to
restore TS narrowing. Two writers for the same field is a maintenance
hazard — a future refactor could drop one and create a silent skew.

Move ownership: the stage computes `duration` and `totalFrames` and
returns them; the sequencer is the sole writer onto the `RenderJob`.
This also aligns with the eventual chunk-worker model where a chunk
running in a separate process cannot mutate the orchestrator's `job`.

No observable behavior change. `job.duration` / `job.totalFrames` end
up with the same values; the zero-duration `throw` still happens
inside the stage (now using the local `duration` constant) before any
sequencer-side assignment. Verified by:

- `bun run --filter @hyperframes/producer typecheck` clean
- `bun test packages/producer/src/services/` 175 pass / 1 pre-existing
  unrelated failure on `main`

Review feedback addressed: vanceingalls on #719.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
James
2026-05-11 19:03:02 +00:00
co-authored by Claude Opus 4.7
parent 56ac52384f
commit 20242515ec
2 changed files with 8 additions and 9 deletions
@@ -13,8 +13,11 @@
* plan lists recompile as a sibling phase.
* - `composition` (videos/audios/duration) is mutated in place — callers
* downstream see the reconciled view through the same object reference.
* - `job.duration` and `job.totalFrames` are assigned at the same code
* points.
* - The stage computes the final composition `duration` and `totalFrames`
* and returns them. Assigning those values onto the `RenderJob` is the
* sequencer's responsibility — a future chunk worker can't mutate the
* orchestrator's `job` object, and keeping the assignment in one place
* prevents the same value living in two writers.
* - The "Composition duration is 0" diagnostic builds the same hint
* string from the same console-buffer regex and `__timelines` probe.
* - The post-probe "failed network requests" warning fires with the same
@@ -288,10 +291,8 @@ export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageR
}
const browserProbeMs = Date.now() - probeStart;
job.duration = composition.duration;
job.totalFrames = Math.ceil(composition.duration * fpsToNumber(job.config.fps));
const duration = composition.duration;
const totalFrames = job.totalFrames;
const totalFrames = Math.ceil(duration * fpsToNumber(job.config.fps));
if (duration <= 0) {
// Gather diagnostics to help users understand why the render would produce a black video.
@@ -2153,10 +2153,8 @@ export async function executeRenderJob(
fileServer = probeResult.fileServer;
probeSession = probeResult.probeSession;
lastBrowserConsole = probeResult.lastBrowserConsole;
// Re-assign through the typed result so the rest of the function sees
// `job.duration` and `job.totalFrames` narrowed to `number` — the
// assignments happened inside `runProbeStage`, but mirroring them here
// restores TypeScript's control-flow narrowing.
// The probe stage produces `duration` / `totalFrames` values; the
// sequencer owns the `RenderJob` and writes them onto it.
job.duration = probeResult.duration;
job.totalFrames = probeResult.totalFrames;
const totalFrames = probeResult.totalFrames;