OpenCut-app / OpenCut-app/OpenCut

[BUG] Concurrent autosaves and exit can silently lose newer project changes

Open
#870 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
89.8k
Forks
8.9k
PR merge metrics
No merged PRs in 30d

Description

Platform

Web editor on current dev (c2e266870172312f461df75da3e7f6fbe9d2a1fc); browser-independent persistence race.

Browser

All supported browsers (deterministic with a delayed/rejecting storage adapter).

Current Behavior

Project persistence is not ordered, and an older captured snapshot can overwrite or republish over newer active state.

On current dev:

  • SaveManager.saveNow() returns immediately when isSaving is true, so flush() does not necessarily await the in-flight save.
  • ProjectManager.saveCurrentProject() captures this.active plus scenes before awaiting storageService.saveProject(), then unconditionally assigns that captured updatedProject back to this.active and metadata after the await.
  • saveCurrentProject() catches and logs storage failures without rethrowing, while SaveManager has already cleared hasPendingSave.
  • editor-header.tsx::handleExit() closes and routes in finally, including after prepareExit() fails.

A settings, scene, timeline, thumbnail, or view mutation made while save A is pending can therefore be absent from A, overwritten in memory when A completes, or treated as clean after a failed write. Exit can close before the newest generation is durable.

Expected Behavior

Project record writes should serialize per project. Every persisted mutation should advance a generation, and publication after an awaited write should occur only when project ID, active session, lifecycle epoch, and generation are still current. flush() should drain through the newest dirty generation. A rejected write should remain dirty, reject the caller, and block close/navigation; stale successful snapshots must never replace current active fields or metadata.

Recurrence Probability

Always

Steps To Reproduce
  1. Open a project and replace/delay storageService.saveProject() so save A remains pending.
  2. Trigger a timeline change and let the debounce start save A.
  3. While A is pending, change project settings, switch a scene, update timeline content/view state, or start editor exit.
  4. Resolve A and inspect ProjectManager.getActive(), saved metadata, and a raw reload. The completion can publish A's older snapshot over the newer mutation; an exit flush can return while A is still pending.
  5. Repeat with save A rejecting. The rejection is swallowed, dirty state can clear, and editor exit still closes/routes.
Anything else?

Impact: This is a critical data-integrity issue: normal concurrent editing or exit can silently lose acknowledged local changes, and a transient persistence failure is presented as successful completion.

Bounded proposed fix: Add a small per-project persistence coordinator shared by save/lifecycle operations. Track monotonic content generations and active-session/lifecycle tokens; append record writes to a rejection-consuming settled tail while preserving caller errors; recapture newer work after settlement; merge only save-owned duration/updatedAt fields on current-token publication; make flush cancel debounce and drain until persisted generation catches dirty generation. Keep thumbnail generation best-effort, but make project persistence failure prevent close/routing and produce one caller-owned error notification.

Focused acceptance tests: Deferred save A followed independently by settings, scene, thumbnail, timeline view, timeline content, and all combined; inspect save B's snapshot, active state, metadata, dirty state, and raw reload. Cover A/B rejection, flush during A, debounce timer, repeated exit, and close/navigation occurring exactly once only after successful durability.

Per the contribution policy, this report is requesting explicit maintainer approval for this candidate before any implementation or PR work begins.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with SaveManager.saveNow() and flush(), ProjectManager.saveCurrentProject(), storageService.saveProject(), and editor-header.tsx::handleExit(). Reproduce the deferred and rejecting storage cases, then use the focused acceptance scenarios in the issue to verify generation ordering, dirty-state preservation, and that close or navigation occurs only after successful durability.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
frontend
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.