From ce5c164958836f1eaaa349a70fd9622fd5b707fc Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Wed, 1 Apr 2026 17:58:17 +0200 Subject: [PATCH] feat(web): add federation registry state, syncRegistry, and helper actions to instanceStore --- packages/web/src/stores/instanceStore.ts | 77 +++++++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/packages/web/src/stores/instanceStore.ts b/packages/web/src/stores/instanceStore.ts index 2f802181..5d5e378c 100644 --- a/packages/web/src/stores/instanceStore.ts +++ b/packages/web/src/stores/instanceStore.ts @@ -1,5 +1,5 @@ import { create } from 'zustand'; -import type { User, InstanceInfoResponse, ReplicatedInstance, AuthResponse } from '@backspace/shared'; +import type { User, InstanceInfoResponse, ReplicatedInstance, AuthResponse, FederationRegistryEntry } from '@backspace/shared'; import { BackspaceApiClient, createApiClient, api } from '../api/client'; import { useAuthStore } from './authStore'; import { setApiForOriginResolver, setUserIdForOriginResolver, setOriginFromHostnameResolver, useSpaceStore } from './spaceStore'; @@ -9,6 +9,7 @@ import { syncProfileToRemote } from '../utils/profileSync'; // Safe because both modules access each other lazily (at call time, not import time). // clearPasswordSyncTimers itself does not reference useInstanceStore. import { clearPasswordSyncTimers } from '../utils/federationOps'; +import { useUIStore } from './uiStore'; // ─── Types ─────────────────────────────────────────────────────────────────── @@ -118,6 +119,33 @@ function normalizeOrigin(url: string): string { // ─── API client resolution ─────────────────────────────────────────────────── +// ─── Registry helpers ──────────────────────────────────────────────────────── + +function upsertRegistryEntry( + registry: Map, + origin: string, + updates: Partial & { origin: string }, +): Map { + const next = new Map(registry); + const existing = next.get(origin); + if (existing) { + next.set(origin, { ...existing, ...updates }); + } else { + next.set(origin, { + label: '', + username: '', + remoteUserId: '', + status: 'connected', + addedAt: Date.now(), + lastConnectedAt: null, + disconnectedAt: null, + errorMessage: null, + ...updates, + }); + } + return next; +} + // ─── Store ─────────────────────────────────────────────────────────────────── interface InstanceState { @@ -126,6 +154,11 @@ interface InstanceState { error: string | null; _autoConnectDone: boolean; pendingSyncOrigins: string[]; + registry: Map; + registryUpdatedAt: number; + syncRegistry: () => Promise; + deleteIdentity: (origin: string) => void; + forceRemoveEntry: (origin: string) => void; probeInstance: (url: string) => Promise; connectToRemote: (origin: string, password: string, displayName?: string) => Promise; @@ -148,6 +181,8 @@ export const useInstanceStore = create((set, get) => ({ error: null, _autoConnectDone: false, pendingSyncOrigins: [], + registry: new Map(), + registryUpdatedAt: 0, probeInstance: async (url: string) => { const origin = normalizeOrigin(url); @@ -535,6 +570,44 @@ export const useInstanceStore = create((set, get) => ({ await Promise.all([homePromise, ...remotePromises]); }, + syncRegistry: async () => { + if (!get()._autoConnectDone) return; + + const { registry, registryUpdatedAt, instances } = get(); + const currentUser = useAuthStore.getState().user; + if (!currentUser) return; + + const entries = Array.from(registry.values()); + const payload = { registry: entries, updatedAt: registryUpdatedAt }; + + // Push to home instance + const homePromise = api.users.putFederationRegistry(payload).catch((err) => { + console.warn('Failed to sync registry to home:', err); + }); + + // Push to all connected remote instances + const connectedInstances = instances.filter(i => i.status === 'connected'); + const remotePromises = connectedInstances.map(inst => + inst.api.users.putFederationRegistry(payload).catch((err) => { + console.warn(`Failed to sync registry to ${inst.origin}:`, err); + }) + ); + + await Promise.all([homePromise, ...remotePromises]); + }, + + deleteIdentity: (_origin: string) => { + useUIStore.getState().addToast('Identity deletion is not yet implemented', 'info', 3000); + }, + + forceRemoveEntry: (origin: string) => { + const registry = new Map(get().registry); + registry.delete(origin); + const registryUpdatedAt = Date.now(); + set({ registry, registryUpdatedAt }); + get().syncRegistry().catch(() => {}); + }, + autoConnectAll: async () => { const currentUser = useAuthStore.getState().user; if (!currentUser || currentUser.replicatedInstances.length === 0) { @@ -712,7 +785,7 @@ export const useInstanceStore = create((set, get) => ({ clearPasswordSyncTimers(); - set({ instances: [], isLoading: false, error: null, _autoConnectDone: false, pendingSyncOrigins: [] }); + set({ instances: [], isLoading: false, error: null, _autoConnectDone: false, pendingSyncOrigins: [], registry: new Map(), registryUpdatedAt: 0 }); // Token cache preserved — scoped per user, survives logout for seamless reconnect }, }));