1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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];
}
|