fix(app): stabilize startup connection routing
This commit is contained in:
@@ -178,6 +178,11 @@ class StartupAssertions {
|
|||||||
await expect(this.page.locator('[data-testid="sidebar-project-list"]:visible')).toHaveCount(0);
|
await expect(this.page.locator('[data-testid="sidebar-project-list"]:visible')).toHaveCount(0);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async expectsNoUndefinedRoute(): Promise<this> {
|
||||||
|
await expect(this.page).not.toHaveURL(/\/h\/undefined\/workspace\/undefined/);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function installPendingDesktopBridge(page: Page): Promise<void> {
|
async function installPendingDesktopBridge(page: Page): Promise<void> {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ test.describe("Startup loading presentation", () => {
|
|||||||
|
|
||||||
await startup.expectsDesktopDaemonStartup();
|
await startup.expectsDesktopDaemonStartup();
|
||||||
await startup.expectsSidebarHidden();
|
await startup.expectsSidebarHidden();
|
||||||
|
await startup.expectsNoUndefinedRoute();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("host-route refresh does not render route chrome around the bootstrap splash", async ({
|
test("host-route refresh does not render route chrome around the bootstrap splash", async ({
|
||||||
|
|||||||
@@ -353,7 +353,9 @@ function HostRuntimeBootstrapProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
void store.bootstrap({ manageBuiltInDaemon: settings.manageBuiltInDaemon });
|
setPhase("connecting");
|
||||||
|
setError(null);
|
||||||
|
await store.bootstrap({ manageBuiltInDaemon: settings.manageBuiltInDaemon });
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setPhase("online");
|
setPhase("online");
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -435,8 +437,6 @@ function AppContainer({
|
|||||||
const closeDesktopFileExplorer = usePanelStore((state) => state.closeDesktopFileExplorer);
|
const closeDesktopFileExplorer = usePanelStore((state) => state.closeDesktopFileExplorer);
|
||||||
const toggleFocusMode = usePanelStore((state) => state.toggleFocusMode);
|
const toggleFocusMode = usePanelStore((state) => state.toggleFocusMode);
|
||||||
const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled);
|
const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled);
|
||||||
const agentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
|
|
||||||
const sidebarWidth = usePanelStore((state) => state.sidebarWidth);
|
|
||||||
|
|
||||||
const cycleTheme = useCallback(() => {
|
const cycleTheme = useCallback(() => {
|
||||||
const currentIndex = THEME_CYCLE_ORDER.indexOf(settings.theme as ThemeName);
|
const currentIndex = THEME_CYCLE_ORDER.indexOf(settings.theme as ThemeName);
|
||||||
@@ -473,35 +473,6 @@ function AppContainer({
|
|||||||
// other non-workspace routes) don't need a special-case to keep shortcuts alive.
|
// other non-workspace routes) don't need a special-case to keep shortcuts alive.
|
||||||
const keyboardShortcutsEnabled = chromeEnabled || pathname.startsWith("/settings");
|
const keyboardShortcutsEnabled = chromeEnabled || pathname.startsWith("/settings");
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const bp = UnistylesRuntime.breakpoint;
|
|
||||||
const screenW = UnistylesRuntime.screen.width;
|
|
||||||
const screenH = UnistylesRuntime.screen.height;
|
|
||||||
const isElectron = getIsElectronRuntime();
|
|
||||||
const windowW = isWeb ? window.innerWidth : undefined;
|
|
||||||
const windowH = isWeb ? window.innerHeight : undefined;
|
|
||||||
const dpr = isWeb ? window.devicePixelRatio : undefined;
|
|
||||||
const ua = isWeb ? navigator.userAgent : undefined;
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"[layout-debug]",
|
|
||||||
JSON.stringify({
|
|
||||||
breakpoint: bp,
|
|
||||||
isCompactLayout,
|
|
||||||
isElectron,
|
|
||||||
chromeEnabled,
|
|
||||||
isFocusModeEnabled,
|
|
||||||
agentListOpen,
|
|
||||||
sidebarWidth,
|
|
||||||
sidebarRenderedInRow: !isCompactLayout && chromeEnabled && !isFocusModeEnabled,
|
|
||||||
unistylesScreen: { w: screenW, h: screenH },
|
|
||||||
window: { w: windowW, h: windowH },
|
|
||||||
devicePixelRatio: dpr,
|
|
||||||
userAgent: ua,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}, [isCompactLayout, chromeEnabled, isFocusModeEnabled, agentListOpen, sidebarWidth]);
|
|
||||||
|
|
||||||
useKeyboardShortcuts({
|
useKeyboardShortcuts({
|
||||||
enabled: keyboardShortcutsEnabled,
|
enabled: keyboardShortcutsEnabled,
|
||||||
isMobile: isCompactLayout,
|
isMobile: isCompactLayout,
|
||||||
@@ -876,6 +847,7 @@ function RootStack() {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Stack.Screen name="index" />
|
||||||
<Stack.Protected guard={storeReady}>
|
<Stack.Protected guard={storeReady}>
|
||||||
<Stack.Screen name="welcome" />
|
<Stack.Screen name="welcome" />
|
||||||
<Stack.Screen name="settings/index" />
|
<Stack.Screen name="settings/index" />
|
||||||
@@ -895,7 +867,6 @@ function RootStack() {
|
|||||||
<Stack.Screen name="h/[serverId]/sessions" />
|
<Stack.Screen name="h/[serverId]/sessions" />
|
||||||
<Stack.Screen name="h/[serverId]/open-project" />
|
<Stack.Screen name="h/[serverId]/open-project" />
|
||||||
<Stack.Screen name="h/[serverId]/settings" />
|
<Stack.Screen name="h/[serverId]/settings" />
|
||||||
<Stack.Screen name="index" />
|
|
||||||
<Stack.Screen name="settings/hosts/[serverId]" />
|
<Stack.Screen name="settings/hosts/[serverId]" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -247,7 +247,10 @@ export function useKeyboardShortcuts({
|
|||||||
const lastWorkspaceRoute = getLastNavigationWorkspaceRouteSelection();
|
const lastWorkspaceRoute = getLastNavigationWorkspaceRouteSelection();
|
||||||
if (lastWorkspaceRoute) {
|
if (lastWorkspaceRoute) {
|
||||||
router.replace(
|
router.replace(
|
||||||
buildHostWorkspaceRoute(lastWorkspaceRoute.serverId, lastWorkspaceRoute.workspaceId),
|
buildHostWorkspaceRoute(
|
||||||
|
lastWorkspaceRoute.serverId,
|
||||||
|
lastWorkspaceRoute.workspaceId,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,17 +213,21 @@ function makeDeps(
|
|||||||
return client as unknown as DaemonClient;
|
return client as unknown as DaemonClient;
|
||||||
},
|
},
|
||||||
connectToDaemon: async ({ host, connection }) => {
|
connectToDaemon: async ({ host, connection }) => {
|
||||||
const value = latencyByConnectionId[connection.id];
|
const readLatency = (): number => {
|
||||||
if (value instanceof Error) {
|
const value = latencyByConnectionId[connection.id];
|
||||||
throw value;
|
if (value instanceof Error) {
|
||||||
}
|
throw value;
|
||||||
if (typeof value !== "number") {
|
}
|
||||||
throw new Error(`missing latency for ${connection.id}`);
|
if (typeof value !== "number") {
|
||||||
}
|
throw new Error(`missing latency for ${connection.id}`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
readLatency();
|
||||||
const client = new FakeDaemonClient();
|
const client = new FakeDaemonClient();
|
||||||
client.connectCalls = 1;
|
client.connectCalls = 1;
|
||||||
client.setConnectionState({ status: "connected" });
|
client.setConnectionState({ status: "connected" });
|
||||||
client.ping = async () => ({ rttMs: value });
|
client.ping = async () => ({ rttMs: readLatency() });
|
||||||
createdClients.push(client);
|
createdClients.push(client);
|
||||||
return {
|
return {
|
||||||
client: client as unknown as DaemonClient,
|
client: client as unknown as DaemonClient,
|
||||||
@@ -433,7 +437,95 @@ describe("HostRuntimeController", () => {
|
|||||||
await probeCycle;
|
await probeCycle;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("fails over when active connection becomes unavailable", async () => {
|
it("probes the active online connection through the existing client", async () => {
|
||||||
|
const host = makeHost({ preferredConnectionId: "direct:lan:6767" });
|
||||||
|
const probeAttempts: string[] = [];
|
||||||
|
const latencies: Record<string, number | Error> = {
|
||||||
|
"direct:lan:6767": 12,
|
||||||
|
"relay:relay.paseo.sh:443": 65,
|
||||||
|
};
|
||||||
|
const controller = new HostRuntimeController({
|
||||||
|
host,
|
||||||
|
deps: {
|
||||||
|
createClient: () => {
|
||||||
|
throw new Error("should adopt probe clients");
|
||||||
|
},
|
||||||
|
connectToDaemon: async ({ host, connection }) => {
|
||||||
|
probeAttempts.push(connection.id);
|
||||||
|
const value = latencies[connection.id];
|
||||||
|
if (value instanceof Error) {
|
||||||
|
throw value;
|
||||||
|
}
|
||||||
|
if (typeof value !== "number") {
|
||||||
|
throw new Error(`missing latency for ${connection.id}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
client: makeConnectedProbeClient(value) as unknown as DaemonClient,
|
||||||
|
serverId: host.serverId,
|
||||||
|
hostname: host.label ?? null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getClientId: async () => "cid_test_runtime",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await controller.start({ autoProbe: false });
|
||||||
|
expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767");
|
||||||
|
|
||||||
|
probeAttempts.length = 0;
|
||||||
|
const activeClient = controller.getSnapshot().client as unknown as FakeDaemonClient;
|
||||||
|
activeClient.ping = async () => ({ rttMs: 9 });
|
||||||
|
clearProbeBackoff(controller);
|
||||||
|
await controller.runProbeCycleNow();
|
||||||
|
|
||||||
|
expect(probeAttempts).toEqual(["relay:relay.paseo.sh:443"]);
|
||||||
|
expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767");
|
||||||
|
expect(controller.getSnapshot().connectionStatus).toBe("online");
|
||||||
|
expect(controller.getSnapshot().probeByConnectionId.get("direct:lan:6767")).toEqual({
|
||||||
|
status: "available",
|
||||||
|
latencyMs: 9,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects probes that resolve to a different server id", async () => {
|
||||||
|
const host = makeHost({
|
||||||
|
serverId: "srv_old",
|
||||||
|
connections: [
|
||||||
|
{
|
||||||
|
id: "direct:localhost:6767",
|
||||||
|
type: "directTcp",
|
||||||
|
endpoint: "localhost:6767",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const mismatchedClient = makeConnectedProbeClient(8);
|
||||||
|
const controller = new HostRuntimeController({
|
||||||
|
host,
|
||||||
|
deps: {
|
||||||
|
createClient: () => {
|
||||||
|
throw new Error("should not create active client");
|
||||||
|
},
|
||||||
|
connectToDaemon: async () => ({
|
||||||
|
client: mismatchedClient as unknown as DaemonClient,
|
||||||
|
serverId: "srv_current",
|
||||||
|
hostname: "current host",
|
||||||
|
}),
|
||||||
|
getClientId: async () => "cid_test_runtime",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await controller.start({ autoProbe: false });
|
||||||
|
|
||||||
|
expect(controller.getSnapshot().connectionStatus).toBe("connecting");
|
||||||
|
expect(controller.getSnapshot().activeConnectionId).toBeNull();
|
||||||
|
expect(controller.getSnapshot().probeByConnectionId.get("direct:localhost:6767")).toEqual({
|
||||||
|
status: "unavailable",
|
||||||
|
latencyMs: null,
|
||||||
|
});
|
||||||
|
expect(mismatchedClient.closeCalls).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails over when the active client ping fails", async () => {
|
||||||
const host = makeHost({ preferredConnectionId: "direct:lan:6767" });
|
const host = makeHost({ preferredConnectionId: "direct:lan:6767" });
|
||||||
const clients: FakeDaemonClient[] = [];
|
const clients: FakeDaemonClient[] = [];
|
||||||
const latencies: Record<string, number | Error> = {
|
const latencies: Record<string, number | Error> = {
|
||||||
@@ -450,6 +542,9 @@ describe("HostRuntimeController", () => {
|
|||||||
const initialClient = controller.getSnapshot().client;
|
const initialClient = controller.getSnapshot().client;
|
||||||
expect(initialClient).toBeTruthy();
|
expect(initialClient).toBeTruthy();
|
||||||
|
|
||||||
|
(initialClient as unknown as FakeDaemonClient).ping = async () => {
|
||||||
|
throw new Error("active ping failed");
|
||||||
|
};
|
||||||
latencies["direct:lan:6767"] = new Error("direct unavailable");
|
latencies["direct:lan:6767"] = new Error("direct unavailable");
|
||||||
latencies["relay:relay.paseo.sh:443"] = 42;
|
latencies["relay:relay.paseo.sh:443"] = 42;
|
||||||
clearProbeBackoff(controller);
|
clearProbeBackoff(controller);
|
||||||
@@ -838,7 +933,7 @@ describe("HostRuntimeController", () => {
|
|||||||
expect(createdClients[0]?.closeCalls).toBe(1);
|
expect(createdClients[0]?.closeCalls).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores stale probe results when overlapping probe cycles finish out of order", async () => {
|
it("coalesces overlapping probe cycles instead of invalidating the in-flight result", async () => {
|
||||||
const host = makeHost({
|
const host = makeHost({
|
||||||
connections: [
|
connections: [
|
||||||
{
|
{
|
||||||
@@ -849,7 +944,6 @@ describe("HostRuntimeController", () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
const slowProbe = createDeferred<number>();
|
const slowProbe = createDeferred<number>();
|
||||||
const fastProbe = createDeferred<number>();
|
|
||||||
let probeCalls = 0;
|
let probeCalls = 0;
|
||||||
|
|
||||||
const controller = new HostRuntimeController({
|
const controller = new HostRuntimeController({
|
||||||
@@ -865,9 +959,6 @@ describe("HostRuntimeController", () => {
|
|||||||
if (probeCalls === 1) {
|
if (probeCalls === 1) {
|
||||||
return { rttMs: await slowProbe.promise };
|
return { rttMs: await slowProbe.promise };
|
||||||
}
|
}
|
||||||
if (probeCalls === 2) {
|
|
||||||
return { rttMs: await fastProbe.promise };
|
|
||||||
}
|
|
||||||
throw new Error("unexpected probe call");
|
throw new Error("unexpected probe call");
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
@@ -883,23 +974,14 @@ describe("HostRuntimeController", () => {
|
|||||||
const first = controller.runProbeCycleNow();
|
const first = controller.runProbeCycleNow();
|
||||||
clearProbeBackoff(controller);
|
clearProbeBackoff(controller);
|
||||||
const second = controller.runProbeCycleNow();
|
const second = controller.runProbeCycleNow();
|
||||||
|
expect(probeCalls).toBe(1);
|
||||||
fastProbe.resolve(12);
|
|
||||||
await second;
|
|
||||||
const probeAfterSecond = controller.getSnapshot().probeByConnectionId.get("direct:lan:6767");
|
|
||||||
expect(probeAfterSecond).toEqual({
|
|
||||||
status: "available",
|
|
||||||
latencyMs: 12,
|
|
||||||
});
|
|
||||||
|
|
||||||
slowProbe.resolve(900);
|
slowProbe.resolve(900);
|
||||||
await first;
|
await Promise.all([first, second]);
|
||||||
const probeAfterFirstSettles = controller
|
const probeAfterCycle = controller.getSnapshot().probeByConnectionId.get("direct:lan:6767");
|
||||||
.getSnapshot()
|
expect(probeAfterCycle).toEqual({
|
||||||
.probeByConnectionId.get("direct:lan:6767");
|
|
||||||
expect(probeAfterFirstSettles).toEqual({
|
|
||||||
status: "available",
|
status: "available",
|
||||||
latencyMs: 12,
|
latencyMs: 900,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
connectionFromListen,
|
connectionFromListen,
|
||||||
normalizeStoredHostProfile,
|
normalizeStoredHostProfile,
|
||||||
upsertHostConnectionInProfiles,
|
upsertHostConnectionInProfiles,
|
||||||
registryHasDirectEndpoint,
|
|
||||||
type HostConnection,
|
type HostConnection,
|
||||||
type HostProfile,
|
type HostProfile,
|
||||||
} from "@/types/host-connection";
|
} from "@/types/host-connection";
|
||||||
@@ -489,6 +488,7 @@ export class HostRuntimeController {
|
|||||||
private clientIdHash: string | null = null;
|
private clientIdHash: string | null = null;
|
||||||
private switchRequestVersion = 0;
|
private switchRequestVersion = 0;
|
||||||
private probeRequestVersion = 0;
|
private probeRequestVersion = 0;
|
||||||
|
private probeCycleInFlight: Promise<void> | null = null;
|
||||||
|
|
||||||
constructor(input: {
|
constructor(input: {
|
||||||
host: HostProfile;
|
host: HostProfile;
|
||||||
@@ -629,6 +629,20 @@ export class HostRuntimeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async runProbeCycleNow(): Promise<void> {
|
async runProbeCycleNow(): Promise<void> {
|
||||||
|
if (this.probeCycleInFlight) {
|
||||||
|
return this.probeCycleInFlight;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cycle = this.runProbeCycle().finally(() => {
|
||||||
|
if (this.probeCycleInFlight === cycle) {
|
||||||
|
this.probeCycleInFlight = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.probeCycleInFlight = cycle;
|
||||||
|
return cycle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async runProbeCycle(): Promise<void> {
|
||||||
const requestVersion = ++this.probeRequestVersion;
|
const requestVersion = ++this.probeRequestVersion;
|
||||||
if (this.host.connections.length === 0) {
|
if (this.host.connections.length === 0) {
|
||||||
if (!this.isCurrentProbeRequest(requestVersion)) {
|
if (!this.isCurrentProbeRequest(requestVersion)) {
|
||||||
@@ -664,10 +678,15 @@ export class HostRuntimeController {
|
|||||||
const probeByConnectionId = new Map(this.snapshot.probeByConnectionId);
|
const probeByConnectionId = new Map(this.snapshot.probeByConnectionId);
|
||||||
for (const connection of connectionsToProbe) {
|
for (const connection of connectionsToProbe) {
|
||||||
this.connectionLastProbedAt.set(connection.id, performance.now());
|
this.connectionLastProbedAt.set(connection.id, performance.now());
|
||||||
probeByConnectionId.set(connection.id, {
|
const existingProbe = probeByConnectionId.get(connection.id);
|
||||||
status: "pending",
|
const shouldPreserveActiveLatency =
|
||||||
latencyMs: null,
|
isOnline && connection.id === activeConnectionId && existingProbe?.status === "available";
|
||||||
});
|
if (!shouldPreserveActiveLatency) {
|
||||||
|
probeByConnectionId.set(connection.id, {
|
||||||
|
status: "pending",
|
||||||
|
latencyMs: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.updateSnapshot({ probeByConnectionId: new Map(probeByConnectionId) });
|
this.updateSnapshot({ probeByConnectionId: new Map(probeByConnectionId) });
|
||||||
|
|
||||||
@@ -801,22 +820,39 @@ export class HostRuntimeController {
|
|||||||
for (const connection of connectionsToProbe) {
|
for (const connection of connectionsToProbe) {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
let connectedClient: DaemonClient | null = null;
|
let connectedClient: DaemonClient | null = null;
|
||||||
let handedOffClient = false;
|
let shouldCloseClient = false;
|
||||||
try {
|
try {
|
||||||
const { client } = await this.deps.connectToDaemon({
|
const activeClient =
|
||||||
host: this.host,
|
this.snapshot.connectionStatus === "online" &&
|
||||||
connection,
|
this.snapshot.activeConnectionId === connection.id
|
||||||
});
|
? this.snapshot.client
|
||||||
connectedClient = client;
|
: null;
|
||||||
|
|
||||||
|
if (activeClient) {
|
||||||
|
connectedClient = activeClient;
|
||||||
|
} else {
|
||||||
|
const { client, serverId } = await this.deps.connectToDaemon({
|
||||||
|
host: this.host,
|
||||||
|
connection,
|
||||||
|
});
|
||||||
|
if (serverId !== this.host.serverId) {
|
||||||
|
await client.close().catch(() => undefined);
|
||||||
|
throw new Error(
|
||||||
|
`Connection resolved to ${serverId}, expected ${this.host.serverId}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
connectedClient = client;
|
||||||
|
shouldCloseClient = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.isCurrentProbeRequest(requestVersion)) {
|
if (!this.isCurrentProbeRequest(requestVersion)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activated = await maybeActivateFirstAvailable(connection.id, client);
|
const activated = await maybeActivateFirstAvailable(connection.id, connectedClient);
|
||||||
handedOffClient = activated;
|
shouldCloseClient = shouldCloseClient && !activated;
|
||||||
|
|
||||||
const { rttMs } = await client.ping({ timeoutMs: 5000 });
|
const { rttMs } = await connectedClient.ping({ timeoutMs: 5000 });
|
||||||
if (!this.isCurrentProbeRequest(requestVersion)) {
|
if (!this.isCurrentProbeRequest(requestVersion)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -835,7 +871,7 @@ export class HostRuntimeController {
|
|||||||
publishProbeState();
|
publishProbeState();
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (connectedClient && !handedOffClient) {
|
if (connectedClient && shouldCloseClient) {
|
||||||
await connectedClient.close().catch(() => undefined);
|
await connectedClient.close().catch(() => undefined);
|
||||||
}
|
}
|
||||||
settleProbe();
|
settleProbe();
|
||||||
@@ -1100,7 +1136,6 @@ export class HostRuntimeController {
|
|||||||
|
|
||||||
const REGISTRY_STORAGE_KEY = "@paseo:daemon-registry";
|
const REGISTRY_STORAGE_KEY = "@paseo:daemon-registry";
|
||||||
const DEFAULT_LOCALHOST_ENDPOINT = process.env.EXPO_PUBLIC_LOCAL_DAEMON?.trim() || "localhost:6767";
|
const DEFAULT_LOCALHOST_ENDPOINT = process.env.EXPO_PUBLIC_LOCAL_DAEMON?.trim() || "localhost:6767";
|
||||||
const DEFAULT_LOCALHOST_BOOTSTRAP_KEY = "@paseo:default-localhost-bootstrap-v1";
|
|
||||||
const DEFAULT_LOCALHOST_BOOTSTRAP_TIMEOUT_MS = 2500;
|
const DEFAULT_LOCALHOST_BOOTSTRAP_TIMEOUT_MS = 2500;
|
||||||
const CONNECTION_ONLINE_TIMEOUT_MS = 15_000;
|
const CONNECTION_ONLINE_TIMEOUT_MS = 15_000;
|
||||||
const E2E_STORAGE_KEY = "@paseo:e2e";
|
const E2E_STORAGE_KEY = "@paseo:e2e";
|
||||||
@@ -1230,16 +1265,6 @@ export class HostRuntimeStore {
|
|||||||
|
|
||||||
private async bootstrapLocalhost(): Promise<void> {
|
private async bootstrapLocalhost(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const alreadyHandled = await AsyncStorage.getItem(DEFAULT_LOCALHOST_BOOTSTRAP_KEY);
|
|
||||||
if (alreadyHandled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (registryHasDirectEndpoint(this.hosts, DEFAULT_LOCALHOST_ENDPOINT)) {
|
|
||||||
await AsyncStorage.setItem(DEFAULT_LOCALHOST_BOOTSTRAP_KEY, "1");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { client, serverId, hostname } = await connectToDaemon(
|
const { client, serverId, hostname } = await connectToDaemon(
|
||||||
{
|
{
|
||||||
@@ -1256,7 +1281,6 @@ export class HostRuntimeStore {
|
|||||||
label: hostname ?? undefined,
|
label: hostname ?? undefined,
|
||||||
existingClient: client,
|
existingClient: client,
|
||||||
});
|
});
|
||||||
await AsyncStorage.setItem(DEFAULT_LOCALHOST_BOOTSTRAP_KEY, "1");
|
|
||||||
} catch {
|
} catch {
|
||||||
// Best-effort bootstrap only
|
// Best-effort bootstrap only
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,10 +61,12 @@ import { useIsCompactFormFactor } from "@/constants/layout";
|
|||||||
import { useLocalDaemonServerId } from "@/hooks/use-is-local-daemon";
|
import { useLocalDaemonServerId } from "@/hooks/use-is-local-daemon";
|
||||||
import {
|
import {
|
||||||
buildHostOpenProjectRoute,
|
buildHostOpenProjectRoute,
|
||||||
|
buildHostWorkspaceRoute,
|
||||||
buildSettingsHostRoute,
|
buildSettingsHostRoute,
|
||||||
buildSettingsSectionRoute,
|
buildSettingsSectionRoute,
|
||||||
type SettingsSectionSlug,
|
type SettingsSectionSlug,
|
||||||
} from "@/utils/host-routes";
|
} from "@/utils/host-routes";
|
||||||
|
import { getLastNavigationWorkspaceRouteSelection } from "@/stores/navigation-active-workspace-store";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// View model
|
// View model
|
||||||
@@ -720,6 +722,15 @@ export default function SettingsScreen({ view }: SettingsScreenProps) {
|
|||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
const handleBackToWorkspace = useCallback(() => {
|
const handleBackToWorkspace = useCallback(() => {
|
||||||
|
if (!isCompactLayout) {
|
||||||
|
const lastWorkspaceRoute = getLastNavigationWorkspaceRouteSelection();
|
||||||
|
if (lastWorkspaceRoute) {
|
||||||
|
router.replace(
|
||||||
|
buildHostWorkspaceRoute(lastWorkspaceRoute.serverId, lastWorkspaceRoute.workspaceId),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (router.canGoBack()) {
|
if (router.canGoBack()) {
|
||||||
router.back();
|
router.back();
|
||||||
return;
|
return;
|
||||||
@@ -729,7 +740,7 @@ export default function SettingsScreen({ view }: SettingsScreenProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
router.replace("/");
|
router.replace("/");
|
||||||
}, [anyOnlineServerId, router]);
|
}, [anyOnlineServerId, isCompactLayout, router]);
|
||||||
|
|
||||||
const detailHeader = ((): {
|
const detailHeader = ((): {
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user