Skip to content

[BUG] A failed storage migration permanently poisons retries and can leave saving paused #871

Description

@kimdogyeom

Platform

Web editor on current dev (c2e266870172312f461df75da3e7f6fbe9d2a1fc); browser-independent storage initialization failure.

Browser

All supported browsers (deterministic with a migration adapter that rejects once).

Current Behavior

A transient storage migration rejection is cached permanently at two layers, so later project loads cannot retry successfully.

On current dev:

  • ProjectManager.ensureStorageMigrations() stores storageMigrationPromise and never clears it when runStorageMigrations() rejects.
  • StorageService.ensureMigrations() likewise retains a rejected migrationsPromise.
  • ProjectManager.loadProject() calls editor.save.pause() and awaits ensureStorageMigrations() before entering its try/finally. If migration rejects, the finally that resets isLoading, notifies, and calls editor.save.resume() is skipped.
  • Subsequent loadProject()/loadAllProjects() calls await the same rejected promise rather than rerunning the migration.

After one transient IndexedDB/OPFS migration failure, project loading can remain poisoned for the manager lifetime and autosave can remain paused.

Expected Behavior

Concurrent migration callers should share one in-flight promise, successful completion should remain cached, and a rejected current promise should be cleared so the next explicit load retries. Every path after pausing save should restore loading/migration flags, notify, and resume saving exactly once. Failed initialization should not clear the currently loaded media/scenes or partially publish project state.

Recurrence Probability

Always

Steps To Reproduce

  1. Configure runStorageMigrations() to reject its first call and resolve its second.
  2. Call ProjectManager.loadProject({ id }) and observe the first rejection.
  3. Restore the migration dependency and call loadProject({ id }) or loadAllProjects() again.
  4. Observe that the original rejected storageMigrationPromise is awaited again, so migration is not retried.
  5. Observe that the first loadProject() rejected before its try/finally, leaving editor.save paused and initial loading state uncleared.
  6. Repeat at the StorageService.ensureMigrations() layer; its rejected cache behaves the same way.

Anything else?

Impact: A recoverable local-storage hiccup can make all projects unusable until the application/manager is recreated, while silently disabling further autosaves in the affected session. This is a critical availability and data-durability failure.

Bounded proposed fix: At both migration-cache layers, assign one local promise, share it across concurrent callers, cache fulfillment, and clear rejection only when the cache still points to that promise. In loadProject(), place all work after save.pause() under one try/finally; restore flags/notification and resume exactly once on every outcome. Preserve existing media/scenes and active state until migration and load succeed.

Focused acceptance tests: For both caches, rejection followed by resolution, concurrent calls invoking migration once, fulfilled-cache reuse, and identity-safe rejection cleanup. For loadProject()/loadAllProjects(), assert state preservation, loading/migration flag reset, notifications, and save pause/resume balance on every failure path.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions