import { writable, get } from 'svelte/store'; import { authStore } from './auth'; import { getDex, getFeed, getLeaderboard, getStats, getUser } from '$lib/services/dataset'; import type { Monster, MonsterSummary, LeaderboardEntry, UserRecord, Stats, } from '$lib/types'; interface DataState { dex: MonsterSummary[] | null; feed: MonsterSummary[] | null; leaderboard: LeaderboardEntry[] | null; stats: Stats | null; myUser: UserRecord | null; loading: boolean; error: string | null; } const initialState: DataState = { dex: null, feed: null, leaderboard: null, stats: null, myUser: null, loading: false, error: null, }; function createDataStore() { const { subscribe, set, update } = writable(initialState); /** Fetch all index files + the current user's record. Used by PullToRefresh * and on first load. No cache-busting — stale reads for other people's * finds are fine; monsters are immutable. */ async function refreshAll(): Promise { update((s) => ({ ...s, loading: true, error: null })); try { const [dex, feed, leaderboard, stats] = await Promise.all([ getDex(), getFeed(), getLeaderboard(), getStats(), ]); const auth = get(authStore); let myUser: UserRecord | null = null; if (auth.userInfo?.sub) { try { myUser = await getUser(auth.userInfo.sub); } catch (err) { console.error('[dataStore] Failed to fetch user record for sub:', auth.userInfo.sub, err); } } set({ dex, feed, leaderboard, stats, myUser, loading: false, error: null }); } catch (err) { update((s) => ({ ...s, loading: false, error: err instanceof Error ? err.message : 'Failed to load data', })); } } /** Re-fetch just the current user's record. */ async function refreshMine(): Promise { const auth = get(authStore); if (!auth.userInfo?.sub) return; try { const myUser = await getUser(auth.userInfo.sub); update((s) => ({ ...s, myUser })); } catch { // ignore — user may not exist in dataset yet } } /** Optimistic insert after a successful scan. Mutates in-memory state only — * no refetch. The /scan response already contains the full monster, so we * can show it instantly regardless of CDN propagation lag. * * - Always ensures the monster is in `dex` (fixes CDN lag for both new * and existing scans). * - Prepends to `feed` only for genuinely new discoveries. * - Updates `myUser` (discoveries, total_rarity, discovery_count) only * when the current user is the discoverer (status === 'new'). */ function optimisticInsert(monster: Monster, isNew: boolean): void { update((s) => { const summary: MonsterSummary = { key: monster.key, name: monster.name, type: monster.type, rarity: monster.rarity, image_url: monster.image_url, discoverer_username: monster.discoverer.username, discovered_at: monster.discovered_at, }; const dex = s.dex && s.dex.some((m) => m.key === monster.key) ? s.dex : [summary, ...(s.dex || [])]; const feed = isNew && s.feed && !s.feed.some((m) => m.key === monster.key) ? [summary, ...s.feed].slice(0, 50) : s.feed; let myUser = s.myUser; if (isNew && myUser && !myUser.discoveries.includes(monster.key)) { myUser = { ...myUser, discoveries: [...myUser.discoveries, monster.key], total_rarity: myUser.total_rarity + monster.rarity, discovery_count: myUser.discovery_count + 1, }; } return { ...s, dex, feed, myUser }; }); } return { subscribe, set, update, refreshAll, refreshMine, optimisticInsert }; } export const dataStore = createDataStore();