📄 src/web3/session/projects.ts
D-OPEN SOVEREIGN
1import type { Project, ProjectVersion } from '../client/types';
2import { getChain } from '../network/chains';
3import { getReadClient } from '../client/registry';
4import { getItem, setItem } from './storage';
5
6export interface ProjectLocale {
7  id: number;
8  lang: string;
9  version: ProjectVersion;
10  balance: number;
11}
12
13export const projectLocale = (project: Project, userLanguage: string, chainId: number): ProjectLocale => {
14  const { decimals } = getChain(chainId);
15  const balance = project.balance / 10 ** decimals;
16
17  const projectLangIndex = project.availableLanguages.findIndex((lang) => lang === userLanguage);
18  if (projectLangIndex !== -1) {
19    return {
20      lang: project.availableLanguages[projectLangIndex],
21      version: { ...project.versions[projectLangIndex] },
22      id: project.id,
23      balance,
24    };
25  }
26
27  // for now, selects English as the default language.
28  // TODO: add a language selector to select a set of default languages
29  const englishIndex = project.availableLanguages.findIndex((lang) => lang === ' eng');
30  if (englishIndex !== -1) {
31    return {
32      lang: ' eng',
33      version: { ...project.versions[englishIndex] },
34      id: project.id,
35      balance,
36    };
37  }
38
39  if (project.availableLanguages.length === 0) {
40    throw new Error(`No language available for project ${project.id}`);
41  }
42  return {
43    lang: project.availableLanguages[0],
44    version: { ...project.versions[0] },
45    id: project.id,
46    balance,
47  };
48};
49
50class ProjectRegistry {
51  private projectKey(chainId: number, language: string, projectId: number): string {
52    return `${chainId}_${language}_${projectId}_v2`;
53  }
54
55  private nbProjectsKey(chainId: number, language: string): string {
56    return `${chainId}_${language}_nbProjects_v2`;
57  }
58
59  private nbProjects(chainId: number, language: string): number {
60    const nb = getItem<string>(this.nbProjectsKey(chainId, language));
61    return nb ? parseInt(nb, 10) : 0;
62  }
63
64  private setNbProjects(chainId: number, language: string, nb: number): void {
65    setItem(this.nbProjectsKey(chainId, language), nb.toString());
66  }
67
68  private getProject(chainId: number, language: string, projectId: number): ProjectLocale | null {
69    return getItem<ProjectLocale>(this.projectKey(chainId, language, projectId));
70  }
71
72  private cacheProject(chainId: number, language: string, project: ProjectLocale): void {
73    setItem(this.projectKey(chainId, language, project.id), project);
74  }
75
76  // Preserves position (null for a missing entry) so callers can index this
77  // the same way as loadProjects()'s online path: result[i] is always project
78  // (i+1), whether the data came from a live fetch or this offline fallback.
79  getCachedProjects(chainId: number, language: string): Array<ProjectLocale | null> {
80    const nbProjects = this.nbProjects(chainId, language);
81    if (nbProjects <= 0) return [];
82    const projects: Array<ProjectLocale | null> = [];
83    for (let i = 1; i <= nbProjects; i++) {
84      projects.push(this.getProject(chainId, language, i));
85    }
86    return projects;
87  }
88
89  async loadProject(chainId: number, language: string, projectId: number, refresh = false): Promise<ProjectLocale> {
90    const cached = this.getProject(chainId, language, projectId);
91    if (cached !== null && !refresh) {
92      return cached;
93    }
94
95    const client = getReadClient(chainId);
96    await client.ready;
97
98    const loaded = projectLocale(await client.projectManagerStorage.loadProject(projectId), language, chainId);
99    this.cacheProject(chainId, language, loaded);
100    return loaded;
101  }
102
103  async loadProjects(chainId: number, language: string): Promise<Array<ProjectLocale | null>> {
104    let client;
105    try {
106      client = getReadClient(chainId);
107    } catch (e) {
108      console.warn(`[ProjectRegistry] No read client for chain ${chainId} — returning cached projects only.`, e);
109      return this.getCachedProjects(chainId, language);
110    }
111    await client.ready;
112
113    const cachedNbProjects = this.nbProjects(chainId, language);
114    const liveNbProjects = await client.projectManagerStorage.nbProjects();
115
116    if (liveNbProjects !== cachedNbProjects) {
117      this.setNbProjects(chainId, language, liveNbProjects);
118    }
119
120    const range = Array.from({ length: liveNbProjects }, (_, i) => i + 1);
121    return Promise.all(
122      range.map((id) =>
123        this.loadProject(chainId, language, id).catch((e: any) => {
124          console.error(`[ProjectRegistry] Error loading project ${id}:`, e);
125          return null;
126        })
127      )
128    );
129  }
130}
131
132const projectRegistry = new ProjectRegistry();
133
134export const loadProjects = (chainId: number, language: string): Promise<Array<ProjectLocale | null>> =>
135  projectRegistry.loadProjects(chainId, language);
136
137export const loadProject = (chainId: number, language: string, projectId: number, refresh = false): Promise<ProjectLocale> =>
138  projectRegistry.loadProject(chainId, language, projectId, refresh);
139