ServerConnection.key

This commit is contained in:
Brendan Allan
2026-02-17 12:56:02 +08:00
parent f30a73b80e
commit 30835b0fea
5 changed files with 56 additions and 81 deletions

View File

@@ -147,10 +147,10 @@ function ServerKey(props: ParentProps) {
export function AppInterface(props: {
children?: JSX.Element
defaultUrl: string
servers?: Array<ServerConnection.Any>
servers?: Array<ServerConnection.Ssh | ServerConnection.Sidecar>
}) {
return (
<ServerProvider defaultUrl={props.defaultUrl} servers={props.servers} isSidecar>
<ServerProvider defaultUrl={props.defaultUrl} servers={props.servers}>
<ServerKey>
<GlobalSDKProvider>
<GlobalSyncProvider>

View File

@@ -214,13 +214,14 @@ export function DialogSelectServer() {
})
}
const replaceServer = (original: ServerConnection.Http, next: string) => {
const replaceServer = (original: ServerConnection.Http, next: ServerConnection.HttpBase) => {
const active = server.url
const nextActive = active === original.http.url ? next : active
const newConn = server.add(next)
if (!newConn) return
server.add(next)
const nextActive = active === ServerConnection.key(original) ? ServerConnection.key(newConn) : active
if (nextActive) server.setActive(nextActive)
server.remove(original.http.url)
server.remove(ServerConnection.key(original))
}
const items = createMemo(() => {
@@ -273,11 +274,11 @@ export function DialogSelectServer() {
if (!persist && store.status[value.http.url]?.healthy === false) return
dialog.close()
if (persist) {
server.add(value.http.url)
server.add(value.http)
navigate("/")
return
}
server.setActive(value.http.url)
server.setActive(ServerConnection.key(value))
navigate("/")
}
@@ -346,7 +347,7 @@ export function DialogSelectServer() {
return
}
replaceServer(original, normalized)
replaceServer(original, { url: normalized })
resetEdit()
}
@@ -378,7 +379,7 @@ export function DialogSelectServer() {
handleEdit(original, store.editServer.value)
}
async function handleRemove(url: string) {
async function handleRemove(url: ServerConnection.Key) {
server.remove(url)
if ((await platform.getDefaultServerUrl?.()) === url) {
platform.setDefaultServerUrl?.(null)
@@ -506,7 +507,7 @@ export function DialogSelectServer() {
</Show>
<DropdownMenu.Separator />
<DropdownMenu.Item
onSelect={() => handleRemove(i.http.url)}
onSelect={() => handleRemove(ServerConnection.key(i))}
class="text-text-on-critical-base hover:bg-surface-critical-weak"
>
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel>

View File

@@ -12,7 +12,7 @@ import { ServerRow } from "@/components/server/server-row"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { useSDK } from "@/context/sdk"
import { normalizeServerUrl, type ServerConnection, useServer } from "@/context/server"
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
import { useSync } from "@/context/sync"
import { checkServerHealth, type ServerHealth } from "@/utils/server-health"
import { DialogSelectServer } from "./dialog-select-server"
@@ -262,7 +262,7 @@ export function StatusPopover() {
aria-disabled={isBlocked()}
onClick={() => {
if (isBlocked()) return
server.setActive(s.http.url)
server.setActive(ServerConnection.key(s))
navigate("/")
}}
>

View File

@@ -43,7 +43,7 @@ export namespace ServerConnection {
} & Base
export type Sidecar = {
type: "local"
type: "sidecar"
http: HttpBase
} & (
| // Regular desktop server
@@ -69,29 +69,32 @@ export namespace ServerConnection {
// All these are desktop-only
| (Sidecar | Ssh)
export const key = (conn: Any): string => {
export const key = (conn: Any): Key => {
switch (conn.type) {
case "http":
return conn.http.url
case "local": {
if (conn.variant === "wsl") return `wsl:${conn.distro}`
return "local"
return Key.make(conn.http.url)
case "sidecar": {
if (conn.variant === "wsl") return Key.make(`wsl:${conn.distro}`)
return Key.make("sidecar")
}
case "ssh":
return `ssh:${conn.host}`
return Key.make(`ssh:${conn.host}`)
}
}
export type Key = string & { _brand: "Key" }
export const Key = { make: (v: string) => v as Key }
}
export const { use: useServer, provider: ServerProvider } = createSimpleContext({
name: "Server",
init: (props: { defaultUrl: string; isSidecar?: boolean; servers?: Array<ServerConnection.Any> }) => {
init: (props: { defaultUrl: string; servers?: Array<ServerConnection.Ssh | ServerConnection.Sidecar> }) => {
const platform = usePlatform()
const [store, setStore, _, ready] = persisted(
Persist.global("server", ["server.v3"]),
createStore({
list: [] as string[],
list: [] as (string | ServerConnection.HttpBase)[],
projects: {} as Record<string, StoredProject[]>,
lastProject: {} as Record<string, string>,
}),
@@ -99,61 +102,19 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
const allServers = (): Array<ServerConnection.Any> => [
...(props.servers ?? []),
...store.list.map((url) => ({
...store.list.map((value) => ({
type: "http" as const,
http: { url },
http: typeof value === "string" ? { url: value } : value,
})),
]
const [state, setState] = createStore({
active: props.defaultUrl,
active: ServerConnection.key({ type: "http", http: { url: props.defaultUrl } }),
healthy: undefined as boolean | undefined,
})
const healthy = () => state.healthy
// const defaultUrl = () => normalizeServerUrl(props.defaultUrl)
function reconcileStartup() {
const fallback = props.defaultUrl
if (!fallback) return
// const previousSidecarUrl = normalizeServerUrl(store.currentSidecarUrl)
// const list = previousSidecarUrl ? store.list.filter((url) => url !== previousSidecarUrl) : store.list
// if (!props.isSidecar) {
// batch(() => {
// setStore("list", list)
// if (store.currentSidecarUrl) setStore("currentSidecarUrl", "")
setState("active", fallback)
// })
// return
// }
// const nextList = list.includes(fallback) ? list : [...list, fallback]
// batch(() => {
// setStore("list", nextList)
// setStore("currentSidecarUrl", fallback)
// setState("active", fallback)
// })
}
function updateServerList(url: string, remove = false) {
if (remove) {
const list = store.list.filter((x) => x !== url)
const next = state.active === url ? (list[0] ?? props.defaultUrl ?? "") : state.active
batch(() => {
setStore("list", list)
setState("active", next)
})
return
}
batch(() => {
if (!store.list.includes(url)) {
setStore("list", store.list.length, url)
}
setState("active", url)
})
}
function startHealthPolling(conn: ServerConnection.Any) {
let alive = true
let busy = false
@@ -179,30 +140,43 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
}
}
function setActive(input: string) {
function setActive(input: ServerConnection.Key) {
const url = normalizeServerUrl(input)
if (!url) return
setState("active", url)
}
function add(input: string) {
const url = normalizeServerUrl(input)
function add(input: ServerConnection.HttpBase) {
const url = normalizeServerUrl(input.url)
if (!url) return
updateServerList(url)
return batch(() => {
const http: ServerConnection.HttpBase = { ...input, url }
if (!store.list.includes(url)) {
setStore("list", store.list.length, http)
}
const conn: ServerConnection.Http = { type: "http", http }
setState("active", ServerConnection.key(conn))
return conn
})
}
function remove(input: string) {
function remove(input: ServerConnection.Key) {
const url = normalizeServerUrl(input)
if (!url) return
updateServerList(url, true)
const list = store.list.filter((x) => x !== url)
const next = state.active === url ? (list[0] ?? props.defaultUrl ?? "") : state.active
batch(() => {
setStore("list", list)
setState(
"active",
ServerConnection.key({
type: "http",
http: typeof next === "string" ? { url: next } : next,
}),
)
})
}
createEffect(() => {
if (!ready()) return
if (state.active) return
reconcileStartup()
})
const isReady = createMemo(() => ready() && !!state.active)
const fetcher = platform.fetch ?? globalThis.fetch

View File

@@ -447,9 +447,9 @@ render(() => {
<AppBaseProviders>
<ServerGate>
{(data) => {
const [servers] = createStore<Array<ServerConnection.Any>>([
const [servers] = createStore<Array<ServerConnection.Sidecar>>([
{
type: "local",
type: "sidecar",
variant: "base",
http: {
url: data().url,