metagame/stores

PURPOSE — Zustand stores for the React metagame UI. Holds the client-side projection of server-owned identity and bootstrap state: the authenticated user, the resolved player profile, cloud-save / pity payloads, the bootstrap lifecycle flags the app shell gates on, sync status for the cloud round-trip, and the offline-mode escape hatch with its background recovery loop.

OWNS

  • Auth user handle and a loading flag for the initial mount.
  • Player profile projection (id, display name, email, account state, role, analytics opt-in, created-at) derived from the bootstrap payload.
  • Bootstrap lifecycle flags: bootstrapped (gates app-shell render), bootstrapError (init failure message).
  • Sync status state machine (idle / syncing / synced / error) and its accompanying error message.
  • Cloud-save payload and pity counter map handed back by the bootstrap RPC.
  • showWelcome latch that fires the callsign modal on the guest → claimed transition.
  • offlineMode flag plus its recoveryToast companion latch.
  • Module-scope recovery setInterval handle that pings auth on a fixed cadence while offline mode is active.
  • Synthetic offline profile minted with a crypto.randomUUID() id so downstream code that keys on profile.id sees a stable value with no server contact.
  • Dev-playground short-circuit: when the URL carries the dev flag, the store skips auth + bootstrap and seeds a synthetic profile.
  • A window-global mirror of offlineMode so non-importing modules can short-circuit RPCs without taking a store dependency.
  • One-shot localStorage flag (welcome_shown) that suppresses repeat welcome modals across reloads.
  • Auth subscription installed during init that re-bootstraps on user-id change or anon-to-claimed email upgrade.

READS FROM

  • metagame/services/auth for anonymous-session creation, current-user lookup, sign-out, magic-link send, and the auth-change subscription.
  • metagame/services/playerBootstrap for the bootstrap RPC call and the BootstrapPayload shape (profile, save, pity).
  • metagame/services/supabase client for the recovery-loop session probe.
  • window.location.search for the dev-playground flag, window for the offline-mirror global, localStorage for the welcome-shown flag, crypto.randomUUID() for the offline profile id.

PUSHES TO

  • engine/telemetry/sender via setTelemetryPlayerId whenever the active user id changes (init, auth-change subscription, offline entry, sign-out path).
  • engine/telemetry/sampler via Sampler.setPlayer on the same transitions.
  • engine/telemetry/diag via setDiagPlayerId on the same transitions.
  • window.__PLAYER_STORE_OFFLINE__ flag, written when entering offline mode and cleared when the recovery probe succeeds — read by the RPC layer to no-op cloud writes without circular imports.
  • React subscribers via Zustand set; no signals, no event bus.

DOES NOT

  • Own identity. Supabase Auth is authoritative; this store only projects the current user object.
  • Own profile / wallet / inventory creation. The bootstrap_player RPC is authoritative; this store only stamps the returned payload into local fields and hydrates downstream UI state.
  • Run gameplay state, run definitions, inventory entries, wallet balances, or any other domain data — only the profile / save / pity slice handed back by bootstrap.
  • Validate auth tokens, mint magic links, or talk to the OAuth flow — that lives in metagame/services/auth.
  • Decide when to gate the app shell — exposes the bootstrapped flag and lets the shell read it.
  • Render the welcome modal, sync indicator, or offline splash — exposes the flags those components read.
  • Persist save state back to the server — this store only ingests the payload at bootstrap; cloud writes go through the RPC layer.
  • Talk to game engine systems beyond pushing the player id into telemetry; engine state lives elsewhere.

Signals fired / Signals watched — none. The store communicates with React via Zustand subscriptions, with Supabase via direct service calls, with telemetry via direct setter calls, and with the RPC layer via a window-global flag. No engine signals are emitted or subscribed.

Entry points

  • usePlayerStore — the Zustand hook React components select from.
  • init — full mount-time bootstrap: dev-flag short-circuit, anonymous session, bootstrap RPC, telemetry wiring, welcome-modal detection, auth-change subscription install.
  • retrySync — Settings-driven re-run of init from the error state.
  • signIn — anonymous sign-in path that refreshes the local user handle.
  • signOut — clears profile / save / pity / bootstrapped, then re-runs init to land on a fresh anon session.
  • claimWithEmail — fires the magic link; completion is observed through the auth-change subscription installed in init.
  • setProfile — re-projects a fresh bootstrap profile payload into the store.
  • setSyncStatus — external setter for the sync state machine.
  • setPity — overwrites the pity map from a fresh server response.
  • dismissWelcome — sets the welcome_shown localStorage flag and hides the modal.
  • enterOfflineMode — mints the synthetic profile, flips the window-global, wires telemetry, and starts the recovery interval.
  • dismissRecoveryToast — hides the one-shot “CONNECTED” toast after offline recovery.

Pattern notes

  • Single store today (playerStore); the directory is set up for additional Zustand slices but currently holds only this file.
  • The dev-flag branch in init returns early before any auth or RPC work, so playground sessions never touch Supabase.
  • The auth-change handler captures the previous user id from get().user before calling set({ user }) — a re-bootstrap fires on id change, and a separate branch handles the anon-to-claimed case where the id is stable but email appears for the first time.
  • Offline mode communicates with the RPC layer through a window-global rather than a direct import, deliberately avoiding a circular module graph between the store and the service layer.
  • The recovery interval is held in a module-scope variable (not store state) so it survives store-level resets and can be enforced single-instance; entering offline mode clears any prior handle before scheduling.
  • The recovery probe wraps supabase.auth.getSession() in a Promise.race against a fixed timeout so a partial outage (auth up, DB down) cannot stall the loop.
  • Telemetry player-id is set in three coordinated calls (sender, sampler, diag) at every identity transition; the trio always moves together.
  • The welcome-modal trigger is gated by both the claimed account state and the absence of the welcome_shown localStorage flag, so the modal fires exactly once per claim across reloads.