187 lines
5.4 KiB
TypeScript
187 lines
5.4 KiB
TypeScript
import type { DaemonTransport, DaemonTransportFactory } from "@server/client/daemon-client";
|
|
import {
|
|
closeLocalTransportSession,
|
|
listenToLocalTransportEvents,
|
|
openLocalTransportSession,
|
|
sendLocalTransportMessage,
|
|
type LocalTransportTarget,
|
|
} from "@/desktop/managed-runtime/managed-runtime";
|
|
|
|
const LOCAL_TRANSPORT_SCHEME = "paseo+local:";
|
|
|
|
function encodeBinaryToBase64(data: Uint8Array | ArrayBuffer): string {
|
|
const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
|
|
let binary = "";
|
|
for (let index = 0; index < bytes.length; index += 1) {
|
|
binary += String.fromCharCode(bytes[index]!);
|
|
}
|
|
return globalThis.btoa(binary);
|
|
}
|
|
|
|
function decodeBase64ToBytes(base64: string): Uint8Array {
|
|
const binary = globalThis.atob(base64);
|
|
const bytes = new Uint8Array(binary.length);
|
|
for (let index = 0; index < binary.length; index += 1) {
|
|
bytes[index] = binary.charCodeAt(index);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
export function buildLocalDaemonTransportUrl(target: LocalTransportTarget): string {
|
|
const url = new URL(`${LOCAL_TRANSPORT_SCHEME}//${target.transportType}`);
|
|
url.searchParams.set("path", target.transportPath);
|
|
return url.toString();
|
|
}
|
|
|
|
function parseLocalDaemonTransportUrl(url: string): LocalTransportTarget {
|
|
const parsed = new URL(url);
|
|
if (parsed.protocol !== LOCAL_TRANSPORT_SCHEME) {
|
|
throw new Error(`Unsupported local transport URL: ${url}`);
|
|
}
|
|
const transportType = parsed.hostname;
|
|
const transportPath = parsed.searchParams.get("path")?.trim() ?? "";
|
|
if ((transportType !== "socket" && transportType !== "pipe") || !transportPath) {
|
|
throw new Error(`Invalid local transport target: ${url}`);
|
|
}
|
|
return {
|
|
transportType,
|
|
transportPath,
|
|
};
|
|
}
|
|
|
|
export function createTauriLocalDaemonTransportFactory(): DaemonTransportFactory | null {
|
|
return ({ url }) => {
|
|
const target = parseLocalDaemonTransportUrl(url);
|
|
let sessionId: string | null = null;
|
|
let unlisten: (() => void) | null = null;
|
|
let disposed = false;
|
|
let didEmitOpen = 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 emitOpen = () => {
|
|
if (didEmitOpen || disposed) {
|
|
return;
|
|
}
|
|
didEmitOpen = true;
|
|
for (const handler of openHandlers) {
|
|
handler();
|
|
}
|
|
};
|
|
const emitClose = (event?: unknown) => {
|
|
for (const handler of closeHandlers) {
|
|
handler(event);
|
|
}
|
|
};
|
|
const emitError = (event?: unknown) => {
|
|
for (const handler of errorHandlers) {
|
|
handler(event);
|
|
}
|
|
};
|
|
const emitMessage = (data: unknown) => {
|
|
for (const handler of messageHandlers) {
|
|
handler(data);
|
|
}
|
|
};
|
|
|
|
void listenToLocalTransportEvents((payload) => {
|
|
if (disposed || !sessionId || payload.sessionId !== sessionId) {
|
|
return;
|
|
}
|
|
if (payload.kind === "open") {
|
|
emitOpen();
|
|
return;
|
|
}
|
|
if (payload.kind === "message") {
|
|
if (payload.text) {
|
|
emitMessage({ data: payload.text });
|
|
return;
|
|
}
|
|
if (payload.binaryBase64) {
|
|
emitMessage({ data: decodeBase64ToBytes(payload.binaryBase64) });
|
|
}
|
|
return;
|
|
}
|
|
if (payload.kind === "close") {
|
|
emitClose(payload);
|
|
return;
|
|
}
|
|
emitError(payload.error ?? "Local daemon transport error");
|
|
})
|
|
.then((cleanup) => {
|
|
if (disposed) {
|
|
cleanup();
|
|
return;
|
|
}
|
|
unlisten = cleanup;
|
|
})
|
|
.catch((error) => {
|
|
emitError(error);
|
|
});
|
|
|
|
void openLocalTransportSession(target)
|
|
.then((id) => {
|
|
if (disposed) {
|
|
void closeLocalTransportSession(id).catch((error) => emitError(error));
|
|
return;
|
|
}
|
|
sessionId = id;
|
|
emitOpen();
|
|
})
|
|
.catch((error) => {
|
|
emitError(error);
|
|
});
|
|
|
|
const transport: DaemonTransport = {
|
|
send: (data) => {
|
|
if (!sessionId) {
|
|
return;
|
|
}
|
|
if (typeof data === "string") {
|
|
void sendLocalTransportMessage({ sessionId, text: data }).catch((error) =>
|
|
emitError(error)
|
|
);
|
|
return;
|
|
}
|
|
const binaryBase64 = encodeBinaryToBase64(
|
|
data instanceof ArrayBuffer ? data : new Uint8Array(data)
|
|
);
|
|
void sendLocalTransportMessage({ sessionId, binaryBase64 }).catch((error) =>
|
|
emitError(error)
|
|
);
|
|
},
|
|
close: () => {
|
|
disposed = true;
|
|
const currentSessionId = sessionId;
|
|
sessionId = null;
|
|
if (currentSessionId) {
|
|
void closeLocalTransportSession(currentSessionId).catch((error) => emitError(error));
|
|
}
|
|
unlisten?.();
|
|
unlisten = null;
|
|
},
|
|
onMessage: (handler) => {
|
|
messageHandlers.add(handler);
|
|
return () => messageHandlers.delete(handler);
|
|
},
|
|
onOpen: (handler) => {
|
|
openHandlers.add(handler);
|
|
return () => openHandlers.delete(handler);
|
|
},
|
|
onClose: (handler) => {
|
|
closeHandlers.add(handler);
|
|
return () => closeHandlers.delete(handler);
|
|
},
|
|
onError: (handler) => {
|
|
errorHandlers.add(handler);
|
|
return () => errorHandlers.delete(handler);
|
|
},
|
|
};
|
|
|
|
return transport;
|
|
};
|
|
}
|