From 5c0ba37eda80d39e6ceca59bb1d5f4942f858995 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Thu, 17 Sep 2026 14:32:37 +0800 Subject: chore: establish Strategy Lab source baseline (development, not release) --- frontend/src/lib/listsStore.svelte.ts | 92 +++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 frontend/src/lib/listsStore.svelte.ts (limited to 'frontend/src/lib/listsStore.svelte.ts') diff --git a/frontend/src/lib/listsStore.svelte.ts b/frontend/src/lib/listsStore.svelte.ts new file mode 100644 index 0000000..102543f --- /dev/null +++ b/frontend/src/lib/listsStore.svelte.ts @@ -0,0 +1,92 @@ +import type { Project } from './types'; +import { getProject, listProjects } from './client'; +import { ApiError } from './api'; + +let projects: Project[] = $state([]); + +// ids that failed to fetch because the project is not visible to this session +// (404 not found / 403 not owner). Remembered so the sidebar can show an honest +// 不可见项目 state instead of an endless 加载中… retry loop. Cleared on logout / +// session switch so cross-user stale names or failure marks never leak. +// $state map so derived views (sidebar) re-react when a mark is added/cleared. +let invisibleIds: Record = $state({}); +let inflight = new Map>(); + +export interface ProjectsStore { + get all(): Project[]; + set(items: Project[]): void; + /** fetch + cache a project the sidebar/derived views cannot see yet (deep link) */ + ensure(id: string): Promise; + bump(p: Project): void; + /** reactive lookup: reads only the $state projects array */ + get(id: string): Project | undefined; + load(): Promise; + /** drop all cached projects + failure marks (must be called on logout so a + * future session on the same browser cannot see the previous user's names) */ + clear(): void; +} + +function upsert(p: Project) { + const idx = projects.findIndex((x) => x.id === p.id); + if (idx >= 0) projects[idx] = p; + else projects = [...projects, p]; +} + +export const projectsStore: ProjectsStore = { + get all() { + return projects; + }, + set(items: Project[]) { + projects = items; + }, + get(id: string) { + return projects.find((p) => p.id === id); + }, + bump(p: Project) { + upsert(p); + }, + async ensure(id: string) { + const known = projects.find((p) => p.id === id); + if (known) return known; + if (invisibleIds[id]) return null; + const pending = inflight.get(id); + if (pending) return pending; + const fetchPromise = (async () => { + try { + const p = await getProject(id); + upsert(p); + return p; + } catch (e) { + if (e instanceof ApiError && (e.status === 403 || e.status === 404)) { + invisibleIds = { ...invisibleIds, [id]: true }; + return null; + } + throw e; + } finally { + inflight.delete(id); + } + })(); + inflight.set(id, fetchPromise); + return fetchPromise; + }, + async load() { + const { items } = await listProjects(); + projects = items; + return items; + }, + clear() { + projects = []; + invisibleIds = {}; + inflight.clear(); + } +}; + +/** reactive derived read for components */ +export function projectOf(id: string): Project | undefined { + return projectsStore.get(id); +} + +/** true when a previous ensure() proved the id is not visible for this session */ +export function projectInvisible(id: string): boolean { + return !!invisibleIds[id]; +} -- cgit v1.2.3