fix(web): critical fixes — preserve tokens on disconnect, clean up forceRemoveEntry, update registry on reconnect failure

- disconnectInstance: save cached tokens BEFORE filtering the instance out, so
  reconnectInstance can restore disconnected entries from localStorage
- reconnectInstance: if instance is not in active instances array, attempt to
  restore it from cached localStorage token before proceeding with reconnect
- reconnectInstance: update registry to unreachable/auth_expired on failure
- forceRemoveEntry: now tears down WS, removes from instances array, purges
  token from localStorage, and cleans up space store (was registry-only before)
This commit is contained in:
Jannis Braun
2026-04-01 18:20:53 +02:00
parent ad879fc077
commit 7135990340
+77 -9
View File
@@ -437,12 +437,17 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
}); });
const registryUpdatedAt = Date.now(); const registryUpdatedAt = Date.now();
set((state) => { // Save the token BEFORE filtering it out of instances — this preserves
const updated = state.instances.filter(i => i.origin !== origin); // the cached token in localStorage so reconnectInstance can restore it later.
const userId = useAuthStore.getState().user?.id; const currentInstances = get().instances;
if (userId) saveCachedTokens(updated, userId); const userId = useAuthStore.getState().user?.id;
return { instances: updated, registry, registryUpdatedAt }; if (userId) saveCachedTokens(currentInstances, userId);
});
set((state) => ({
instances: state.instances.filter(i => i.origin !== origin),
registry,
registryUpdatedAt,
}));
// Remove spaces from this instance from the space store // Remove spaces from this instance from the space store
useSpaceStore.getState().removeInstanceSpaces(origin); useSpaceStore.getState().removeInstanceSpaces(origin);
@@ -453,8 +458,39 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
}, },
reconnectInstance: async (origin: string) => { reconnectInstance: async (origin: string) => {
const inst = get().instances.find(i => i.origin === origin); let inst = get().instances.find(i => i.origin === origin);
if (!inst || inst.status === 'connected' || inst.status === 'connecting') return;
// If the instance was disconnected (removed from active instances array) but
// has a cached token in localStorage, restore it so reconnect can proceed.
if (!inst) {
const userId = useAuthStore.getState().user?.id;
if (!userId) return;
const cached = loadCachedTokens(userId);
const entry = cached[origin];
if (!entry?.token) return; // No cached token — needs full re-authentication via connectToRemote
const currentUser = useAuthStore.getState().user;
if (!currentUser) return;
const client = createApiClient(origin, () => entry.token);
const restoredInstance: ConnectedInstance = {
origin,
label: entry.label || new URL(origin).host,
token: entry.token,
user: currentUser,
username: entry.username || '',
status: 'connecting' as const,
api: client,
};
set((state) => ({
instances: [...state.instances, restoredInstance],
}));
inst = restoredInstance;
}
if (inst.status === 'connected' || inst.status === 'connecting') return;
// Tokenless placeholders can't reconnect — they need full re-authentication // Tokenless placeholders can't reconnect — they need full re-authentication
if (!inst.token) return; if (!inst.token) return;
@@ -497,6 +533,15 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
: i : i
), ),
})); }));
// Update registry on network error
const errRegistry = upsertRegistryEntry(get().registry, origin, {
origin,
status: 'unreachable',
errorMessage: 'Instance unreachable',
});
set({ registry: errRegistry, registryUpdatedAt: Date.now() });
connectInstance(origin, inst.token); connectInstance(origin, inst.token);
} else { } else {
set((state) => ({ set((state) => ({
@@ -506,6 +551,14 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
: i : i
), ),
})); }));
// Update registry on auth error
const errRegistry = upsertRegistryEntry(get().registry, origin, {
origin,
status: 'auth_expired',
errorMessage: 'Token expired',
});
set({ registry: errRegistry, registryUpdatedAt: Date.now() });
} }
} }
}, },
@@ -655,10 +708,25 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
}, },
forceRemoveEntry: (origin: string) => { forceRemoveEntry: (origin: string) => {
// Tear down WebSocket if connected
disconnectWs(origin);
// Remove from registry
const registry = new Map(get().registry); const registry = new Map(get().registry);
registry.delete(origin); registry.delete(origin);
const registryUpdatedAt = Date.now(); const registryUpdatedAt = Date.now();
set({ registry, registryUpdatedAt });
// Remove from instances and purge token from localStorage
set((state) => {
const updated = state.instances.filter(i => i.origin !== origin);
const userId = useAuthStore.getState().user?.id;
if (userId) saveCachedTokens(updated, userId);
return { instances: updated, registry, registryUpdatedAt };
});
// Clean up spaces belonging to this instance
useSpaceStore.getState().removeInstanceSpaces(origin);
get().syncRegistry().catch(() => {}); get().syncRegistry().catch(() => {});
}, },