146 lines
4.1 KiB
TypeScript
146 lines
4.1 KiB
TypeScript
import { SessionProvider } from "@/contexts/session-context";
|
|
import {
|
|
useDaemonRegistry,
|
|
type HostProfile,
|
|
} from "@/contexts/daemon-registry-context";
|
|
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
|
import { buildDaemonWebSocketUrl, buildRelayWebSocketUrl } from "@/utils/daemon-endpoints";
|
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
|
|
function buildCandidateUrls(daemon: HostProfile): string[] {
|
|
const endpoints = daemon.endpoints ?? [];
|
|
const sessionId = daemon.relay?.sessionId ?? null;
|
|
const lastKnownGood =
|
|
typeof daemon.metadata?.lastKnownGoodEndpoint === "string"
|
|
? (daemon.metadata.lastKnownGoodEndpoint as string)
|
|
: null;
|
|
|
|
const out: string[] = [];
|
|
const push = (url: string) => {
|
|
if (!out.includes(url)) out.push(url);
|
|
};
|
|
|
|
if (lastKnownGood) {
|
|
push(buildDaemonWebSocketUrl(lastKnownGood));
|
|
}
|
|
|
|
for (const endpoint of endpoints) {
|
|
push(buildDaemonWebSocketUrl(endpoint));
|
|
}
|
|
|
|
if (sessionId) {
|
|
if (lastKnownGood) {
|
|
push(buildRelayWebSocketUrl({ endpoint: lastKnownGood, sessionId }));
|
|
}
|
|
for (const endpoint of endpoints) {
|
|
push(buildRelayWebSocketUrl({ endpoint, sessionId }));
|
|
}
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
function extractEndpointFromUrl(url: string): string | null {
|
|
try {
|
|
const parsed = new URL(url);
|
|
const host = parsed.hostname;
|
|
const port = parsed.port ? Number(parsed.port) : parsed.protocol === "wss:" ? 443 : 80;
|
|
const isIpv6 = host.includes(":") && !host.startsWith("[") && !host.endsWith("]");
|
|
return isIpv6 ? `[${host}]:${port}` : `${host}:${port}`;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function ManagedDaemonSession({ daemon }: { daemon: HostProfile }) {
|
|
const { connectionStates } = useDaemonConnections();
|
|
const { updateDaemon } = useDaemonRegistry();
|
|
|
|
const candidates = useMemo(() => buildCandidateUrls(daemon), [daemon]);
|
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
const activeUrl = candidates[activeIndex] ?? candidates[0] ?? null;
|
|
|
|
const lastAttemptedUrlRef = useRef<string | null>(null);
|
|
const pendingMetadataWriteRef = useRef(false);
|
|
|
|
useEffect(() => {
|
|
if (!activeUrl) {
|
|
return;
|
|
}
|
|
// If the active URL fell out of the candidate set (e.g. endpoints updated), snap back.
|
|
const idx = candidates.indexOf(activeUrl);
|
|
if (idx === -1) {
|
|
setActiveIndex(0);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [candidates.join("|")]);
|
|
|
|
if (!activeUrl) {
|
|
return null;
|
|
}
|
|
|
|
const connection = connectionStates.get(daemon.id);
|
|
const status = connection?.status ?? "idle";
|
|
const lastError = connection?.lastError ?? null;
|
|
|
|
useEffect(() => {
|
|
if (!connection) return;
|
|
|
|
if (status === "online") {
|
|
if (pendingMetadataWriteRef.current) return;
|
|
const endpoint = extractEndpointFromUrl(activeUrl);
|
|
if (!endpoint) return;
|
|
if (daemon.metadata?.lastKnownGoodEndpoint === endpoint) return;
|
|
|
|
pendingMetadataWriteRef.current = true;
|
|
void updateDaemon(daemon.id, {
|
|
metadata: { ...(daemon.metadata ?? {}), lastKnownGoodEndpoint: endpoint },
|
|
}).finally(() => {
|
|
pendingMetadataWriteRef.current = false;
|
|
});
|
|
return;
|
|
}
|
|
|
|
if ((status === "error" || (status === "offline" && lastError)) && candidates.length > 1) {
|
|
if (lastAttemptedUrlRef.current === activeUrl) {
|
|
return;
|
|
}
|
|
lastAttemptedUrlRef.current = activeUrl;
|
|
if (activeIndex < candidates.length - 1) {
|
|
setActiveIndex((idx) => Math.min(idx + 1, candidates.length - 1));
|
|
}
|
|
}
|
|
}, [
|
|
activeIndex,
|
|
activeUrl,
|
|
candidates.length,
|
|
daemon.id,
|
|
daemon.metadata,
|
|
lastError,
|
|
status,
|
|
updateDaemon,
|
|
connection,
|
|
]);
|
|
|
|
return (
|
|
<SessionProvider key={`${daemon.id}:${activeUrl}`} serverUrl={activeUrl} serverId={daemon.id}>
|
|
{null}
|
|
</SessionProvider>
|
|
);
|
|
}
|
|
|
|
export function MultiDaemonSessionHost() {
|
|
const { daemons } = useDaemonRegistry();
|
|
if (daemons.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{daemons.map((daemon) => (
|
|
<ManagedDaemonSession key={daemon.id} daemon={daemon} />
|
|
))}
|
|
</>
|
|
);
|
|
}
|