summaryrefslogtreecommitdiff
path: root/frontend/src/lib/listsStore.svelte.ts
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-17 14:32:37 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-17 14:32:37 +0800
commit5c0ba37eda80d39e6ceca59bb1d5f4942f858995 (patch)
tree948723f9cedf7ccb0707fa6ee516bd30fe20fd10 /frontend/src/lib/listsStore.svelte.ts
downloadstrategy-lab-5c0ba37eda80d39e6ceca59bb1d5f4942f858995.tar.gz
chore: establish Strategy Lab source baseline (development, not release)
Diffstat (limited to 'frontend/src/lib/listsStore.svelte.ts')
-rw-r--r--frontend/src/lib/listsStore.svelte.ts92
1 files changed, 92 insertions, 0 deletions
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<string, boolean> = $state({});
+let inflight = new Map<string, Promise<Project | null>>();
+
+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<Project | null>;
+ bump(p: Project): void;
+ /** reactive lookup: reads only the $state projects array */
+ get(id: string): Project | undefined;
+ load(): Promise<Project[]>;
+ /** 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];
+}