import { type OrbitDB, createOrbitDB, KeyValueDatabase, OpenDatabaseOptions, } from "@orbitdb/core"; import { v4 as uuidv4 } from "uuid"; import { type FeedDatabaseType, registerFeed } from "@orbitdb/feed-db"; import { type OrderedKeyValueDatabaseType, registerOrderedKeyValue, } from "@orbitdb/ordered-keyvalue-db"; import { type SetDatabaseType, registerSet } from "@orbitdb/set-db"; import { typedFeed, TypedFeed, typedKeyValue, TypedKeyValue, typedOrderedKeyValue, TypedOrderedKeyValue, typedSet, TypedSet, } from "@constl/bohr-db"; import { type JSONSchemaType } from "ajv"; import { Semaphore } from "@chriscdn/promise-semaphore"; import { anySignal } from "any-signal"; import { type ServiceMap } from "@libp2p/interface"; import { AbortError } from "p-retry"; import { enregistrerContrôleurs } from "@/accès/index.js"; import type { schémaFonctionOublier, élémentsBd } from "./types.js"; import type { HeliaLibp2p } from "helia"; import type { Libp2p } from "libp2p"; import type { ServicesLibp2p } from "./sfip/index.js"; const pSignal = async (signal: AbortSignal): Promise => { if (signal.aborted) throw new AbortError(Error("Signal avorté")); return new Promise((_résoudre, rejeter) => { const lorsquAvorté = () => { signal.removeEventListener("abort", lorsquAvorté); rejeter(); }; signal.addEventListener("abort", lorsquAvorté); }); }; export const réessayer = async ({ f, signal, }: { f: () => Promise; signal: AbortSignal; }): Promise => { let avant = Date.now(); const _interne = async ({ f, signal, n, }: { f: () => Promise; signal: AbortSignal; n: number; }): Promise => { try { avant = Date.now(); return await Promise.race([f(), pSignal(signal)]); } catch (e) { if (signal.aborted) throw new AbortError(Error("Signal avorté")); console.log(e); n++; const maintenant = Date.now(); const tempsÀAttendre = n * 1000 - (maintenant - avant); if (tempsÀAttendre > 0) { await new Promise((résoudre) => { const chrono = setTimeout(résoudre, tempsÀAttendre); signal.addEventListener("abort", () => { clearInterval(chrono); résoudre(); }); }); if (signal.aborted) throw new AbortError(Error("Signal avorté")); } return await _interne({ f, signal, n }); } }; return await _interne({ f, signal, n: 0 }); }; export type Store = | FeedDatabaseType | SetDatabaseType | KeyValueDatabase | OrderedKeyValueDatabaseType; type TypesBdsOrbites = { keyvalue: KeyValueDatabase; feed: FeedDatabaseType; set: SetDatabaseType; "ordered-keyvalue": OrderedKeyValueDatabaseType; }; // https://stackoverflow.com/questions/56863875/typescript-how-do-you-filter-a-types-properties-to-those-of-a-certain-type type KeysMatching = { [K in keyof T]-?: T[K] extends V ? K : never; }[keyof T]; export const préparerOrbite = () => { registerFeed(); registerSet(); registerOrderedKeyValue(); enregistrerContrôleurs(); }; export async function initOrbite({ sfip, dossierOrbite, }: { sfip: HeliaLibp2p>; dossierOrbite: string; }): Promise> { préparerOrbite(); const orbite = await createOrbitDB({ ipfs: sfip, id: "constellation", directory: dossierOrbite, }); return orbite; } type Typer< T extends Store, U extends T extends KeyValueDatabase | OrderedKeyValueDatabaseType ? { [clef: string]: élémentsBd } : élémentsBd, > = T extends KeyValueDatabase ? TypedKeyValue> : T extends FeedDatabaseType ? TypedFeed : T extends SetDatabaseType ? TypedSet : T extends OrderedKeyValueDatabaseType ? TypedOrderedKeyValue> : never; const typerBd = < T extends Store, U extends T extends KeyValueDatabase | OrderedKeyValueDatabaseType ? { [clef: string]: élémentsBd } : élémentsBd, >({ bd, schéma, }: { bd: T; schéma: JSONSchemaType; }): Typer => { switch (bd.type) { case "feed": return typedFeed({ db: bd, schema: schéma as JSONSchemaType, }) as Typer; case "set": return typedSet({ db: bd, schema: schéma as JSONSchemaType, }) as unknown as Typer; case "keyvalue": return typedKeyValue({ db: bd, // @ts-expect-error Je ne sais pas pourquoi schema: schéma as JSONSchemaType, }) as unknown as Typer; case "ordered-keyvalue": return typedOrderedKeyValue({ db: bd, // @ts-expect-error Je ne sais pas pourquoi schema: schéma as JSONSchemaType, }) as unknown as Typer; default: throw new Error("Type de bd non reconnu."); } }; type bdOuverte = { bd: T; idsRequêtes: Set }; export class GestionnaireOrbite { orbite: OrbitDB; _bdsOrbite: { [key: string]: bdOuverte }; verrouOuvertureBd: Semaphore; _oublierNettoyageBdsOuvertes?: schémaFonctionOublier; signaleurArrêt: AbortController; constructor({ orbite, signaleurArrêt, }: { orbite: OrbitDB; signaleurArrêt?: AbortController; }) { this.orbite = orbite; this._bdsOrbite = {}; this.verrouOuvertureBd = new Semaphore(); this._oublierNettoyageBdsOuvertes = this.lancerNettoyageBdsOuvertes(); this.signaleurArrêt = signaleurArrêt || new AbortController(); } get identity(): OrbitDB["identity"] { return this.orbite.identity; } async ouvrirBd({ id, type, signal, options, }: { id: string; type: "keyvalue"; signal?: AbortSignal; options?: Omit; }): Promise<{ bd: T; fOublier: schémaFonctionOublier }>; async ouvrirBd({ id, type, signal, options, }: { id: string; type: "feed"; signal?: AbortSignal; options?: Omit; }): Promise<{ bd: T; fOublier: schémaFonctionOublier }>; async ouvrirBd({ id, type, signal, options, }: { id: string; type: "set"; signal?: AbortSignal; options?: Omit; }): Promise<{ bd: T; fOublier: schémaFonctionOublier }>; async ouvrirBd({ id, type, signal, options, }: { id: string; type: "ordered-keyvalue"; signal?: AbortSignal; options?: Omit; }): Promise<{ bd: T; fOublier: schémaFonctionOublier }>; async ouvrirBd({ id, signal, }: { id: string; signal?: AbortSignal; options?: Omit; }): Promise<{ bd: T; fOublier: schémaFonctionOublier }>; async ouvrirBd({ id, type, signal, options, }: { id: string; type?: "keyvalue" | "feed" | "set" | "ordered-keyvalue"; signal?: AbortSignal; options?: Omit; }): Promise<{ bd: T; fOublier: schémaFonctionOublier }>; async ouvrirBd({ id, type, signal, options, }: { id: string; type?: "keyvalue" | "feed" | "set" | "ordered-keyvalue"; signal?: AbortSignal; options?: Omit; }): Promise<{ bd: T; fOublier: schémaFonctionOublier; }> { const signalCombiné = anySignal([ this.signaleurArrêt.signal, ...(signal ? [signal] : []), ]); // Nous avons besoin d'un verrou afin d'éviter la concurrence await this.verrouOuvertureBd.acquire(id); try { const existante = this._bdsOrbite[id]; const idRequête = uuidv4(); const fOublier = async () => { // Si la BD a été effacée entre-temps par `orbite.effacerBd`, // elle ne sera plus disponible ici this._bdsOrbite[id]?.idsRequêtes.delete(idRequête); }; // Fonction utilitaire pour vérifier le type de la bd const vérifierTypeBd = ( bd: Store, type: U, stricte = true, ): bd is TypesBdsOrbites[U] => { const { type: typeBd } = bd; return ( typeBd === type || (!stricte && typeBd === "keyvalue" && type === "ordered-keyvalue") ); }; if (existante) { this._bdsOrbite[id].idsRequêtes.add(idRequête); this.verrouOuvertureBd.release(id); if (type && !vérifierTypeBd(existante.bd, type, false)) throw new Error( // @ts-expect-error Je ne comprends pas complètement `La bd est de type ${existante.bd.type}, et non ${type}.`, ); this.verrouOuvertureBd.release(id); return { bd: existante.bd as T, fOublier, }; } const bd = await réessayer({ f: async (): Promise => (await this.orbite.open(id, { type, ...options, })) as T, signal: signalCombiné, }); bd.events.on("error", console.log); signalCombiné.clear(); this._bdsOrbite[id] = { bd, idsRequêtes: new Set([idRequête]) }; // Maintenant que la BD a été créée, on peut relâcher le verrou this.verrouOuvertureBd.release(id); return { bd, fOublier, }; } finally { this.verrouOuvertureBd.release(id); } } async ouvrirBdTypée< U extends { [clef: string]: élémentsBd }, T = TypedKeyValue, >({ id, type, schéma, signal, options, }: { id: string; type: "keyvalue"; schéma: JSONSchemaType; signal?: AbortSignal; options?: Omit; }): Promise<{ bd: T; fOublier: schémaFonctionOublier }>; async ouvrirBdTypée>({ id, type, schéma, signal, options, }: { id: string; type: "feed"; schéma: JSONSchemaType; signal?: AbortSignal; options?: Omit; }): Promise<{ bd: T; fOublier: schémaFonctionOublier }>; async ouvrirBdTypée>({ id, type, schéma, signal, options, }: { id: string; type: "set"; schéma: JSONSchemaType; signal?: AbortSignal; options?: Omit; }): Promise<{ bd: T; fOublier: schémaFonctionOublier }>; async ouvrirBdTypée< U extends { [clef: string]: élémentsBd }, T = TypedOrderedKeyValue, >({ id, type, schéma, signal, options, }: { id: string; type: "ordered-keyvalue"; schéma: JSONSchemaType; signal?: AbortSignal; options?: Omit; }): Promise<{ bd: T; fOublier: schémaFonctionOublier }>; async ouvrirBdTypée({ id, type, schéma, signal, options, }: { id: string; type: "ordered-keyvalue" | "set" | "keyvalue" | "feed"; schéma: JSONSchemaType; signal?: AbortSignal; options?: Omit; }): Promise<{ bd: T; fOublier: schémaFonctionOublier }> { const { bd, fOublier } = await this.ouvrirBd({ id, type, signal, options, }); return { bd: typerBd({ bd, schéma }) as T, fOublier, }; } async créerBdIndépendante({ type, options, nom, }: { type: "set" | "ordered-keyvalue" | "keyvalue" | "feed"; options: Omit; nom?: string; }): Promise { const bd = (await this.orbite.open(nom || uuidv4(), { type, ...options, })) as Store; const { address } = bd; bd.events.on("error", console.log); this._bdsOrbite[address] = { bd, idsRequêtes: new Set() }; return address; } async effacerBd({ id }: { id: string }): Promise { const { bd } = await this.ouvrirBd({ id }); await bd.drop(); delete this._bdsOrbite[id]; } private lancerNettoyageBdsOuvertes(): schémaFonctionOublier { const fNettoyer = async () => { /* await Promise.allSettled( Object.keys(this._bdsOrbite).map(async (id) => { const { bd, idsRequêtes } = this._bdsOrbite[id]; if (!idsRequêtes.size) { delete this._bdsOrbite[id]; await bd.close(); } }), ); */ }; const i = setInterval(fNettoyer, 1000 * 60 * 5); return async () => clearInterval(i); } async appliquerFonctionBdOrbite< T extends KeysMatching unknown>, >({ idBd, fonction, args, }: { idBd: string; fonction: T; args: Parameters; }): Promise> { const { bd, fOublier } = await this.ouvrirBd({ id: idBd }); const résultat = await bd[fonction](...(args as unknown[])); await fOublier(); return résultat; } async fermer({ arrêterOrbite }: { arrêterOrbite: boolean }): Promise { this.signaleurArrêt.abort(); if (this._oublierNettoyageBdsOuvertes) await this._oublierNettoyageBdsOuvertes(); if (arrêterOrbite) { await this.orbite.stop(); } } } export class GestionnaireOrbiteGénéral { gestionnaires: { [idOrbite: string]: GestionnaireOrbite }; constructor() { this.gestionnaires = {}; } obtGestionnaireOrbite({ orbite, signaleurArrêt, }: { orbite: OrbitDB; signaleurArrêt?: AbortController; }): GestionnaireOrbite { if (!this.gestionnaires[orbite.identity.id]) { this.gestionnaires[orbite.identity.id] = new GestionnaireOrbite({ orbite, signaleurArrêt, }); } return this.gestionnaires[orbite.identity.id]; } async fermer({ orbite, arrêterOrbite, signaleurArrêt, }: { orbite: OrbitDB; arrêterOrbite: boolean; signaleurArrêt: AbortController; }): Promise { const gestionnaireOrbite = this.obtGestionnaireOrbite({ orbite, signaleurArrêt, }); await gestionnaireOrbite.fermer({ arrêterOrbite }); delete this.gestionnaires[orbite.identity.id]; } } export const gestionnaireOrbiteGénéral = new GestionnaireOrbiteGénéral();