import { HABITAT_VAULT_ROOT_COLLECTION } from '../habitat/config'; import type { HabitatDoc } from '../habitat/types'; import { listPrivateRecords } from '../habitat/xrpc'; import type { HabitatUri, VaultConfig } from './types'; /** * Unified vault model. A vault is one of three states: * * - `local` — a local-only directory (no PDS sync). * - `synced` — a PDS-backed vault cloned into a local directory. * - `remote-unsynced` — a PDS-backed vault that exists remotely but hasn't been * cloned locally yet. * * `local`/`synced` are "mounted" (present locally, persisted in the registry). * `remote-unsynced` is discovered from the PDS and never persisted. */ export interface LocalVault { state: 'local'; /** Stable id derived from the local path. */ id: string; name: string; /** Absolute filesystem path to the vault root. */ path: string; } export interface SyncedVault { state: 'synced'; id: string; name: string; path: string; /** DID of the owning Atmosphere account. */ did: string; /** Habitat URI of the root directory record. */ rootUri: HabitatUri; } export interface RemoteVault { state: 'remote-unsynced'; name: string; did: string; rootUri: HabitatUri; } /** A vault present locally and persisted in the registry. */ export type MountedVault = LocalVault | SyncedVault; /** Any vault the app knows about (mounted or discoverable remotely). */ export type Vault = MountedVault | RemoteVault; /** Project a synced/remote vault to the engine's input contract. */ export function toVaultConfig(vault: SyncedVault | RemoteVault): VaultConfig { return { type: 'synced', rootUri: vault.rootUri }; } const STORAGE_KEY = 'textile.openVaults'; /** * Owns the catalog of vaults: the persisted set of mounted (local/synced) * vaults, plus discovery of remote vault roots from the PDS. */ export class VaultRegistry { private vaults: MountedVault[] = loadPersisted(); private listeners = new Set<() => void>(); /** Vaults present locally (local + synced). */ getMounted(): MountedVault[] { return this.vaults; } add(vault: MountedVault): void { this.vaults = [...this.vaults, vault]; this.persist(); this.emit(); } remove(id: string): void { this.vaults = this.vaults.filter((v) => v.id !== id); this.persist(); this.emit(); } subscribe(listener: () => void): () => void { this.listeners.add(listener); return () => { this.listeners.delete(listener); }; } /** Stable id derived from a vault's local path. */ idFromPath(path: string): string { let hash = 0; for (let i = 0; i < path.length; i++) { const char = path.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; } return `vault-${Math.abs(hash).toString(36)}`; } /** * List the account's vault roots from the PDS. Vault roots live in their own * collection, so this is a single unfiltered query. */ async listRemoteRoots(did: string): Promise { const { records } = await listPrivateRecords( HABITAT_VAULT_ROOT_COLLECTION, { subjects: [did] }, ); return records.map((record) => { const fallback = record.uri.split('/').filter(Boolean).pop() ?? record.uri; const name = record.value.name?.trim() ? record.value.name : fallback; return { state: 'remote-unsynced', name, did, rootUri: record.uri as HabitatUri, }; }); } /** Remote roots not yet synced into a local directory. */ static unsynced( remote: RemoteVault[], mounted: MountedVault[], ): RemoteVault[] { const syncedRoots = new Set( mounted .filter((v): v is SyncedVault => v.state === 'synced') .map((v) => v.rootUri), ); return remote.filter((r) => !syncedRoots.has(r.rootUri)); } private persist(): void { if (typeof localStorage === 'undefined') return; try { localStorage.setItem(STORAGE_KEY, JSON.stringify(this.vaults)); } catch { // Storage may be unavailable. } } private emit(): void { for (const listener of this.listeners) listener(); } } function loadPersisted(): MountedVault[] { if (typeof localStorage === 'undefined') return []; try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return []; const parsed = JSON.parse(raw); return Array.isArray(parsed) ? (parsed as MountedVault[]) : []; } catch { return []; } } /** App-wide singleton. */ export const vaultRegistry = new VaultRegistry();