Fix dev override host bootstrap (#789)
This commit is contained in:
@@ -65,6 +65,7 @@ import { polyfillCrypto } from "@/polyfills/crypto";
|
|||||||
import { queryClient } from "@/query/query-client";
|
import { queryClient } from "@/query/query-client";
|
||||||
import {
|
import {
|
||||||
getHostRuntimeStore,
|
getHostRuntimeStore,
|
||||||
|
hasConfiguredLocalDaemonOverride,
|
||||||
useHostMutations,
|
useHostMutations,
|
||||||
useHostRuntimeClient,
|
useHostRuntimeClient,
|
||||||
useHosts,
|
useHosts,
|
||||||
@@ -326,6 +327,8 @@ function HostRuntimeBootstrapProvider({ children }: { children: ReactNode }) {
|
|||||||
const anyOnlineHostServerId = useEarliestOnlineHostServerId();
|
const anyOnlineHostServerId = useEarliestOnlineHostServerId();
|
||||||
const daemonStartError = useDaemonStartLastError();
|
const daemonStartError = useDaemonStartLastError();
|
||||||
const daemonStartIsRunning = useDaemonStartIsRunning();
|
const daemonStartIsRunning = useDaemonStartIsRunning();
|
||||||
|
const waitForConfiguredLocalDaemon =
|
||||||
|
hasConfiguredLocalDaemonOverride() && !shouldUseDesktopDaemon();
|
||||||
|
|
||||||
const [hasGivenUpWaitingForHost, setHasGivenUpWaitingForHost] = useState(false);
|
const [hasGivenUpWaitingForHost, setHasGivenUpWaitingForHost] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -333,6 +336,7 @@ function HostRuntimeBootstrapProvider({ children }: { children: ReactNode }) {
|
|||||||
anyOnlineHostServerId ||
|
anyOnlineHostServerId ||
|
||||||
daemonStartError ||
|
daemonStartError ||
|
||||||
daemonStartIsRunning ||
|
daemonStartIsRunning ||
|
||||||
|
waitForConfiguredLocalDaemon ||
|
||||||
hasGivenUpWaitingForHost
|
hasGivenUpWaitingForHost
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -343,7 +347,13 @@ function HostRuntimeBootstrapProvider({ children }: { children: ReactNode }) {
|
|||||||
return () => {
|
return () => {
|
||||||
clearTimeout(handle);
|
clearTimeout(handle);
|
||||||
};
|
};
|
||||||
}, [anyOnlineHostServerId, daemonStartError, daemonStartIsRunning, hasGivenUpWaitingForHost]);
|
}, [
|
||||||
|
anyOnlineHostServerId,
|
||||||
|
daemonStartError,
|
||||||
|
daemonStartIsRunning,
|
||||||
|
waitForConfiguredLocalDaemon,
|
||||||
|
hasGivenUpWaitingForHost,
|
||||||
|
]);
|
||||||
|
|
||||||
const retry = useCallback(() => {
|
const retry = useCallback(() => {
|
||||||
const daemonStartService = getDaemonStartService({ store: getHostRuntimeStore() });
|
const daemonStartService = getDaemonStartService({ store: getHostRuntimeStore() });
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
serializeConnectionUri,
|
serializeConnectionUri,
|
||||||
serializeConnectionUriForStorage,
|
serializeConnectionUriForStorage,
|
||||||
} from "@/utils/daemon-endpoints";
|
} from "@/utils/daemon-endpoints";
|
||||||
import { DaemonConnectionTestError, connectToDaemon } from "@/utils/test-daemon-connection";
|
import { DaemonConnectionTestError } from "@/utils/test-daemon-connection";
|
||||||
import { AdaptiveModalSheet, AdaptiveTextInput } from "./adaptive-modal-sheet";
|
import { AdaptiveModalSheet, AdaptiveTextInput } from "./adaptive-modal-sheet";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
@@ -267,7 +267,7 @@ export interface AddHostModalProps {
|
|||||||
export function AddHostModal({ visible, onClose, onCancel, onSaved }: AddHostModalProps) {
|
export function AddHostModal({ visible, onClose, onCancel, onSaved }: AddHostModalProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const daemons = useHosts();
|
const daemons = useHosts();
|
||||||
const { upsertDirectConnection } = useHostMutations();
|
const { probeAndUpsertDirectConnection } = useHostMutations();
|
||||||
const isMobile = useIsCompactFormFactor();
|
const isMobile = useIsCompactFormFactor();
|
||||||
|
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
@@ -336,22 +336,12 @@ export function AddHostModal({ visible, onClose, onCancel, onSaved }: AddHostMod
|
|||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
setErrorMessage("");
|
setErrorMessage("");
|
||||||
|
|
||||||
const { client, serverId, hostname } = await connectToDaemon({
|
const { profile, serverId, hostname } = await probeAndUpsertDirectConnection({
|
||||||
id: "probe",
|
|
||||||
type: "directTcp",
|
|
||||||
endpoint: connection.endpoint,
|
endpoint: connection.endpoint,
|
||||||
useTls: connection.useTls,
|
useTls: connection.useTls,
|
||||||
...(connection.password ? { password: connection.password } : {}),
|
...(connection.password ? { password: connection.password } : {}),
|
||||||
});
|
});
|
||||||
await client.close().catch(() => undefined);
|
|
||||||
const isNewHost = !daemons.some((daemon) => daemon.serverId === serverId);
|
const isNewHost = !daemons.some((daemon) => daemon.serverId === serverId);
|
||||||
const profile = await upsertDirectConnection({
|
|
||||||
serverId,
|
|
||||||
endpoint: connection.endpoint,
|
|
||||||
useTls: connection.useTls,
|
|
||||||
...(connection.password ? { password: connection.password } : {}),
|
|
||||||
label: hostname ?? undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
onSaved?.({ profile, serverId, hostname, isNewHost });
|
onSaved?.({ profile, serverId, hostname, isNewHost });
|
||||||
handleClose();
|
handleClose();
|
||||||
@@ -381,7 +371,7 @@ export function AddHostModal({ visible, onClose, onCancel, onSaved }: AddHostMod
|
|||||||
onSaved,
|
onSaved,
|
||||||
password,
|
password,
|
||||||
port,
|
port,
|
||||||
upsertDirectConnection,
|
probeAndUpsertDirectConnection,
|
||||||
useTls,
|
useTls,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1596,6 +1596,85 @@ describe("HostRuntimeStore", () => {
|
|||||||
store.syncHosts([]);
|
store.syncHosts([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("probeAndUpsertConnection learns the real server id before storing a direct host", async () => {
|
||||||
|
const connection: HostConnection = {
|
||||||
|
id: "direct:lan:6767",
|
||||||
|
type: "directTcp",
|
||||||
|
endpoint: "lan:6767",
|
||||||
|
};
|
||||||
|
const probeClient = makeConnectedProbeClient(5);
|
||||||
|
const seenProbeHosts: string[] = [];
|
||||||
|
const store = new HostRuntimeStore({
|
||||||
|
deps: {
|
||||||
|
createClient: () => new FakeDaemonClient() as unknown as DaemonClient,
|
||||||
|
connectToDaemon: async ({ host, connection: probedConnection }) => {
|
||||||
|
seenProbeHosts.push(host.serverId);
|
||||||
|
expect(probedConnection).toEqual(connection);
|
||||||
|
return {
|
||||||
|
client: probeClient as unknown as DaemonClient,
|
||||||
|
serverId: "srv_real_direct",
|
||||||
|
hostname: "mbp",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getClientId: async () => "cid_test_runtime",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await store.probeAndUpsertConnection({ connection });
|
||||||
|
|
||||||
|
expect(result.serverId).toBe("srv_real_direct");
|
||||||
|
expect(result.hostname).toBe("mbp");
|
||||||
|
expect(seenProbeHosts).toEqual([""]);
|
||||||
|
expect(probeClient.closeCalls).toBe(0);
|
||||||
|
expect(store.getHosts()).toMatchObject([
|
||||||
|
{
|
||||||
|
serverId: "srv_real_direct",
|
||||||
|
label: "mbp",
|
||||||
|
connections: [connection],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
store.syncHosts([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("probeAndUpsertConnection replaces a matching placeholder host with the real server id", async () => {
|
||||||
|
const connection: HostConnection = {
|
||||||
|
id: "direct:lan:6767",
|
||||||
|
type: "directTcp",
|
||||||
|
endpoint: "lan:6767",
|
||||||
|
};
|
||||||
|
const store = new HostRuntimeStore({
|
||||||
|
deps: {
|
||||||
|
createClient: () => new FakeDaemonClient() as unknown as DaemonClient,
|
||||||
|
connectToDaemon: async () => ({
|
||||||
|
client: makeConnectedProbeClient(5) as unknown as DaemonClient,
|
||||||
|
serverId: "srv_real_direct",
|
||||||
|
hostname: "mbp",
|
||||||
|
}),
|
||||||
|
getClientId: async () => "cid_test_runtime",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
(
|
||||||
|
store as unknown as {
|
||||||
|
hosts: HostProfile[];
|
||||||
|
}
|
||||||
|
).hosts = [
|
||||||
|
makeHost({
|
||||||
|
serverId: "local:lan:6767",
|
||||||
|
label: "local:lan:6767",
|
||||||
|
connections: [connection],
|
||||||
|
preferredConnectionId: connection.id,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
await store.probeAndUpsertConnection({ connection });
|
||||||
|
|
||||||
|
expect(store.getHosts().map((host) => host.serverId)).toEqual(["srv_real_direct"]);
|
||||||
|
expect(store.getHosts()[0]?.label).toBe("mbp");
|
||||||
|
|
||||||
|
store.syncHosts([]);
|
||||||
|
});
|
||||||
|
|
||||||
it("uses the advertised hostname when adding a relay host from a pairing offer", async () => {
|
it("uses the advertised hostname when adding a relay host from a pairing offer", async () => {
|
||||||
const store = new HostRuntimeStore({
|
const store = new HostRuntimeStore({
|
||||||
deps: {
|
deps: {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
} from "@server/client/daemon-client";
|
} from "@server/client/daemon-client";
|
||||||
import {
|
import {
|
||||||
connectionFromListen,
|
connectionFromListen,
|
||||||
hostHasConnection,
|
|
||||||
normalizeStoredHostProfile,
|
normalizeStoredHostProfile,
|
||||||
upsertHostConnectionInProfiles,
|
upsertHostConnectionInProfiles,
|
||||||
registryHasConnection,
|
registryHasConnection,
|
||||||
@@ -120,7 +119,11 @@ export interface HostRuntimeControllerDeps {
|
|||||||
clientId: string;
|
clientId: string;
|
||||||
runtimeGeneration: number;
|
runtimeGeneration: number;
|
||||||
}) => DaemonClient;
|
}) => DaemonClient;
|
||||||
connectToDaemon: (input: { host: HostProfile; connection: HostConnection }) => Promise<{
|
connectToDaemon: (input: {
|
||||||
|
host: HostProfile;
|
||||||
|
connection: HostConnection;
|
||||||
|
timeoutMs?: number;
|
||||||
|
}) => Promise<{
|
||||||
client: DaemonClient;
|
client: DaemonClient;
|
||||||
serverId: string;
|
serverId: string;
|
||||||
hostname: string | null;
|
hostname: string | null;
|
||||||
@@ -142,6 +145,7 @@ const PROBE_MAX_BACKOFF_MS = 30_000;
|
|||||||
const ADAPTIVE_SWITCH_THRESHOLD_MS = 40;
|
const ADAPTIVE_SWITCH_THRESHOLD_MS = 40;
|
||||||
const ADAPTIVE_SWITCH_CONSECUTIVE_PROBES = 3;
|
const ADAPTIVE_SWITCH_CONSECUTIVE_PROBES = 3;
|
||||||
const DEFAULT_AGENT_DIRECTORY_PAGE_LIMIT = 200;
|
const DEFAULT_AGENT_DIRECTORY_PAGE_LIMIT = 200;
|
||||||
|
const CONFIGURED_OVERRIDE_BOOTSTRAP_RETRY_MS = 1_000;
|
||||||
|
|
||||||
const DEFAULT_AGENT_DIRECTORY_SORT: NonNullable<FetchAgentsOptions["sort"]> = [
|
const DEFAULT_AGENT_DIRECTORY_SORT: NonNullable<FetchAgentsOptions["sort"]> = [
|
||||||
{ key: "updated_at", direction: "desc" },
|
{ key: "updated_at", direction: "desc" },
|
||||||
@@ -485,8 +489,11 @@ function createDefaultDeps(): HostRuntimeControllerDeps {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
connectToDaemon: ({ host, connection }) =>
|
connectToDaemon: ({ host, connection, timeoutMs }) =>
|
||||||
connectToDaemon(connection, { serverId: host.serverId }),
|
connectToDaemon(connection, {
|
||||||
|
...(host.serverId ? { serverId: host.serverId } : {}),
|
||||||
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
||||||
|
}),
|
||||||
getClientId: () => getOrCreateClientId(),
|
getClientId: () => getOrCreateClientId(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1179,14 +1186,18 @@ function readConfiguredLocalDaemonOverride(): string | null {
|
|||||||
return value && value.length > 0 ? value : null;
|
return value && value.length > 0 ? value : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function placeholderServerIdForEndpoint(endpoint: string): string {
|
export function hasConfiguredLocalDaemonOverride(): boolean {
|
||||||
return `local:${normalizeHostPort(endpoint)}`;
|
return readConfiguredLocalDaemonOverride() !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPlaceholderServerId(serverId: string): boolean {
|
function isPlaceholderServerId(serverId: string): boolean {
|
||||||
return serverId.startsWith("local:");
|
return serverId.startsWith("local:");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function delay(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
function rekeyMap<V>(map: Map<string, V>, oldKey: string, newKey: string): void {
|
function rekeyMap<V>(map: Map<string, V>, oldKey: string, newKey: string): void {
|
||||||
const value = map.get(oldKey);
|
const value = map.get(oldKey);
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
@@ -1207,6 +1218,7 @@ export class HostRuntimeStore {
|
|||||||
private deps: HostRuntimeControllerDeps;
|
private deps: HostRuntimeControllerDeps;
|
||||||
private lastConnectionStatusByServer = new Map<string, HostRuntimeConnectionStatus>();
|
private lastConnectionStatusByServer = new Map<string, HostRuntimeConnectionStatus>();
|
||||||
private agentDirectoryBootstrapInFlight = new Map<string, Promise<void>>();
|
private agentDirectoryBootstrapInFlight = new Map<string, Promise<void>>();
|
||||||
|
private configuredOverrideBootstrapInFlight: Promise<void> | null = null;
|
||||||
private bootStarted = false;
|
private bootStarted = false;
|
||||||
|
|
||||||
constructor(input?: { deps?: HostRuntimeControllerDeps }) {
|
constructor(input?: { deps?: HostRuntimeControllerDeps }) {
|
||||||
@@ -1239,12 +1251,8 @@ export class HostRuntimeStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async runBoot(): Promise<void> {
|
private async runBoot(): Promise<void> {
|
||||||
await this.loadFromStorage();
|
|
||||||
|
|
||||||
const override = readConfiguredLocalDaemonOverride();
|
const override = readConfiguredLocalDaemonOverride();
|
||||||
if (override) {
|
await this.loadFromStorage();
|
||||||
this.prunePlaceholderHostsForOtherEndpoints(override);
|
|
||||||
}
|
|
||||||
|
|
||||||
let isE2E: string | null = null;
|
let isE2E: string | null = null;
|
||||||
try {
|
try {
|
||||||
@@ -1277,12 +1285,16 @@ export class HostRuntimeStore {
|
|||||||
if (!Array.isArray(parsed)) {
|
if (!Array.isArray(parsed)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const profiles = parsed
|
const normalizedProfiles = parsed
|
||||||
.map((entry) => normalizeStoredHostProfile(entry))
|
.map((entry) => normalizeStoredHostProfile(entry))
|
||||||
.filter((entry): entry is HostProfile => entry !== null);
|
.filter((entry): entry is HostProfile => entry !== null);
|
||||||
|
const profiles = normalizedProfiles.filter((entry) => !isPlaceholderServerId(entry.serverId));
|
||||||
this.hosts = profiles;
|
this.hosts = profiles;
|
||||||
this.syncHosts(profiles);
|
this.syncHosts(profiles);
|
||||||
this.emitHostList();
|
this.emitHostList();
|
||||||
|
if (profiles.length !== normalizedProfiles.length) {
|
||||||
|
void this.persistHosts();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[HostRuntime] Failed to load host registry from storage", error);
|
console.error("[HostRuntime] Failed to load host registry from storage", error);
|
||||||
}
|
}
|
||||||
@@ -1295,15 +1307,9 @@ export class HostRuntimeStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { client, serverId, hostname } = await connectToDaemon(connection, {
|
await this.probeAndUpsertConnection({
|
||||||
timeoutMs: DEFAULT_LOCALHOST_BOOTSTRAP_TIMEOUT_MS,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.upsertHostConnection({
|
|
||||||
serverId,
|
|
||||||
label: hostname ?? undefined,
|
|
||||||
connection,
|
connection,
|
||||||
existingClient: client,
|
timeoutMs: DEFAULT_LOCALHOST_BOOTSTRAP_TIMEOUT_MS,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[HostRuntime] bootstrap probe failed", {
|
console.warn("[HostRuntime] bootstrap probe failed", {
|
||||||
@@ -1318,35 +1324,45 @@ export class HostRuntimeStore {
|
|||||||
if (!connection) {
|
if (!connection) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const placeholderId = placeholderServerIdForEndpoint(endpoint);
|
|
||||||
const staleHosts = this.hosts.filter(
|
|
||||||
(host) => host.serverId !== placeholderId && hostHasConnection(host, connection),
|
|
||||||
);
|
|
||||||
for (const staleHost of staleHosts) {
|
|
||||||
void this.removeConnection(staleHost.serverId, connection.id);
|
|
||||||
}
|
|
||||||
if (registryHasConnection(this.hosts, connection)) {
|
if (registryHasConnection(this.hosts, connection)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void this.upsertHostConnection({
|
if (this.configuredOverrideBootstrapInFlight) {
|
||||||
serverId: placeholderId,
|
|
||||||
connection,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private prunePlaceholderHostsForOtherEndpoints(currentEndpoint: string): void {
|
|
||||||
const expectedServerId = placeholderServerIdForEndpoint(currentEndpoint);
|
|
||||||
const remaining = this.hosts.filter((host) => {
|
|
||||||
if (!isPlaceholderServerId(host.serverId)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return host.serverId === expectedServerId;
|
|
||||||
});
|
|
||||||
if (remaining.length === this.hosts.length) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setHostsAndSync(remaining);
|
|
||||||
void this.persistHosts();
|
const bootstrap = this.runConfiguredOverrideBootstrap(endpoint, connection).finally(() => {
|
||||||
|
if (this.configuredOverrideBootstrapInFlight === bootstrap) {
|
||||||
|
this.configuredOverrideBootstrapInFlight = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.configuredOverrideBootstrapInFlight = bootstrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async runConfiguredOverrideBootstrap(
|
||||||
|
endpoint: string,
|
||||||
|
connection: HostConnection,
|
||||||
|
): Promise<void> {
|
||||||
|
let attempt = 0;
|
||||||
|
while (!registryHasConnection(this.hosts, connection)) {
|
||||||
|
attempt += 1;
|
||||||
|
try {
|
||||||
|
await this.probeAndUpsertConnection({
|
||||||
|
connection,
|
||||||
|
timeoutMs: DEFAULT_LOCALHOST_BOOTSTRAP_TIMEOUT_MS,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
if (attempt === 1 || attempt % 10 === 0) {
|
||||||
|
console.warn("[HostRuntime] configured bootstrap probe failed", {
|
||||||
|
endpoint,
|
||||||
|
attempt,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await delay(CONFIGURED_OVERRIDE_BOOTSTRAP_RETRY_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reconcileServerId(oldServerId: string, newServerId: string): void {
|
reconcileServerId(oldServerId: string, newServerId: string): void {
|
||||||
@@ -1411,6 +1427,57 @@ export class HostRuntimeStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async probeAndUpsertConnection(input: {
|
||||||
|
connection: HostConnection;
|
||||||
|
label?: string;
|
||||||
|
timeoutMs?: number;
|
||||||
|
}): Promise<{ profile: HostProfile; serverId: string; hostname: string | null }> {
|
||||||
|
if (input.connection.type === "relay") {
|
||||||
|
throw new Error("Cannot probe a relay connection without a server id.");
|
||||||
|
}
|
||||||
|
const probeHost: HostProfile = {
|
||||||
|
serverId: "",
|
||||||
|
label: input.label ?? input.connection.id,
|
||||||
|
lifecycle: {},
|
||||||
|
connections: [input.connection],
|
||||||
|
preferredConnectionId: input.connection.id,
|
||||||
|
createdAt: new Date(0).toISOString(),
|
||||||
|
updatedAt: new Date(0).toISOString(),
|
||||||
|
};
|
||||||
|
const { client, serverId, hostname } = await this.deps.connectToDaemon({
|
||||||
|
host: probeHost,
|
||||||
|
connection: input.connection,
|
||||||
|
...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
|
||||||
|
});
|
||||||
|
const profile = await this.upsertHostConnection({
|
||||||
|
serverId,
|
||||||
|
label: input.label ?? hostname ?? undefined,
|
||||||
|
connection: input.connection,
|
||||||
|
existingClient: client,
|
||||||
|
});
|
||||||
|
return { profile, serverId, hostname };
|
||||||
|
}
|
||||||
|
|
||||||
|
async probeAndUpsertDirectConnection(input: {
|
||||||
|
endpoint: string;
|
||||||
|
useTls?: boolean;
|
||||||
|
password?: string;
|
||||||
|
label?: string;
|
||||||
|
}): Promise<{ profile: HostProfile; serverId: string; hostname: string | null }> {
|
||||||
|
const endpoint = normalizeHostPort(input.endpoint);
|
||||||
|
const password = input.password?.trim();
|
||||||
|
return this.probeAndUpsertConnection({
|
||||||
|
label: input.label,
|
||||||
|
connection: {
|
||||||
|
id: `direct:${endpoint}`,
|
||||||
|
type: "directTcp",
|
||||||
|
endpoint,
|
||||||
|
useTls: input.useTls ?? false,
|
||||||
|
...(password ? { password } : {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async upsertRelayConnection(input: {
|
async upsertRelayConnection(input: {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
relayEndpoint: string;
|
relayEndpoint: string;
|
||||||
@@ -1985,6 +2052,12 @@ export interface HostMutations {
|
|||||||
password?: string;
|
password?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
}) => Promise<HostProfile>;
|
}) => Promise<HostProfile>;
|
||||||
|
probeAndUpsertDirectConnection: (input: {
|
||||||
|
endpoint: string;
|
||||||
|
useTls?: boolean;
|
||||||
|
password?: string;
|
||||||
|
label?: string;
|
||||||
|
}) => Promise<{ profile: HostProfile; serverId: string; hostname: string | null }>;
|
||||||
upsertRelayConnection: (input: {
|
upsertRelayConnection: (input: {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
relayEndpoint: string;
|
relayEndpoint: string;
|
||||||
@@ -2007,6 +2080,7 @@ export function useHostMutations(): HostMutations {
|
|||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
upsertDirectConnection: (input) => store.upsertDirectConnection(input),
|
upsertDirectConnection: (input) => store.upsertDirectConnection(input),
|
||||||
|
probeAndUpsertDirectConnection: (input) => store.probeAndUpsertDirectConnection(input),
|
||||||
upsertRelayConnection: (input) => store.upsertRelayConnection(input),
|
upsertRelayConnection: (input) => store.upsertRelayConnection(input),
|
||||||
upsertConnectionFromOffer: (offer, label) => store.upsertConnectionFromOffer(offer, label),
|
upsertConnectionFromOffer: (offer, label) => store.upsertConnectionFromOffer(offer, label),
|
||||||
upsertConnectionFromOfferUrl: (url, label) => store.upsertConnectionFromOfferUrl(url, label),
|
upsertConnectionFromOfferUrl: (url, label) => store.upsertConnectionFromOfferUrl(url, label),
|
||||||
|
|||||||
Reference in New Issue
Block a user