feat: sync progression across devices, rebalance hunt XP - #10
Conversation
M10 shipped accounts, but a player's XP, level, badges and statistics still lived only in the browser, so signing in on a second device showed a blank profile — short of deliverable 5, which asks for progression synced across devices. Add a `progression` table, one row per account, private to its owner under RLS. It holds the XP, the statistics as one JSON document, and the unlocked badges. The worldwide leaderboard still ranks the `scores` table, so this row drives only its owner's own dashboard. Drop the now-single-source vestigial profiles.xp/level: they were added in M10 but never read — every level shown is derived from the store — and the M10 grants deliberately kept them out of the client's reach, so nothing could keep them up to date anyway. Also ignore the local Google credentials (supabase/.env, client_secret).
Mirror the progression store to the new table for a signed-in player. On sign-in the account's row is pulled: the first time there is none it is seeded from whatever was built as a guest, so that progress is kept; after that the server is the source of truth and a later sign-in on any device restores it. While signed in, each change is written back, debounced into one request. A failed pull is treated as unknown rather than as an empty account, so a transient error can never overwrite a real server copy with the empty local one. The pull also awaits the session first, since the very first request after sign-in can otherwise outrun the token and be rejected — the same guard is applied to the profile fetch, which had the same race.
The hunt paid one XP per ten points, so a strong sixty-second round was worth several hundred XP — more than a dozen wins against the engine — and jumped the player several levels at once. Pay one per hundred points instead, capped near a battle win, so a round is worth about a game rather than a whole evening of them.
The setup named only the init migration; with a second one added, tell the reader to run each file in order and note that re-running is how a later migration reaches a project that already has the earlier ones.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds a Supabase ChangesProgression synchronization
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant App
participant useProgressionSync
participant useProgressionStore
participant Supabase
App->>useProgressionSync: mount hook
useProgressionSync->>Supabase: refresh session and fetch progression
Supabase-->>useProgressionSync: return row or no row
useProgressionSync->>useProgressionStore: hydrate snapshot
useProgressionStore-->>useProgressionSync: emit progression changes
useProgressionSync->>Supabase: debounced upsert
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/features/progression/useProgressionSync.ts`:
- Around line 47-97: The progression sync flow must not enable writes after a
failed pull, since an unhydrated guest snapshot could overwrite server data;
keep `ready` false until the baseline is successfully read or a retry
establishes it. Update `write` and the subscription flow to serialize upserts so
only one write is in flight, and add version/conflict handling that prevents an
older full snapshot from being sent or committed after a newer local state.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d5a1c6ac-1b99-4505-8d0d-34300b425bbc
📒 Files selected for processing (12)
.gitignoreREADME.mdsrc/App.tsxsrc/features/progression/levels.test.tssrc/features/progression/levels.tssrc/features/progression/sync.test.tssrc/features/progression/sync.tssrc/features/progression/useProgressionSync.tssrc/lib/supabase.tssrc/store/useAuthStore.tssrc/store/useProgressionStore.tssupabase/migrations/20260722215455_progression.sql
A write sends the whole progression row, so sending a stale one replaces good server data — the earlier "upsert merges rather than replaces" note was simply wrong. Two holes are closed: A failed pull used to enable writes anyway, so the next local change could upsert an unhydrated guest snapshot over a real server row. The pull is now retried, and writes stay blocked until the baseline is actually read or confirmed absent; a pull that keeps failing leaves the session read-only rather than risk a clobber. Writes were also unserialised, so two in-flight upserts could finish out of order and an older snapshot could land last. Only one upsert now runs at a time, looping once more on the freshest state if the store moved while it ran, so the server always converges on the newest snapshot. Two devices active in the very same second still resolve last-write-wins on the whole row, which matches the "server is the source of truth on sign-in" model and does not warrant per-field version vectors here.
What
Delivers the cross-device progression the specification lists as deliverable 5, which M10 left unfinished: a signed-in player's XP, level, badges and statistics lived only in the browser, so a second device — or a cleared cache — showed a blank profile.
Changes
progressiontable (new migration): one row per account, private to its owner under RLS, holding XP, the statistics as one JSON document, and the unlocked badges. The worldwide leaderboard still ranksscores, so this row drives only its owner's own dashboard.profiles.xp/profiles.level: added in M10 but never read (every level is derived from the store), and kept out of the client's reach by the M10 grants, so they could only ever be a second stale copy.useProgressionSync): on sign-in the account's row is pulled; the first time there is none it is seeded from the guest progress so it is kept; afterwards the server is the source of truth and a later sign-in on any device restores it. Each change is written back, debounced.Verification
npm run cigreen; 255 unit tests pass.Summary by CodeRabbit
progressiontable.