Skip to content

Autosave flush reports success for dirty keys it never writes, then clears the unsaved indicator #72

Description

@BootBlock

Confirmed — the flush layer reports success for keys it cannot write

flushDirtyKeys resolves for several dirty-key kinds that write nothing, so AutosaveQueue clears the key, the unsaved-dot goes out, and projectService.ts:89 calls setModified(false). The user is told their work is saved when nothing was persisted.

Three paths resolve without writing:

1. Unknown kinds fall through to a bare returnsrc/core/project/persist.ts:60-62:

    default:
      return;
  }

2. flushProject early-returns on an id mismatchpersist.ts:68:

  if (project.projectId !== id) return;

3. flushSettings writes nothing unless the key is a Q-Link keypersist.ts:231-235:

async function flushSettings(repositories: Repositories, settingsKey: string): Promise<void> {
  if (settingsKey.startsWith('qlink:')) {
    await repositories.settings.set(settingsKey, JSON.stringify(useHardwareStore.getState().qLinkBindings));
  }
}

Any settings: key that is not settings:qlink:* is silently a no-op that reports success.

Proven: flushDirtyKeys(repos, ['settings:theme']) resolved, wrote nothing to the repositories, and drove AutosaveQueue.onIdle once with hasPending === false.

Consequence. This is the write-behind autosave path §4.4 describes ("every committed mutation marks the owning entity dirty; a write-behind queue flushes dirty entities"). A dirty key that no flushX handles is indistinguishable, from the queue's point of view, from one that flushed cleanly — so the work is dropped and the UI actively confirms it was saved. §4.4's "unobtrusive unsaved-dot until flushed" becomes a false negative.

Distinct from existing issues. #41 is about the save button's messaging on failed/no-op saves; #19 is about project switching discarding unflushed autosave. This is the flush layer itself declaring success for work it never attempted — upstream of both.

Suggested fix: make the contract explicit. flushOne should either return a written/not-written signal that the queue can act on, or throw on an unhandled kind so the retry path engages and the dirty dot stays. Silently resolving is the one behaviour that cannot be right.

Coverage context

There is no test file for persist.ts at all. The only coverage is 5 incidental cases via hydrate.test.ts:166-210, exercising track, project, sequence and events. The program, automation, song and settings flush paths are entirely untested — and the latter three are actively used in production by useSequenceStore.ts:294, useSequenceStore.ts:309 and useHardwareStore.ts:79,91.

Metadata

Metadata

Assignees

Labels

autosavebugSomething isn't workingdata-lossCan lose or silently corrupt the user's workstorageOPFS, SQLite, repositories, and persistence (spec 9)testingUnit, offline-render, and real-browser verification (spec 11)

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions