Seeding the four sample notes is six writes, each its own round trip and its own SQLite transaction:
spaces.create() → one command, one transaction
write(SEEDED_KEY) → preferences
notes.create() × 4 → four commands, four transactions
The process can die between any two of them, and SampleNotesService.seedIfFirstRun is then shut for good. Its two guards are the marker and "no space at all" — and an interruption after the first write leaves a space standing, so the second guard fires on every launch that follows. The marker is already down too. Both locks close on a library that has an empty canvas and no way back to one that is not.
The marker is written before the notes on purpose, and the comment says why: "a failure halfway through leaves an incomplete set rather than a second full one". That is the right call given six separate writes. It is a choice about which bad outcome to have, not a way of not having one.
Not theoretical
The end-to-end harness reproduced it, on both CI platforms. WebDriver's per-file browser.refresh() landed inside the seeding window on a slow runner, the reloaded front end read the marker and skipped the seeding it had just interrupted, and the corpus stayed without samples for the rest of the run — a space, no notes. 01-first-launch then asserted against an empty canvas. Fixed on the harness side in #98 by not refreshing ahead of the first file, which leaves the application's own fragility untouched.
What it is worth
Low. What is lost is the first-launch demonstration corpus, never anything the user wrote. The window is a few hundred milliseconds, on the first launch of a given install, and someone has to kill the application inside it. The cost is a bad first impression, not data.
The better argument is the second one: non-atomic seeding is a trap that has already cost half an hour of CI archaeology, and will cost it again to whoever meets it without this context.
Shape
One command that takes the drafts and writes them in a single transaction — seed_samples(space_name, drafts) -> Space, or near enough. transfer::bundle::merge is the precedent: it wraps a whole import in connection.transaction(...) for exactly this reason, and its comment says a per-note transaction "left half an import behind".
⚠️ The obvious version of this — move the seeding into Rust — is the wrong one. The sample text is translated, and arrives in the language the application started in. Rust would need a translation table, which this codebase refuses in three other places (tray labels, error codes, note titles). So the strings stay on the front end and travel as NoteDrafts; only the atomicity comes from the back.
With the writes atomic, the marker can move to after the seeding: "it is seeded" becomes something observed rather than hoped for. Both guards stay — they guard different things.
Two cheaper ideas, and why they are worse
- Write the marker last, without a transaction. It looks sufficient and is not: at the interruption, the next launch finds the marker missing but the space present, so the second guard fires, writes
'skipped', and the samples never come. Strictly worse than today.
- Re-seed whatever is missing. It needs identity for the samples, and they are deliberately ordinary notes the user may rename or throw away. There is nothing to match on.
Done when
Killing the application at any point during its first launch leaves either the four samples or a database that will seed them at the next start — never a space with nothing in it.
Seeding the four sample notes is six writes, each its own round trip and its own SQLite transaction:
The process can die between any two of them, and
SampleNotesService.seedIfFirstRunis then shut for good. Its two guards are the marker and "no space at all" — and an interruption after the first write leaves a space standing, so the second guard fires on every launch that follows. The marker is already down too. Both locks close on a library that has an empty canvas and no way back to one that is not.The marker is written before the notes on purpose, and the comment says why: "a failure halfway through leaves an incomplete set rather than a second full one". That is the right call given six separate writes. It is a choice about which bad outcome to have, not a way of not having one.
Not theoretical
The end-to-end harness reproduced it, on both CI platforms. WebDriver's per-file
browser.refresh()landed inside the seeding window on a slow runner, the reloaded front end read the marker and skipped the seeding it had just interrupted, and the corpus stayed without samples for the rest of the run — a space, no notes.01-first-launchthen asserted against an empty canvas. Fixed on the harness side in #98 by not refreshing ahead of the first file, which leaves the application's own fragility untouched.What it is worth
Low. What is lost is the first-launch demonstration corpus, never anything the user wrote. The window is a few hundred milliseconds, on the first launch of a given install, and someone has to kill the application inside it. The cost is a bad first impression, not data.
The better argument is the second one: non-atomic seeding is a trap that has already cost half an hour of CI archaeology, and will cost it again to whoever meets it without this context.
Shape
One command that takes the drafts and writes them in a single transaction —
seed_samples(space_name, drafts) -> Space, or near enough.transfer::bundle::mergeis the precedent: it wraps a whole import inconnection.transaction(...)for exactly this reason, and its comment says a per-note transaction "left half an import behind".NoteDrafts; only the atomicity comes from the back.With the writes atomic, the marker can move to after the seeding: "it is seeded" becomes something observed rather than hoped for. Both guards stay — they guard different things.
Two cheaper ideas, and why they are worse
'skipped', and the samples never come. Strictly worse than today.Done when
Killing the application at any point during its first launch leaves either the four samples or a database that will seed them at the next start — never a space with nothing in it.