From 05d8285f9d454d0d37113096bf98cf85ffc19647 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:03:59 +0200 Subject: [PATCH] feat(web): hydrate federation registry from server in autoConnectAll with localStorage migration --- packages/web/src/stores/instanceStore.ts | 78 +++++++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/packages/web/src/stores/instanceStore.ts b/packages/web/src/stores/instanceStore.ts index ae95a621..366c8d10 100644 --- a/packages/web/src/stores/instanceStore.ts +++ b/packages/web/src/stores/instanceStore.ts @@ -664,13 +664,54 @@ export const useInstanceStore = create((set, get) => ({ autoConnectAll: async () => { const currentUser = useAuthStore.getState().user; - if (!currentUser || currentUser.replicatedInstances.length === 0) { + if (!currentUser) { set({ _autoConnectDone: true }); return; } const cached = loadCachedTokens(currentUser.id); + // Fetch server-side registry (source of truth for entry list) + let serverRegistry: FederationRegistryEntry[] = []; + let serverRegistryUpdatedAt = 0; + try { + const res = await api.users.getFederationRegistry(); + serverRegistry = res.registry; + serverRegistryUpdatedAt = res.updatedAt; + } catch (err) { + console.warn('Failed to fetch federation registry from home:', err); + } + + // Initialize registry from server data + const registry = new Map(); + for (const entry of serverRegistry) { + registry.set(entry.origin, entry); + } + + // Migration: promote localStorage-only entries to registry + for (const [origin] of Object.entries(cached)) { + if (origin === window.location.origin) continue; + if (!registry.has(origin)) { + registry.set(origin, { + origin, + label: cached[origin]?.label || new URL(origin).host, + username: cached[origin]?.username || '', + remoteUserId: '', + status: 'connected', + addedAt: Date.now(), + lastConnectedAt: Date.now(), + disconnectedAt: null, + errorMessage: null, + }); + } + } + + // Early return if there's nothing to connect + if (currentUser.replicatedInstances.length === 0 && registry.size === 0) { + set({ _autoConnectDone: true }); + return; + } + // Split server-known instances into two groups: // - withToken: have a cached token → attempt reconnection // - withoutToken: no cached token → add as error placeholder @@ -708,6 +749,14 @@ export const useInstanceStore = create((set, get) => ({ }); } + // Update registry for tokenless placeholders + for (const { origin } of withoutToken) { + const entry = registry.get(origin); + if (entry) { + registry.set(origin, { ...entry, status: 'auth_expired', errorMessage: 'Session expired — re-authenticate to reconnect' }); + } + } + // Connect instances with cached tokens in parallel if (withToken.length > 0) { const results = await Promise.allSettled( @@ -771,6 +820,12 @@ export const useInstanceStore = create((set, get) => ({ instances: state.instances.map(i => i.origin === origin ? connectedInstance : i), })); + // Update registry entry on successful reconnect + const entry = registry.get(origin); + if (entry) { + registry.set(origin, { ...entry, status: 'connected', lastConnectedAt: Date.now(), disconnectedAt: null, errorMessage: null, remoteUserId: user.id, label }); + } + // Open WebSocket connection now that we've verified the token connectInstance(origin, cachedEntry.token); @@ -791,6 +846,13 @@ export const useInstanceStore = create((set, get) => ({ : i ), })); + + // Update registry entry on network error + const entry = registry.get(origin); + if (entry) { + registry.set(origin, { ...entry, status: 'unreachable', errorMessage: 'Instance unreachable' }); + } + // Start WebSocket — its built-in exponential backoff retry will auto-recover // when the network path becomes available (e.g. user switches networks) connectInstance(origin, cachedEntry.token); @@ -803,6 +865,12 @@ export const useInstanceStore = create((set, get) => ({ : i ), })); + + // Update registry entry on auth error + const entry = registry.get(origin); + if (entry) { + registry.set(origin, { ...entry, status: 'auth_expired', errorMessage: 'Token expired' }); + } } } }) @@ -824,7 +892,13 @@ export const useInstanceStore = create((set, get) => ({ const pendingOrigins = Object.entries(freshCached) .filter(([, v]) => v.pendingPasswordSync) .map(([origin]) => origin); - set({ _autoConnectDone: true, pendingSyncOrigins: pendingOrigins }); + + // Persist reconciled registry + const registryUpdatedAt = serverRegistryUpdatedAt > 0 ? Math.max(serverRegistryUpdatedAt, Date.now()) : Date.now(); + set({ _autoConnectDone: true, pendingSyncOrigins: pendingOrigins, registry, registryUpdatedAt }); + + // Sync reconciled registry to all instances + get().syncRegistry().catch(() => {}); }, reset: () => {