Learn Decks Cloud Sync
Syncs CardMirror's Learn custom decks (deckId/name/cardIds/
createdAt) to a signed-in user's account, the same way
Learn Flashcards Cloud Sync
syncs a card's own content — the Learn store
(packages/debate-editor/src/editor/learn-store.ts) was device-local
only.
- Package:
debate-editor(learnDecksSync,LearnDecksSync,learn-decks-client,learn-deck-manage-ui),apps/debate-ai.com(/api/learn-decks,saved_learn_decksD1 table)
What it does
On boot, once the local Learn store has loaded, learnDecksSync.init()
merges once against the account: any deck synced from another device but
missing locally is adopted under its own id, and any deck that exists
only on this device is best-effort pushed up — neither side ever
overwrites the other, it only fills gaps. After the merge, every later
deck create or delete mirrors itself to the account the same way; a rename
or membership change instead sends one op
(addCardId/removeCardId/rename, PATCH /api/learn-decks/[deckId])
per actual change, resolved against the account's current stored deck
rather than a whole-deck replace — see Known gaps. Signed out, or
under the Electron desktop build, decks keep working exactly as before,
purely on-device.
A deck's cardIds reference cards synced separately by
Learn Flashcards Cloud Sync; a
deck can legitimately reference a card that hasn't reached this device
yet, the same soft-reference gap that sync already accepts for card
content.
What's intentionally excluded
The other Learn sub-collections (schedules, anchors, AI threads, notes,
doc registry) stay local-only for the same reason
learn-cards-cloud-sync.mdx documents: they live in one shared blob per
device, not one row per record, so they don't fit this sync's per-record
shape. The review log syncs separately — see
Learn Review Log Cloud Sync.
Known gaps
No optimistic-concurrency handling — editing the same deck (renaming it, or changing its members) from two signed-in devices at once has the last write win.Fixed:LearnDecksSync#pushDeckChange(learn-decks-sync.ts) now diffs the previous vs. current deck and sends one op per actual change —addCardId/removeCardIdper card,renamefor a name change — instead of the whole{name, cardIds}snapshot it used to push.PATCH /api/learn-decks/[deckId]resolves each op against the row's current stored deck (read-then-write) viaapplyLearnDeckOp(debate-editor'slearn-deck-op.ts), the same shape/api/settings'sfavoriteTools/wordLimitPresets/outlineFilterPresets/savedArgumentCollectionsops already use — so two devices adding, removing, or renaming at once can no longer drop each other's change. A brand-new deck still pushes in full (there is no row to race against yet); if an op 404s — this deck's own create push hasn't landed yet —pushDeckChangefalls back to a fullPUT, which also covers every pending change in one call.PUTitself is unchanged and still accepted for a caller that genuinely needs a whole-deck replace, but nothing in this app sends one for a rename or membership change anymore.No dedicated deck-management UI shows a per-deck sync status yet; decks are currently created/edited from the home screen's scope picker, which has no sync indicator.Fixed, and the old description was stale on top of being incomplete: the "home screen's scope picker" it referred to (home-screen.ts's per-scope "due today" rows) can only ever display an existing deck — it never calledcreateDeck/renameDeck/deleteDeck/setDeckMembership— and that screen has been a permanent no-op since 2026-08-26 regardless (see its ownshow()doc comment), so it was never a reachable path either way. Before this fix, a deck could only come to exist on a device via account sync adopting one created somewhere, but no device anywhere could create the first one — decks were fully unreachable from the UI.learn-deck-manage-ui.ts's Decks button (next to History in the "Manage flashcards" overlay's bar) opens create/rename/two-click-delete for decks, and an expandable per-deck card picker (add from a dropdown of cards not yet in the deck, remove with a per-card button) — the first UI surface for any ofLearnStore's deck mutators. It shows the same coarse "Synced to your account" / "Not synced — sign in to sync" line (vialearnDecksSync.isSynced()) the flashcard list and Review history already do.Fixed:LearnDecksSynctracks no per-deck status (its pushes/deletes are fire-and-forget), so a per-deck badge would need that state added to the sync class first — a real follow-up, not done here.LearnDecksSyncnow keeps alandedmap of each deck's last confirmed value — advanced only once a create/op/fallback push actually resolves, distinct from thebaselinemap used purely to diff the next change — and exposes it asgetDeckSyncStatus(deckId)("synced"/"pending"/"unknown", mirroringdebate-data-sync'sgetToolRecordSyncStatusshape) plus anonStatusChangesubscription so a caller can re-render once a fire-and-forget push settles, since that alone doesn't mutate the store.learn-deck-manage-ui.tsshows each deck's status as a small "Synced" / "Not yet synced" badge next to its name — the same chip languageuser-dictionary-ui.tsuses forgetToolRecordSyncStatus— omitted entirely while the coarse status is"unknown".
