0.1.1
This commit is contained in:
198
frontend/chat.ts
Normal file
198
frontend/chat.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import WebSocket from "@tauri-apps/plugin-websocket";
|
||||
import { PeerInfo } from './ts/bindings/PeerInfo';
|
||||
import { ClientWsMessage } from './ts/bindings/ClientWsMessage';
|
||||
import { ServerWsMessage } from './ts/bindings/ServerWsMessage';
|
||||
|
||||
type RemoveListener = (() => void) | null;
|
||||
|
||||
export class LocalSignallingClient {
|
||||
private socket: WebSocket | null = null;
|
||||
private removeListener: RemoveListener = null;
|
||||
private myPeerId: string | null = null;
|
||||
private peers: PeerInfo[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly displayName: string,
|
||||
private readonly onLog: (message: string) => void,
|
||||
private readonly onPeers: (peers: PeerInfo[]) => void,
|
||||
private readonly onChat: (line: string) => void,
|
||||
) {}
|
||||
|
||||
public async connect(): Promise<void> {
|
||||
if (this.socket !== null) {
|
||||
this.onLog("signalling already connected");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = "ws://127.0.0.1:3012";
|
||||
|
||||
this.onLog(`connecting to ${url}`);
|
||||
|
||||
let socket: WebSocket;
|
||||
try {
|
||||
socket = await WebSocket.connect(url);
|
||||
} catch (error) {
|
||||
throw new Error(`websocket connection failed: ${String(error)}`);
|
||||
}
|
||||
|
||||
this.socket = socket;
|
||||
|
||||
const removeListener = socket.addListener((message) => {
|
||||
if (message.type === "Text") {
|
||||
this.handleServerMessage(message.data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === "Close") {
|
||||
this.onLog("signalling disconnected");
|
||||
this.cleanupDisconnectedState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === "Binary") {
|
||||
this.onLog("unexpected binary websocket message");
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
this.removeListener = removeListener;
|
||||
|
||||
this.onLog("signalling connected");
|
||||
|
||||
await this.send({
|
||||
type: "hello",
|
||||
display_name: this.displayName,
|
||||
});
|
||||
}
|
||||
|
||||
public async disconnect(): Promise<void> {
|
||||
if (this.socket === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const socket = this.socket;
|
||||
|
||||
if (this.removeListener !== null) {
|
||||
this.removeListener();
|
||||
this.removeListener = null;
|
||||
}
|
||||
|
||||
this.socket = null;
|
||||
|
||||
try {
|
||||
await socket.disconnect();
|
||||
} catch (error) {
|
||||
this.onLog(`disconnect error: ${String(error)}`);
|
||||
}
|
||||
|
||||
this.cleanupDisconnectedState();
|
||||
}
|
||||
|
||||
public async sendChat(text: string): Promise<void> {
|
||||
await this.send({
|
||||
type: "chat_send",
|
||||
text,
|
||||
});
|
||||
}
|
||||
|
||||
public getMyPeerId(): string | null {
|
||||
return this.myPeerId;
|
||||
}
|
||||
|
||||
public getPeers(): PeerInfo[] {
|
||||
return [...this.peers];
|
||||
}
|
||||
|
||||
private async send(message: ClientWsMessage): Promise<void> {
|
||||
if (this.socket === null) {
|
||||
this.onLog("cannot send: signalling not connected");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = JSON.stringify(message);
|
||||
|
||||
try {
|
||||
await this.socket.send(payload);
|
||||
} catch (error) {
|
||||
this.onLog(`send failed: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
private cleanupDisconnectedState(): void {
|
||||
this.socket = null;
|
||||
this.myPeerId = null;
|
||||
this.peers = [];
|
||||
this.onPeers([]);
|
||||
}
|
||||
|
||||
private handleServerMessage(raw: string): void {
|
||||
let message: ServerWsMessage;
|
||||
|
||||
try {
|
||||
message = JSON.parse(raw) as ServerWsMessage;
|
||||
} catch (error) {
|
||||
this.onLog(`invalid server message: ${String(error)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (message.type) {
|
||||
case "welcome":
|
||||
this.myPeerId = message.peer_id;
|
||||
this.onLog(`welcome peer_id=${message.peer_id}`);
|
||||
break;
|
||||
|
||||
case "peer_list":
|
||||
this.peers = message.peers;
|
||||
this.onPeers([...this.peers]);
|
||||
this.onLog(`peer_list received (${message.peers.length})`);
|
||||
break;
|
||||
|
||||
case "peer_joined":
|
||||
this.peers = mergePeer(this.peers, message.peer);
|
||||
this.onPeers([...this.peers]);
|
||||
this.onLog(`peer joined: ${message.peer.display_name}`);
|
||||
break;
|
||||
|
||||
case "peer_left":
|
||||
this.peers = this.peers.filter((peer) => peer.peer_id !== message.peer_id);
|
||||
this.onPeers([...this.peers]);
|
||||
this.onLog(`peer left: ${message.peer_id}`);
|
||||
break;
|
||||
|
||||
case "chat_receive":
|
||||
this.onChat(`${message.from_display_name}: ${message.text}`);
|
||||
break;
|
||||
|
||||
case "offer":
|
||||
this.onLog(`offer received from ${message.from_peer_id}`);
|
||||
break;
|
||||
|
||||
case "answer":
|
||||
this.onLog(`answer received from ${message.from_peer_id}`);
|
||||
break;
|
||||
|
||||
case "ice_candidate":
|
||||
this.onLog(`ice candidate received from ${message.from_peer_id}`);
|
||||
break;
|
||||
|
||||
case "pong":
|
||||
this.onLog("pong");
|
||||
break;
|
||||
|
||||
case "error":
|
||||
this.onLog(`server error: ${message.message}`);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.onLog(`unhandled server message: ${raw}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mergePeer(peers: PeerInfo[], incoming: PeerInfo): PeerInfo[] {
|
||||
const next = peers.filter((peer) => peer.peer_id !== incoming.peer_id);
|
||||
next.push(incoming);
|
||||
next.sort((a, b) => a.display_name.localeCompare(b.display_name));
|
||||
return next;
|
||||
}
|
||||
Reference in New Issue
Block a user