feat: migrate from wsUrl to endpoints + add relay transport and pairing

This commit is contained in:
Mohamed Boudra
2026-01-21 14:55:23 +07:00
parent ae9a98223f
commit ddbcc272f1
29 changed files with 1729 additions and 280 deletions
+114
View File
@@ -0,0 +1,114 @@
import { Buffer } from "buffer";
export type HostPortParts = {
host: string;
port: number;
isIpv6: boolean;
};
function decodeBase64UrlToUtf8(input: string): string {
const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "=");
return Buffer.from(padded, "base64").toString("utf8");
}
export function decodeOfferFragmentPayload(encoded: string): unknown {
const json = decodeBase64UrlToUtf8(encoded);
return JSON.parse(json) as unknown;
}
export function parseHostPort(input: string): HostPortParts {
const trimmed = input.trim();
if (!trimmed) {
throw new Error("Host is required");
}
// IPv6: [::1]:6767
if (trimmed.startsWith("[")) {
const match = trimmed.match(/^\[([^\]]+)\]:(\d{1,5})$/);
if (!match) {
throw new Error("Invalid host:port (expected [::1]:6767)");
}
const host = match[1].trim();
const port = Number(match[2]);
if (!host) throw new Error("Host is required");
if (!Number.isInteger(port) || port < 1 || port > 65535) {
throw new Error("Port must be between 1 and 65535");
}
return { host, port, isIpv6: true };
}
const match = trimmed.match(/^(.+):(\d{1,5})$/);
if (!match) {
throw new Error("Invalid host:port (expected localhost:6767)");
}
const host = match[1].trim();
const port = Number(match[2]);
if (!host) throw new Error("Host is required");
if (!Number.isInteger(port) || port < 1 || port > 65535) {
throw new Error("Port must be between 1 and 65535");
}
return { host, port, isIpv6: false };
}
export function normalizeHostPort(input: string): string {
const { host, port, isIpv6 } = parseHostPort(input);
if (isIpv6) {
return `[${host}]:${port}`;
}
return `${host}:${port}`;
}
export function extractHostPortFromWebSocketUrl(wsUrl: string): string {
const parsed = new URL(wsUrl);
if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
throw new Error("Invalid WebSocket URL protocol");
}
if (parsed.pathname.replace(/\/+$/, "") !== "/ws") {
throw new Error("Invalid WebSocket URL (expected /ws path)");
}
const host = parsed.hostname;
const port = parsed.port ? Number(parsed.port) : parsed.protocol === "wss:" ? 443 : 80;
if (!host) {
throw new Error("Invalid WebSocket URL (missing hostname)");
}
if (!Number.isInteger(port) || port < 1 || port > 65535) {
throw new Error("Invalid WebSocket URL (invalid port)");
}
const isIpv6 = host.includes(":") && !host.startsWith("[") && !host.endsWith("]");
const hostPort = isIpv6 ? `[${host}]:${port}` : `${host}:${port}`;
return hostPort;
}
function shouldUseSecureWebSocket(port: number): boolean {
return port === 443;
}
export function buildDaemonWebSocketUrl(endpoint: string): string {
const { host, port, isIpv6 } = parseHostPort(endpoint);
const protocol = shouldUseSecureWebSocket(port) ? "wss" : "ws";
const hostPart = isIpv6 ? `[${host}]` : host;
return `${protocol}://${hostPart}:${port}/ws`;
}
export function buildRelayWebSocketUrl(params: { endpoint: string; sessionId: string }): string {
const { host, port, isIpv6 } = parseHostPort(params.endpoint);
const protocol = shouldUseSecureWebSocket(port) ? "wss" : "ws";
const hostPart = isIpv6 ? `[${host}]` : host;
const url = new URL(`${protocol}://${hostPart}:${port}/ws`);
url.searchParams.set("session", params.sessionId);
url.searchParams.set("role", "client");
return url.toString();
}
export function deriveLabelFromEndpoint(endpoint: string): string {
try {
const { host } = parseHostPort(endpoint);
return host || "Unnamed Host";
} catch {
return "Unnamed Host";
}
}
@@ -0,0 +1,232 @@
import type { DaemonTransport, DaemonTransportFactory } from "@server/client/daemon-client-v2";
type TauriWebSocketMessage =
| { type: "Text"; data: string }
| { type: "Binary"; data: number[] }
| { type: "Ping"; data: number[] }
| { type: "Pong"; data: number[] }
| { type: "Close"; data: { code: number; reason: string } | null };
type TauriWebSocketConnection = {
addListener(cb: (msg: TauriWebSocketMessage) => void): () => void;
send(message: string | number[] | TauriWebSocketMessage): Promise<void>;
disconnect(): Promise<void>;
};
type TauriWebSocketModule = {
connect(url: string, config?: unknown): Promise<TauriWebSocketConnection>;
};
function isTauriEnvironment(): boolean {
return typeof window !== "undefined" && (window as any).__TAURI__ !== undefined;
}
function getTauriWebSocketModule(): TauriWebSocketModule | null {
if (!isTauriEnvironment()) {
return null;
}
const ws = (window as any).__TAURI__?.websocket;
if (ws && typeof ws.connect === "function") {
return ws as TauriWebSocketModule;
}
return null;
}
export function createTauriWebSocketTransportFactory(): DaemonTransportFactory | null {
if (!isTauriEnvironment()) {
return null;
}
return ({ url }) => {
let ws: TauriWebSocketConnection | null = null;
let wsListenerCleanup: (() => void) | null = null;
let disposed = false;
let opened = false;
let closeEmitted = false;
const openHandlers = new Set<() => void>();
const closeHandlers = new Set<(event?: unknown) => void>();
const errorHandlers = new Set<(event?: unknown) => void>();
const messageHandlers = new Set<(data: unknown) => void>();
const pendingSends: string[] = [];
let closeRequested: { code?: number; reason?: string } | null = null;
const emitOpen = () => {
if (disposed || opened) return;
opened = true;
for (const handler of openHandlers) {
try {
handler();
} catch {
// no-op
}
}
};
const emitClose = (event?: unknown) => {
if (disposed || closeEmitted) return;
closeEmitted = true;
for (const handler of closeHandlers) {
try {
handler(event);
} catch {
// no-op
}
}
};
const emitError = (event?: unknown) => {
if (disposed) return;
for (const handler of errorHandlers) {
try {
handler(event);
} catch {
// no-op
}
}
};
const emitMessage = (data: unknown) => {
if (disposed) return;
for (const handler of messageHandlers) {
try {
handler(data);
} catch {
// no-op
}
}
};
const connect = async () => {
try {
let module: TauriWebSocketModule | null = getTauriWebSocketModule();
if (!module) {
const start = Date.now();
while (!module && Date.now() - start < 2000) {
await new Promise((r) => setTimeout(r, 50));
module = getTauriWebSocketModule();
}
}
if (!module) {
throw new Error(
"Tauri WebSocket plugin is not available (expected window.__TAURI__.websocket). Did you enable tauri-plugin-websocket and websocket:default capability?"
);
}
const socket = await module.connect(url);
if (disposed) {
try {
await socket.disconnect();
} catch {
// no-op
}
return;
}
ws = socket;
wsListenerCleanup = ws.addListener((msg) => {
if (msg.type === "Text") {
emitMessage({ data: msg.data });
return;
}
if (msg.type === "Close") {
emitClose(msg.data ?? undefined);
return;
}
});
emitOpen();
while (pendingSends.length > 0) {
const next = pendingSends.shift();
if (!next) break;
try {
await ws.send(next);
} catch (error) {
emitError(error);
}
}
if (closeRequested) {
try {
await ws.disconnect();
} catch (error) {
emitError(error);
} finally {
emitClose(closeRequested);
}
}
} catch (error) {
emitError(error);
}
};
void connect();
const transport: DaemonTransport = {
send: (data) => {
if (disposed) return;
if (!ws) {
pendingSends.push(data);
return;
}
void ws.send(data).catch((error) => emitError(error));
},
close: (code?: number, reason?: string) => {
if (disposed) return;
closeRequested = { code, reason };
if (!ws) return;
void ws
.disconnect()
.catch((error) => emitError(error))
.finally(() => emitClose(closeRequested));
},
onMessage: (handler) => {
messageHandlers.add(handler);
return () => messageHandlers.delete(handler);
},
onOpen: (handler) => {
openHandlers.add(handler);
if (opened) {
try {
handler();
} catch {
// no-op
}
}
return () => openHandlers.delete(handler);
},
onClose: (handler) => {
closeHandlers.add(handler);
if (closeEmitted) {
try {
handler(closeRequested ?? undefined);
} catch {
// no-op
}
}
return () => closeHandlers.delete(handler);
},
onError: (handler) => {
errorHandlers.add(handler);
return () => errorHandlers.delete(handler);
},
};
return {
...transport,
close: (code?: number, reason?: string) => {
transport.close(code, reason);
disposed = true;
try {
wsListenerCleanup?.();
} catch {
// no-op
}
wsListenerCleanup = null;
},
};
};
}