Skip to content

fix(timeline): isolate preview renders from concurrent exports#38

Merged
yultyyev merged 2 commits into
feat/ui-timelinefrom
fix/concurrent-render-collision
Jun 18, 2026
Merged

fix(timeline): isolate preview renders from concurrent exports#38
yultyyev merged 2 commits into
feat/ui-timelinefrom
fix/concurrent-render-collision

Conversation

@yultyyev

@yultyyev yultyyev commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Fix concurrent frame/export render collisions

Closes the last v0.3.0 pre-release-audit follow-up (non-blocking). On the localhost clip ui, scrubbing the viewer during an export could produce a transient garbled frame or a raw 500.

Targets feat/ui-timeline (#36's branch), not main — folds into #36 for a single v0.3.0 release. Independent of #37 (different functions/routes) — verified conflict-free either order. Merge into #36's branch; the eventual #36main merge cuts one release.

Root cause

compile.ts named intermediate segments deterministically — tl-seg-<index>-<clip>.mp4 — with no random component (unlike newOutputPath). run-plan.ts runs ffmpeg -y then unlinks intermediates in a finally, and nothing serializes the UI render path. So a preview frame render and a concurrent export wrote/-y-overwrote the same segment files and unlinked each other's → a transient 500 or garbled frame. Self-healing on retry, and the timeline document (source of truth) is never touched — hence low severity — but a real race on the unauthenticated dev server.

Fix

  1. Collision-free preview pathsCompileContext gains an optional tag mixed into intermediate (segment/text/fold) filenames. Export stays deterministic (no tag); the frame route sets a per-request tag = frame-<rand>, so previews get their own paths and can't collide — with an export or with each other. Default (untagged) paths are unchanged, so existing plans and tests are untouched.
  2. Graceful degradation — the frame and export routes translate a non-CompileError (a transient FFmpeg failure) into a clean 503 instead of a raw 500. CompileError (a real doc problem) stays 422.
  3. Abort superseded scrubsViewer fetches the frame as a blob behind an AbortController, so a superseded scrub is cancelled instead of left rendering server-side, and a 503 reads as a message rather than a broken <img>. The previous frame stays on screen until the next decodes — no blank flash.

Verification

pnpm type-check ✓ · 543 tests ✓ (new: tagged context → collision-free segment path, untagged unchanged) · pnpm lint exit 0 · pnpm build ✓ (Vite compiles the new Viewer).

🤖 Generated with Claude Code

yultyyev added 2 commits June 18, 2026 16:39
Intermediate segment/text/fold filenames were deterministic
(tl-seg-<index>-<clip>), so a preview frame render and a concurrent export
wrote and ffmpeg -y-overwrote the SAME files — and each unlinked the other's
in run-plan's finally — yielding a transient garbled frame or 500. The
timeline doc (source of truth) was never touched, so it self-healed on retry,
but it is a real race on the unauthenticated localhost dev server.

Add an optional tag to CompileContext, mixed into the intermediate names.
Export stays deterministic (no tag); the clip ui sets a per-request tag on
each frame render (next commit), so previews get their own paths and cannot
collide. Default (untagged) paths are unchanged — existing plans/tests are
untouched.
- The frame route passes a per-request tag to buildFrameAtPlan, so a preview's
  intermediate segments never collide with a concurrent export's.
- Frame and export routes translate a non-CompileError (a transient FFmpeg
  failure) into a clean 503 instead of a raw 500.
- Viewer fetches the frame as a blob behind an AbortController, so a superseded
  scrub is cancelled instead of left rendering, and a 503 reads as a message
  rather than a broken img. The previous frame stays on screen until the next
  decodes — no blank flash.
@yultyyev yultyyev changed the base branch from main to feat/ui-timeline June 18, 2026 23:46
@yultyyev yultyyev merged commit 42bc488 into feat/ui-timeline Jun 18, 2026
1 check passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.3.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant