📄 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