Files
paseo/packages/app/src/components/multi-daemon-session-host.tsx
T

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} />
))}
</>
);
}