summaryrefslogtreecommitdiff
path: root/frontend/src/lib/listsStore.svelte.ts
blob: 102543f69ff08066df030d9e7cd1b8ff6c10238f (plain)
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];
}