Learn Flashcards Cloud Sync
Syncs the CONTENT (id/type/front/back) of CardMirror's Learn
spaced-repetition flashcards to a signed-in user's account, closing the
same account-linking gap Quick Cards Cloud Save
closed for the reusable-snippet library — the Learn store
(packages/debate-editor/src/editor/learn-store.ts) was device-local
only.
- Package:
debate-editor(learnCardsSync,LearnCardsSync,learn-cards-client),apps/debate-ai.com(/api/learn-cards,saved_learn_cardsD1 table)
What it does
On boot, once the local Learn store has loaded, learnCardsSync.init()
merges once against the account: any card synced from another device but
missing locally is adopted under its own id (never duplicated), and any
card 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 card create, edit, or delete (including a file's cards being
pruned via "forget this file") mirrors itself to the account the same
way. Signed out, or under the Electron desktop build, the Learn store
keeps working exactly as before, purely on-device.
What's intentionally excluded
Only a card's portable content syncs — its review schedule (due date, interval, lapses), file anchors, AI threads, and private notes all stay local-only. A card adopted on a new device starts with no schedule pressure, the same way the manage GUI's own JSON export/import already treats a card whose carried schedule is absent — restoring a card is not meant to restore a specific device's review history. Custom decks and the review log each sync separately — see Learn Decks Cloud Sync and Learn Review Log Cloud Sync. The remaining sub-collections (anchors, AI threads, notes, doc registry) live in one shared blob per device (not one row per record), so they don't fit this sync's per-record shape; syncing them would need a bespoke schema of their own.
Every per-record push carries optimistic concurrency: PUT /api/learn-cards/[cardId] compares the incoming card's own updatedAt
against whatever's already saved for that id, and rejects (409) instead of
overwriting when the saved version is newer — an edit made on another
device that hasn't reached this browser yet. On that rejection, this
browser adopts the newer version into its own LearnStore instead of
losing it, the same "never overwrite, only reconcile" rule init()'s merge
already follows — mirroring
Quick Cards Cloud Save's own fix
for the identical race. CardDef didn't carry an updatedAt field before
this: it's now stamped by upsertCard/importCards themselves (not by
callers), and stays optional on the type so a card written before this
field existed — locally, or already synced to the account — still
round-trips; a side with no updatedAt is treated as arbitrarily old
rather than blocking the sync, which is exactly the previous
last-write-wins behavior until both sides have a real timestamp.
Known gaps
None open.
