feat(web): hydrate federation registry from server in autoConnectAll with localStorage migration

This commit is contained in:
Jannis Braun
2026-04-01 18:03:59 +02:00
parent c22d568605
commit 05d8285f9d
+76 -2
View File
@@ -664,13 +664,54 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
autoConnectAll: async () => { autoConnectAll: async () => {
const currentUser = useAuthStore.getState().user; const currentUser = useAuthStore.getState().user;
if (!currentUser || currentUser.replicatedInstances.length === 0) { if (!currentUser) {
set({ _autoConnectDone: true }); set({ _autoConnectDone: true });
return; return;
} }
const cached = loadCachedTokens(currentUser.id); 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<string, FederationRegistryEntry>();
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: // Split server-known instances into two groups:
// - withToken: have a cached token → attempt reconnection // - withToken: have a cached token → attempt reconnection
// - withoutToken: no cached token → add as error placeholder // - withoutToken: no cached token → add as error placeholder
@@ -708,6 +749,14 @@ export const useInstanceStore = create<InstanceState>((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 // Connect instances with cached tokens in parallel
if (withToken.length > 0) { if (withToken.length > 0) {
const results = await Promise.allSettled( const results = await Promise.allSettled(
@@ -771,6 +820,12 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
instances: state.instances.map(i => i.origin === origin ? connectedInstance : i), 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 // Open WebSocket connection now that we've verified the token
connectInstance(origin, cachedEntry.token); connectInstance(origin, cachedEntry.token);
@@ -791,6 +846,13 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
: i : 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 // Start WebSocket — its built-in exponential backoff retry will auto-recover
// when the network path becomes available (e.g. user switches networks) // when the network path becomes available (e.g. user switches networks)
connectInstance(origin, cachedEntry.token); connectInstance(origin, cachedEntry.token);
@@ -803,6 +865,12 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
: i : 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<InstanceState>((set, get) => ({
const pendingOrigins = Object.entries(freshCached) const pendingOrigins = Object.entries(freshCached)
.filter(([, v]) => v.pendingPasswordSync) .filter(([, v]) => v.pendingPasswordSync)
.map(([origin]) => origin); .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: () => { reset: () => {