piclets / src /lib /stores /data.ts
Fraser's picture
Mine filter by discoverer_username + camera-only scanner
3f82f76
Raw History Blame Contribute Delete
3.93 kB
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<DataState>(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<void> {
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<void> {
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();