fix state pollution and other bugs, and random other stuff
This commit is contained in:
30
ORGANIZATION.md
Normal file
30
ORGANIZATION.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
This document lists the current state of files in this repository. This will serve as a tool for me to reorganize my code and make it easier to find things.
|
||||||
|
|
||||||
|
## Directories
|
||||||
|
|
||||||
|
### /src
|
||||||
|
|
||||||
|
This is the SvelteKit project.
|
||||||
|
|
||||||
|
- **lib/**:
|
||||||
|
- **webrtc.ts**: Holds the WebRTCPeer class, which is used to handle WebRTC connections. It is the place where encryption and decryption is handled.
|
||||||
|
- **shared/**:
|
||||||
|
- **keyConfig.ts**: Holds the configuration for the RSA key pair used for wrapping the unique AES-GCM key for each
|
||||||
|
message, literally nothing else.
|
||||||
|
- **stores/**:
|
||||||
|
- **messageStore.ts**: Holds the messages that are sent between the client and the peer.
|
||||||
|
- **roomStore.ts**: Holds the room information, such as the room ID, the number of participants, and the connection state.
|
||||||
|
- **websocketStore.ts**: Holds the WebSocket connection.
|
||||||
|
- **types/**:
|
||||||
|
- **message.ts**: Defines the types of application messages that are sent between the client and the peer via WebRTC
|
||||||
|
post initialization.
|
||||||
|
- **webrtc.ts**: Defines the WebRTCPeerCallbacks, the WebRTCPacketType, the structure of the WebRTCPacket (even
|
||||||
|
though all WebRTC packets are binary data), and the structure of the KeyStore.
|
||||||
|
- **websocket.ts**: Defines the WebSocketMessageType, and the types for each message along with the union.
|
||||||
|
- **utils/**:
|
||||||
|
- **webrtcUtil.ts**: This file feels like a hodgepodge of random shit. Its responsible for handling application messages that come from the
|
||||||
|
data channel, as well as handling the websocket signaling and room notifications. It need to be usable by both peers.
|
||||||
|
|
||||||
|
### /server
|
||||||
|
|
||||||
|
This is the server that handles the webrtc signaling.
|
||||||
@@ -1,22 +1,61 @@
|
|||||||
import { WebSocketServer } from "ws";
|
import { WebSocketServer } from "ws";
|
||||||
import type { WebSocket } from "ws";
|
import { Socket, WebSocketMessageType, type WebSocketMessage } from "../src/types/websocket";
|
||||||
import { SocketMessageType, type SocketMessage } from "../src/types/websocket";
|
import { LiveMap } from '../src/utils/liveMap.ts';
|
||||||
|
|
||||||
// TODO: remove stale rooms somehow
|
export class ServerRoom {
|
||||||
const rooms = new Map<string, WebSocket[]>();
|
private clients: Socket[] = [];
|
||||||
|
|
||||||
async function createRoom(socket: WebSocket): Promise<string> {
|
constructor(clients?: Socket[]) {
|
||||||
|
if (clients) {
|
||||||
|
this.clients = clients;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyAll(message: WebSocketMessage) {
|
||||||
|
this.clients.forEach(client => {
|
||||||
|
client.send(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get length(): number {
|
||||||
|
return this.clients.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
push(client: Socket) {
|
||||||
|
this.clients.push(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
set(clients: Socket[]) {
|
||||||
|
this.clients = clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
filter(callback: (client: Socket) => boolean): Socket[] {
|
||||||
|
return this.clients.filter(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
forEachClient(callback: (client: Socket) => void) {
|
||||||
|
this.clients.forEach(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rooms = new LiveMap<string, ServerRoom>();
|
||||||
|
|
||||||
|
async function createRoom(socket: Socket): Promise<string> {
|
||||||
let roomId = Math.random().toString(36).substring(2, 10);
|
let roomId = Math.random().toString(36).substring(2, 10);
|
||||||
rooms.set(roomId, []);
|
let room = rooms.set(roomId, new ServerRoom());
|
||||||
|
|
||||||
socket.send(JSON.stringify({ type: SocketMessageType.ROOM_CREATED, data: roomId }));
|
socket.send({ type: WebSocketMessageType.ROOM_CREATED, data: room.key });
|
||||||
|
|
||||||
await joinRoom(roomId, socket);
|
try {
|
||||||
|
await joinRoom(room.key, socket);
|
||||||
|
} catch (e: any) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
return roomId;
|
return roomId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function joinRoom(roomId: string, socket: WebSocket) {
|
async function joinRoom(roomId: string, socket: Socket): Promise<ServerRoom> {
|
||||||
let room = rooms.get(roomId);
|
let room = rooms.get(roomId);
|
||||||
console.log(room?.length);
|
console.log(room?.length);
|
||||||
|
|
||||||
@@ -26,22 +65,21 @@ async function joinRoom(roomId: string, socket: WebSocket) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (room.length == 2) {
|
if (room.length == 2) {
|
||||||
socket.send(JSON.stringify({ type: SocketMessageType.ERROR, data: 'Room is full' }));
|
throw new Error("Room is full");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// notify all clients in the room of the new client, except the client itself
|
// notify all clients in the room of the new client, except the client itself
|
||||||
room.forEach(client => {
|
room.notifyAll({ type: WebSocketMessageType.JOIN_ROOM, roomId });
|
||||||
client.send(JSON.stringify({ type: SocketMessageType.JOIN_ROOM, data: roomId }));
|
|
||||||
});
|
|
||||||
room.push(socket);
|
room.push(socket);
|
||||||
|
|
||||||
socket.addEventListener('close', (ev) => {
|
socket.addEventListener('close', (ev) => {
|
||||||
room = rooms.get(roomId)
|
room = rooms.get(roomId)
|
||||||
if (!room) {
|
if (!room) {
|
||||||
return;
|
throw new Error("Room not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
room.notifyAll({ type: WebSocketMessageType.ROOM_LEFT, roomId });
|
||||||
|
|
||||||
// for some reason, when you filter the array when the length is 1 it stays at 1, but we *know* that if its 1
|
// for some reason, when you filter the array when the length is 1 it stays at 1, but we *know* that if its 1
|
||||||
// then when this client disconnects, the room should be deleted since the room is empty
|
// then when this client disconnects, the room should be deleted since the room is empty
|
||||||
if (room.length === 1) {
|
if (room.length === 1) {
|
||||||
@@ -52,33 +90,33 @@ async function joinRoom(roomId: string, socket: WebSocket) {
|
|||||||
deleteRoom(roomId);
|
deleteRoom(roomId);
|
||||||
}
|
}
|
||||||
}, 5000)
|
}, 5000)
|
||||||
deleteRoom(roomId);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
rooms.set(roomId, room.filter(client => client !== ev.target));
|
room.set(room.filter(client => client.ws !== ev.target));
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: consider letting rooms get larger than 2 clients
|
// TODO: consider letting rooms get larger than 2 clients
|
||||||
if (room.length == 2) {
|
if (room.length == 2) {
|
||||||
room.forEach(async client => {
|
room.forEachClient(client => client.send({ type: WebSocketMessageType.ROOM_READY, data: { isInitiator: client !== socket } }));
|
||||||
// announce the room is ready, and tell each peer if they are the initiator
|
|
||||||
client.send(JSON.stringify({ type: SocketMessageType.ROOM_READY, data: { isInitiator: client !== socket } }));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Room created:", roomId, room.length);
|
console.log("Room created:", roomId, room.length);
|
||||||
|
|
||||||
|
return room;
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteRoom(roomId: string) {
|
function deleteRoom(roomId: string) {
|
||||||
rooms.delete(roomId);
|
rooms.delete(roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function confgiureWebsocketServer(ws: WebSocketServer) {
|
export function confgiureWebsocketServer(wss: WebSocketServer) {
|
||||||
ws.on('connection', socket => {
|
wss.on('connection', ws => {
|
||||||
|
let socket = new Socket(ws);
|
||||||
|
|
||||||
// Handle messages from the client
|
// Handle messages from the client
|
||||||
socket.on('message', async event => {
|
ws.on('message', async event => {
|
||||||
let message: SocketMessage | undefined = undefined;
|
let message: WebSocketMessage | undefined = undefined;
|
||||||
|
|
||||||
if (event instanceof Buffer) { // Assuming JSON is sent as a string
|
if (event instanceof Buffer) { // Assuming JSON is sent as a string
|
||||||
try {
|
try {
|
||||||
@@ -91,50 +129,57 @@ export function confgiureWebsocketServer(ws: WebSocketServer) {
|
|||||||
if (message === undefined) {
|
if (message === undefined) {
|
||||||
console.log("Received non-JSON message:", event);
|
console.log("Received non-JSON message:", event);
|
||||||
// If the message is not JSON, send an error message
|
// If the message is not JSON, send an error message
|
||||||
socket.send(JSON.stringify({ type: SocketMessageType.ERROR, data: 'Invalid message' }));
|
socket.send({ type: WebSocketMessageType.ERROR, data: 'Invalid message' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let room: ServerRoom | undefined = undefined;
|
||||||
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case SocketMessageType.CREATE_ROOM:
|
case WebSocketMessageType.CREATE_ROOM:
|
||||||
// else, create a new room
|
// else, create a new room
|
||||||
await createRoom(socket);
|
try {
|
||||||
|
await createRoom(socket);
|
||||||
|
} catch (e: any) {
|
||||||
|
socket.send({ type: WebSocketMessageType.ERROR, data: e.message });
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case SocketMessageType.JOIN_ROOM:
|
case WebSocketMessageType.JOIN_ROOM:
|
||||||
// if join message has a roomId, join the room
|
// if join message has a roomId, join the room
|
||||||
if (!message.roomId) {
|
if (!message.roomId) {
|
||||||
socket.send(JSON.stringify({ type: SocketMessageType.ERROR, data: 'Invalid message' }));
|
socket.send({ type: WebSocketMessageType.ERROR, data: 'Invalid message' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the user tries to join a room that doesnt exist, send an error message
|
// if the user tries to join a room that doesnt exist, send an error message
|
||||||
if (rooms.get(message.roomId) == undefined) {
|
if (rooms.get(message.roomId) == undefined) {
|
||||||
socket.send(JSON.stringify({ type: SocketMessageType.ERROR, data: 'Invalid roomId' }));
|
socket.send({ type: WebSocketMessageType.ERROR, data: 'Invalid roomId' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await joinRoom(message.roomId, socket);
|
room = await joinRoom(message.roomId, socket);
|
||||||
|
|
||||||
// the client is now in the room and the peer knows about it
|
// the client is now in the room and the peer knows about it
|
||||||
socket.send(JSON.stringify({ type: SocketMessageType.ROOM_JOINED, roomId: message.roomId }));
|
socket.send({ type: WebSocketMessageType.ROOM_JOINED, roomId: message.roomId, participants: room.length });
|
||||||
break;
|
break;
|
||||||
case SocketMessageType.OFFER:
|
case WebSocketMessageType.WEBRTC_OFFER:
|
||||||
case SocketMessageType.ANSWER:
|
case WebSocketMessageType.WERTC_ANSWER:
|
||||||
case SocketMessageType.ICE_CANDIDATE:
|
case WebSocketMessageType.WEBRTC_ICE_CANDIDATE:
|
||||||
// relay these messages to the other peers in the room
|
// relay these messages to the other peers in the room
|
||||||
const room = rooms.get(message.data.roomId);
|
room = rooms.get(message.data.roomId);
|
||||||
|
|
||||||
if (room) {
|
if (room) {
|
||||||
room.forEach(client => {
|
room.forEachClient(client => {
|
||||||
if (client !== socket) {
|
if (client !== socket) {
|
||||||
client.send(JSON.stringify(message));
|
client.send(message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.warn(`Unknown message type: ${message.type}`);
|
console.warn(`Unknown message type: ${message.type}`);
|
||||||
socket.send(JSON.stringify({ type: SocketMessageType.ERROR, data: 'Unknown message type' }));
|
socket.send({ type: WebSocketMessageType.ERROR, data: 'Unknown message type' });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { writable, type Writable } from "svelte/store";
|
import { derived, writable, type Writable } from "svelte/store";
|
||||||
import { room } from "../stores/roomStore";
|
// import { room } from "../stores/roomStore";
|
||||||
import { webSocketConnected } from "../stores/websocketStore";
|
import { webSocketConnected, ws } from "../stores/websocketStore";
|
||||||
import {
|
import {
|
||||||
isRTCConnected,
|
isRTCConnected,
|
||||||
dataChannelReady,
|
dataChannelReady,
|
||||||
@@ -10,13 +10,26 @@
|
|||||||
} from "../utils/webrtcUtil";
|
} from "../utils/webrtcUtil";
|
||||||
import { messages } from "../stores/messageStore";
|
import { messages } from "../stores/messageStore";
|
||||||
import { WebRTCPacketType } from "../types/webrtc";
|
import { WebRTCPacketType } from "../types/webrtc";
|
||||||
import { ConnectionState } from "../types/websocket";
|
import { ConnectionState, type Room } from "../types/websocket";
|
||||||
import { MessageType } from "../types/message";
|
import { MessageType } from "../types/message";
|
||||||
import { fade } from "svelte/transition";
|
import { fade } from "svelte/transition";
|
||||||
|
|
||||||
let inputMessage: Writable<string> = writable("");
|
let inputMessage: Writable<string> = writable("");
|
||||||
let inputFile = writable(null);
|
let inputFile = writable(null);
|
||||||
let inputFileElement: HTMLInputElement;
|
let inputFileElement: HTMLInputElement | null = $state(null);
|
||||||
|
let initialConnectionComplete = derived(
|
||||||
|
[isRTCConnected, dataChannelReady, keyExchangeDone],
|
||||||
|
(values: Array<boolean>) => values.every((value) => value),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { room }: { room: Writable<Room> } = $props();
|
||||||
|
|
||||||
|
room.subscribe((newRoom) => {
|
||||||
|
console.log("Room changed:", newRoom);
|
||||||
|
if (newRoom.id !== $room?.id) {
|
||||||
|
messages.set([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function sendMessage() {
|
function sendMessage() {
|
||||||
if (!$peer) {
|
if (!$peer) {
|
||||||
@@ -68,21 +81,29 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function pickFile() {
|
function pickFile() {
|
||||||
|
if (!inputFileElement) return;
|
||||||
|
|
||||||
inputFileElement.click();
|
inputFileElement.click();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p>{$room?.id} - {$room?.connectionState} - {$webSocketConnected}</p>
|
<p>
|
||||||
|
{$room?.id}
|
||||||
|
({$room?.participants}) - {$room?.connectionState} - {$webSocketConnected}
|
||||||
|
- Initial connection {$initialConnectionComplete
|
||||||
|
? "complete"
|
||||||
|
: "incomplete"}
|
||||||
|
</p>
|
||||||
|
|
||||||
<!-- If we are in a room, connected to the websocket server, and the have been informed that we are connected to the room -->
|
<!-- If we are in a room, connected to the websocket server, and have been informed that we are connected to the room -->
|
||||||
{#if $room !== null && $webSocketConnected === true && $room.connectionState === ConnectionState.CONNECTED}
|
{#if ($room !== null && $webSocketConnected === true && $room.connectionState === ConnectionState.CONNECTED) || $room.connectionState === ConnectionState.RECONNECTING}
|
||||||
<div
|
<div
|
||||||
class="flex flex-col sm:max-w-4/5 lg:max-w-3/5 min-h-[calc(5/12_*_100vh)]"
|
class="flex flex-col sm:max-w-4/5 lg:max-w-3/5 min-h-[calc(5/12_*_100vh)]"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex-grow flex flex-col overflow-y-auto mb-4 p-2 bg-gray-800 rounded break-all relative"
|
class="flex-grow flex flex-col overflow-y-auto mb-4 p-2 bg-gray-800 rounded break-all relative"
|
||||||
>
|
>
|
||||||
{#if !$isRTCConnected || !$dataChannelReady || !$keyExchangeDone || !$canCloseLoadingOverlay}
|
{#if !$initialConnectionComplete || $room.connectionState === ConnectionState.RECONNECTING || $room.participants !== 2 || !$canCloseLoadingOverlay}
|
||||||
<div
|
<div
|
||||||
transition:fade={{ duration: 300 }}
|
transition:fade={{ duration: 300 }}
|
||||||
class="absolute top-0 left-0 bottom-0 right-0 flex justify-center items-center flex-col bg-black/55 backdrop-blur-md"
|
class="absolute top-0 left-0 bottom-0 right-0 flex justify-center items-center flex-col bg-black/55 backdrop-blur-md"
|
||||||
@@ -93,6 +114,15 @@
|
|||||||
<p>Establishing data channel...</p>
|
<p>Establishing data channel...</p>
|
||||||
{:else if !$keyExchangeDone}
|
{:else if !$keyExchangeDone}
|
||||||
<p>Establishing a secure connection with the peer...</p>
|
<p>Establishing a secure connection with the peer...</p>
|
||||||
|
{:else if $room.connectionState === ConnectionState.RECONNECTING}
|
||||||
|
<p>
|
||||||
|
Disconnect from peer, attempting to reconnecting...
|
||||||
|
</p>
|
||||||
|
{:else if $room.participants !== 2}
|
||||||
|
<p>
|
||||||
|
Peer has disconnected, waiting for other peer to
|
||||||
|
reconnect...
|
||||||
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
<p>
|
<p>
|
||||||
Successfully established a secure connection to
|
Successfully established a secure connection to
|
||||||
@@ -100,7 +130,7 @@
|
|||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
{#if !$keyExchangeDone}
|
{#if !$keyExchangeDone || $room.participants !== 2 || $room.connectionState === ConnectionState.RECONNECTING}
|
||||||
<!-- loading spinner -->
|
<!-- loading spinner -->
|
||||||
<svg
|
<svg
|
||||||
class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
||||||
@@ -171,19 +201,21 @@
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={$inputMessage}
|
bind:value={$inputMessage}
|
||||||
on:keyup={(e) => e.key === "Enter" && sendMessage()}
|
onkeyup={(e) => e.key === "Enter" && sendMessage()}
|
||||||
disabled={!$isRTCConnected ||
|
disabled={!$isRTCConnected ||
|
||||||
!$dataChannelReady ||
|
!$dataChannelReady ||
|
||||||
!$keyExchangeDone}
|
!$keyExchangeDone ||
|
||||||
|
$room.connectionState === ConnectionState.RECONNECTING}
|
||||||
placeholder="Type your message..."
|
placeholder="Type your message..."
|
||||||
class="flex-grow p-2 rounded bg-gray-700 border border-gray-600 text-gray-100 placeholder-gray-400
|
class="flex-grow p-2 rounded bg-gray-700 border border-gray-600 text-gray-100 placeholder-gray-400
|
||||||
focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
on:click={pickFile}
|
onclick={pickFile}
|
||||||
disabled={!$isRTCConnected ||
|
disabled={!$isRTCConnected ||
|
||||||
!$dataChannelReady ||
|
!$dataChannelReady ||
|
||||||
!$keyExchangeDone}
|
!$keyExchangeDone ||
|
||||||
|
$room.connectionState === ConnectionState.RECONNECTING}
|
||||||
aria-label="Pick file"
|
aria-label="Pick file"
|
||||||
class="px-4 py-2 bg-blue-600 not-disabled:hover:bg-blue-700 text-white rounded disabled:opacity-50 disabled:cursor-not-allowed"
|
class="px-4 py-2 bg-blue-600 not-disabled:hover:bg-blue-700 text-white rounded disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
@@ -203,10 +235,11 @@
|
|||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
on:click={sendMessage}
|
onclick={sendMessage}
|
||||||
disabled={!$isRTCConnected ||
|
disabled={!$isRTCConnected ||
|
||||||
!$dataChannelReady ||
|
!$dataChannelReady ||
|
||||||
!$keyExchangeDone}
|
!$keyExchangeDone ||
|
||||||
|
$room.connectionState === ConnectionState.RECONNECTING}
|
||||||
class="px-4 py-2 bg-blue-600 not-disabled:hover:bg-blue-700 text-white rounded disabled:opacity-50 disabled:cursor-not-allowed"
|
class="px-4 py-2 bg-blue-600 not-disabled:hover:bg-blue-700 text-white rounded disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
Send
|
Send
|
||||||
@@ -214,3 +247,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
$ws.close();
|
||||||
|
}}>Simulate disconnect</button
|
||||||
|
>
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
// place files you want to import through the `$lib` alias in this folder.
|
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { WebSocketMessageType, ws } from '../stores/websocketStore';
|
import { ws } from '../stores/websocketStore';
|
||||||
|
import { WebSocketMessageType } from '../types/websocket';
|
||||||
import { WebRTCPacketType, type KeyStore, type WebRTCPeerCallbacks } from '../types/webrtc';
|
import { WebRTCPacketType, type KeyStore, type WebRTCPeerCallbacks } from '../types/webrtc';
|
||||||
import { clientKeyConfig } from '../shared/keyConfig';
|
import { clientKeyConfig } from '../shared/keyConfig';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
export class WebRTCPeer {
|
export class WebRTCPeer {
|
||||||
private peer: RTCPeerConnection | null = null;
|
private peer: RTCPeerConnection | null = null;
|
||||||
@@ -38,6 +40,8 @@ export class WebRTCPeer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async initialize() {
|
public async initialize() {
|
||||||
|
if (!browser) throw new Error("Cannot initialize WebRTCPeer in non-browser environment");
|
||||||
|
|
||||||
// dont initialize twice
|
// dont initialize twice
|
||||||
if (this.peer) return;
|
if (this.peer) return;
|
||||||
|
|
||||||
@@ -115,14 +119,9 @@ export class WebRTCPeer {
|
|||||||
|
|
||||||
console.log("Received key exchange", data.buffer);
|
console.log("Received key exchange", data.buffer);
|
||||||
|
|
||||||
const textDecoder = new TextDecoder();
|
|
||||||
const jsonKey = JSON.parse(textDecoder.decode(data));
|
|
||||||
|
|
||||||
console.log("Received key exchange", jsonKey);
|
|
||||||
|
|
||||||
this.keys.peersPublicKey = await window.crypto.subtle.importKey(
|
this.keys.peersPublicKey = await window.crypto.subtle.importKey(
|
||||||
"jwk",
|
"spki",
|
||||||
jsonKey,
|
data.buffer,
|
||||||
clientKeyConfig,
|
clientKeyConfig,
|
||||||
true,
|
true,
|
||||||
["wrapKey"],
|
["wrapKey"],
|
||||||
@@ -276,14 +275,12 @@ export class WebRTCPeer {
|
|||||||
|
|
||||||
console.log("exporting key", this.keys.localKeys.publicKey);
|
console.log("exporting key", this.keys.localKeys.publicKey);
|
||||||
|
|
||||||
const exported = await window.crypto.subtle.exportKey("jwk", this.keys.localKeys.publicKey);
|
const exported = await window.crypto.subtle.exportKey("spki", this.keys.localKeys.publicKey);
|
||||||
|
|
||||||
// convert exported key to a string then pack that sting into an array buffer
|
// convert exported key to a string then pack that sting into an array buffer
|
||||||
const exportedKeyBuffer = new TextEncoder().encode(JSON.stringify(exported));
|
console.log("exported key buffer", exported);
|
||||||
|
|
||||||
console.log("exported key buffer", exportedKeyBuffer);
|
this.send(exported, WebRTCPacketType.KEY_EXCHANGE);
|
||||||
|
|
||||||
this.send(exportedKeyBuffer.buffer, WebRTCPacketType.KEY_EXCHANGE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async encrypt(data: Uint8Array<ArrayBuffer>, key: CryptoKey, iv: Uint8Array<ArrayBuffer>): Promise<ArrayBuffer> {
|
private async encrypt(data: Uint8Array<ArrayBuffer>, key: CryptoKey, iv: Uint8Array<ArrayBuffer>): Promise<ArrayBuffer> {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '../app.css';
|
import "../app.css";
|
||||||
import favicon from '$lib/assets/favicon.svg';
|
import favicon from "$lib/assets/favicon.svg";
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<link rel="icon" href={favicon} />
|
<link rel="icon" href={favicon} />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import { ws, webSocketConnected } from "../stores/websocketStore";
|
||||||
ws,
|
import { WebSocketMessageType } from "../types/websocket";
|
||||||
webSocketConnected,
|
|
||||||
WebSocketMessageType,
|
|
||||||
} from "../stores/websocketStore";
|
|
||||||
import { room } from "../stores/roomStore";
|
import { room } from "../stores/roomStore";
|
||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
import { peer, handleMessage } from "../utils/webrtcUtil";
|
import { peer, handleMessage } from "../utils/webrtcUtil";
|
||||||
@@ -12,10 +9,6 @@
|
|||||||
import { ConnectionState } from "../types/websocket";
|
import { ConnectionState } from "../types/websocket";
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
room.update((room) => ({
|
|
||||||
...room,
|
|
||||||
connectionState: ConnectionState.CONNECTING,
|
|
||||||
}));
|
|
||||||
$ws.addEventListener("message", handleMessage);
|
$ws.addEventListener("message", handleMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -38,7 +31,14 @@
|
|||||||
|
|
||||||
{#if $webSocketConnected}
|
{#if $webSocketConnected}
|
||||||
<button
|
<button
|
||||||
on:click={() => {
|
onclick={() => {
|
||||||
|
// if we are in a room already, leave it
|
||||||
|
if ($room.id) {
|
||||||
|
$ws.send({
|
||||||
|
type: WebSocketMessageType.LEAVE_ROOM,
|
||||||
|
roomId: $room.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
$ws.send({ type: WebSocketMessageType.CREATE_ROOM }); // send a message when the button is clicked
|
$ws.send({ type: WebSocketMessageType.CREATE_ROOM }); // send a message when the button is clicked
|
||||||
}}>Create Room</button
|
}}>Create Room</button
|
||||||
>
|
>
|
||||||
@@ -52,5 +52,5 @@
|
|||||||
<a href={`${location.origin}/${$room}`}>{location.origin}/{$room.id}</a>
|
<a href={`${location.origin}/${$room}`}>{location.origin}/{$room.id}</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<RtcMessage />
|
<RtcMessage {room} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,39 +1,36 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from "$app/state";
|
|
||||||
import { onDestroy, onMount } from "svelte";
|
import { onDestroy, onMount } from "svelte";
|
||||||
import { room } from "../../stores/roomStore";
|
import { room } from "../../stores/roomStore";
|
||||||
import { error, handleMessage, peer } from "../../utils/webrtcUtil";
|
import { error, handleMessage, peer } from "../../utils/webrtcUtil";
|
||||||
import {
|
import { ws, webSocketConnected } from "../../stores/websocketStore";
|
||||||
ws,
|
import { WebSocketMessageType } from "../../types/websocket";
|
||||||
webSocketConnected,
|
|
||||||
WebSocketMessageType,
|
|
||||||
} from "../../stores/websocketStore";
|
|
||||||
import RtcMessage from "../../components/RTCMessage.svelte";
|
import RtcMessage from "../../components/RTCMessage.svelte";
|
||||||
import { ConnectionState } from "../../types/websocket";
|
import { ConnectionState } from "../../types/websocket";
|
||||||
|
export let data: { roomId: string };
|
||||||
const roomId = page.params.roomId;
|
const { roomId } = data;
|
||||||
if (roomId === undefined) {
|
|
||||||
throw new Error("Room ID not provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
// subscribe to the websocket store
|
|
||||||
room.update((room) => ({ ...room, id: roomId }));
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
room.update((room) => ({ ...room, id: roomId }));
|
||||||
|
|
||||||
$ws.addEventListener("message", handleMessage);
|
$ws.addEventListener("message", handleMessage);
|
||||||
|
|
||||||
webSocketConnected.subscribe((value) => {
|
webSocketConnected.subscribe((value) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
$ws.send({ type: WebSocketMessageType.JOIN_ROOM, roomId });
|
room.update((room) => ({
|
||||||
|
...room,
|
||||||
|
connectionState: ConnectionState.CONNECTING,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if ($room.id === null) {
|
||||||
|
throw new Error("Room ID not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
$ws.send({
|
||||||
|
type: WebSocketMessageType.JOIN_ROOM,
|
||||||
|
roomId: $room.id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// $ws.onopen = () => {
|
|
||||||
// room.update((room) => ({
|
|
||||||
// ...room,
|
|
||||||
// connectionState: ConnectionState.CONNECTING,
|
|
||||||
// }));
|
|
||||||
// $ws.send({ type: WebSocketMessageType.JOIN_ROOM, roomId });
|
|
||||||
// };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
@@ -53,9 +50,9 @@
|
|||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
{#if $error}
|
{#if $error}
|
||||||
<p>Whoops! That room doesn't exist.</p>
|
<p>Whoops! That room doesn't exist.</p>
|
||||||
{:else if !$webSocketConnected || $room.connectionState === ConnectionState.CONNECTING}
|
{:else if $room.connectionState !== ConnectionState.CONNECTED && $room.connectionState !== ConnectionState.RECONNECTING}
|
||||||
<p>Connecting to server...</p>
|
<p>Connecting to server...</p>
|
||||||
{:else}
|
{:else}
|
||||||
<RtcMessage />
|
<RtcMessage {room} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
17
src/routes/[roomId]/+page.ts
Normal file
17
src/routes/[roomId]/+page.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageLoad = ({ params }) => {
|
||||||
|
const roomId = params.roomId;
|
||||||
|
|
||||||
|
if (!roomId) {
|
||||||
|
// SvelteKit's way of handling errors in load functions
|
||||||
|
throw error(404, 'Room ID not provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
// This return value is SAFELY passed to your page component
|
||||||
|
// It is NOT stored in a global variable on the server.
|
||||||
|
return {
|
||||||
|
roomId: roomId
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
import { writable, type Writable } from 'svelte/store';
|
import { writable, type Writable } from 'svelte/store';
|
||||||
import { ConnectionState } from '../types/websocket';
|
import { ConnectionState } from '../types/websocket';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
export interface Room {
|
export interface Room {
|
||||||
id: string | null;
|
id: string | null;
|
||||||
|
participants: number;
|
||||||
connectionState: ConnectionState;
|
connectionState: ConnectionState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const room: Writable<Room> = writable({
|
export const room: Writable<Room> = writable({
|
||||||
id: null,
|
id: null,
|
||||||
|
participants: 0,
|
||||||
connectionState: ConnectionState.DISCONNECTED,
|
connectionState: ConnectionState.DISCONNECTED,
|
||||||
key: null,
|
key: null,
|
||||||
});
|
});
|
||||||
@@ -1,120 +1,7 @@
|
|||||||
import { writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
import { room } from './roomStore';
|
||||||
export enum WebSocketMessageType {
|
import { ConnectionState, Socket, WebSocketMessageType } from '../types/websocket';
|
||||||
// room messages
|
|
||||||
CREATE_ROOM = "create",
|
|
||||||
JOIN_ROOM = "join",
|
|
||||||
|
|
||||||
// response messages
|
|
||||||
ROOM_CREATED = "created",
|
|
||||||
ROOM_JOINED = "joined",
|
|
||||||
ROOM_READY = "ready",
|
|
||||||
|
|
||||||
// webrtc messages
|
|
||||||
WEBRTC_OFFER = "offer",
|
|
||||||
WERTC_ANSWER = "answer",
|
|
||||||
WEBRTC_ICE_CANDIDATE = "ice-candidate",
|
|
||||||
|
|
||||||
ERROR = "error",
|
|
||||||
}
|
|
||||||
|
|
||||||
export type WebSocketMessage =
|
|
||||||
| CreateRoomMessage
|
|
||||||
| JoinRoomMessage
|
|
||||||
| RoomCreatedMessage
|
|
||||||
| RoomJoinedMessage
|
|
||||||
| RoomReadyMessage
|
|
||||||
| OfferMessage
|
|
||||||
| AnswerMessage
|
|
||||||
| IceCandidateMessage
|
|
||||||
| ErrorMessage;
|
|
||||||
|
|
||||||
interface ErrorMessage {
|
|
||||||
type: WebSocketMessageType.ERROR;
|
|
||||||
data: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CreateRoomMessage {
|
|
||||||
type: WebSocketMessageType.CREATE_ROOM;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface JoinRoomMessage {
|
|
||||||
type: WebSocketMessageType.JOIN_ROOM;
|
|
||||||
roomId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RoomCreatedMessage {
|
|
||||||
type: WebSocketMessageType.ROOM_CREATED;
|
|
||||||
data: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RoomJoinedMessage {
|
|
||||||
type: WebSocketMessageType.ROOM_JOINED;
|
|
||||||
roomId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RoomReadyMessage {
|
|
||||||
type: WebSocketMessageType.ROOM_READY;
|
|
||||||
data: {
|
|
||||||
isInitiator: boolean;
|
|
||||||
roomKey: {
|
|
||||||
key: JsonWebKey;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OfferMessage {
|
|
||||||
type: WebSocketMessageType.WEBRTC_OFFER;
|
|
||||||
data: {
|
|
||||||
roomId: string;
|
|
||||||
sdp: RTCSessionDescriptionInit;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AnswerMessage {
|
|
||||||
type: WebSocketMessageType.WERTC_ANSWER;
|
|
||||||
data: {
|
|
||||||
roomId: string;
|
|
||||||
sdp: RTCSessionDescriptionInit;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IceCandidateMessage {
|
|
||||||
type: WebSocketMessageType.WEBRTC_ICE_CANDIDATE;
|
|
||||||
data: {
|
|
||||||
roomId: string;
|
|
||||||
candidate: RTCIceCandidateInit;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Socket {
|
|
||||||
private ws: WebSocket;
|
|
||||||
|
|
||||||
public addEventListener: typeof WebSocket.prototype.addEventListener;
|
|
||||||
public removeEventListener: typeof WebSocket.prototype.removeEventListener;
|
|
||||||
public dispatchEvent: typeof WebSocket.prototype.dispatchEvent;
|
|
||||||
public close: typeof WebSocket.prototype.close;
|
|
||||||
|
|
||||||
constructor(public url: string, public protocols?: string | string[] | undefined) {
|
|
||||||
this.ws = new WebSocket(url, protocols);
|
|
||||||
|
|
||||||
this.ws.addEventListener("open", () => {
|
|
||||||
console.log("WebSocket opened");
|
|
||||||
});
|
|
||||||
|
|
||||||
this.addEventListener = this.ws.addEventListener.bind(this.ws);
|
|
||||||
this.removeEventListener = this.ws.removeEventListener.bind(this.ws);
|
|
||||||
this.dispatchEvent = this.ws.dispatchEvent.bind(this.ws);
|
|
||||||
this.close = this.ws.close.bind(this.ws);
|
|
||||||
}
|
|
||||||
|
|
||||||
public send(message: WebSocketMessage) {
|
|
||||||
console.log("Sending message:", message);
|
|
||||||
|
|
||||||
this.ws.send(JSON.stringify(message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let socket: Socket | null = null;
|
let socket: Socket | null = null;
|
||||||
export const webSocketConnected = writable(false);
|
export const webSocketConnected = writable(false);
|
||||||
@@ -129,7 +16,7 @@ function createSocket(): Socket {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
socket = new Socket(`${protocol}//${location.host}/`);
|
socket = new Socket(new WebSocket(`${protocol}//${location.host}/`));
|
||||||
|
|
||||||
socket.addEventListener('open', () => {
|
socket.addEventListener('open', () => {
|
||||||
webSocketConnected.set(true);
|
webSocketConnected.set(true);
|
||||||
@@ -137,12 +24,27 @@ function createSocket(): Socket {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.addEventListener('close', () => {
|
socket.addEventListener('close', () => {
|
||||||
|
// TODO: massively rework the reconnection logic, currently it only works if one client disconnects, if the
|
||||||
|
// TODO: other client disconnects after the other client has diconnected at least once, everything explodes
|
||||||
|
if (get(webSocketConnected) && get(room)?.connectionState === ConnectionState.CONNECTED) {
|
||||||
|
room.update((room) => ({ ...room, connectionState: ConnectionState.RECONNECTING }));
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
ws.set(createSocket());
|
||||||
|
|
||||||
|
// attempt to rejoin the room if we were previously connected
|
||||||
|
get(ws).addEventListener('open', () => {
|
||||||
|
let oldRoomId = get(room)?.id;
|
||||||
|
if (oldRoomId) {
|
||||||
|
get(ws).send({ type: WebSocketMessageType.JOIN_ROOM, roomId: oldRoomId });
|
||||||
|
room.update((room) => ({ ...room, connectionState: ConnectionState.CONNECTED }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
webSocketConnected.set(false);
|
webSocketConnected.set(false);
|
||||||
socket = null;
|
socket = null;
|
||||||
console.log('Disconnected from websocket server, reconnecting...');
|
console.log('Disconnected from websocket server, reconnecting...');
|
||||||
setTimeout(() => {
|
|
||||||
ws.set(createSocket());
|
|
||||||
}, 1000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return socket;
|
return socket;
|
||||||
|
|||||||
@@ -1,96 +1,145 @@
|
|||||||
|
|
||||||
export enum ConnectionState {
|
export enum ConnectionState {
|
||||||
CONNECTING,
|
CONNECTING,
|
||||||
|
RECONNECTING,
|
||||||
CONNECTED,
|
CONNECTED,
|
||||||
DISCONNECTED,
|
DISCONNECTED,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SocketMessageType {
|
export interface Room {
|
||||||
// requests
|
id: string | null;
|
||||||
CREATE_ROOM = 'create',
|
participants: number;
|
||||||
JOIN_ROOM = 'join',
|
connectionState: ConnectionState;
|
||||||
|
|
||||||
// responses
|
|
||||||
ROOM_CREATED = 'created',
|
|
||||||
ROOM_JOINED = 'joined',
|
|
||||||
ROOM_READY = 'ready',
|
|
||||||
|
|
||||||
// webrtc
|
|
||||||
ICE_CANDIDATE = 'ice-candidate',
|
|
||||||
OFFER = 'offer',
|
|
||||||
ANSWER = 'answer',
|
|
||||||
|
|
||||||
ERROR = 'error',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SocketMessageBase = {
|
export enum WebSocketMessageType {
|
||||||
type: SocketMessageType;
|
// room messages
|
||||||
};
|
CREATE_ROOM = "create",
|
||||||
|
JOIN_ROOM = "join",
|
||||||
|
LEAVE_ROOM = "leave",
|
||||||
|
|
||||||
export interface SocketMessageCreateRoom extends SocketMessageBase {
|
// response messages
|
||||||
type: SocketMessageType.CREATE_ROOM;
|
ROOM_CREATED = "created",
|
||||||
|
ROOM_JOINED = "joined",
|
||||||
|
ROOM_LEFT = "left",
|
||||||
|
ROOM_READY = "ready",
|
||||||
|
|
||||||
|
// webrtc messages
|
||||||
|
WEBRTC_OFFER = "offer",
|
||||||
|
WERTC_ANSWER = "answer",
|
||||||
|
WEBRTC_ICE_CANDIDATE = "ice-candidate",
|
||||||
|
|
||||||
|
ERROR = "error",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SocketMessageJoinRoom extends SocketMessageBase {
|
export type WebSocketMessage =
|
||||||
type: SocketMessageType.JOIN_ROOM;
|
| CreateRoomMessage
|
||||||
|
| JoinRoomMessage
|
||||||
|
| LeaveRoomMessage
|
||||||
|
| RoomCreatedMessage
|
||||||
|
| RoomJoinedMessage
|
||||||
|
| RoomLeftMessage
|
||||||
|
| RoomReadyMessage
|
||||||
|
| OfferMessage
|
||||||
|
| AnswerMessage
|
||||||
|
| IceCandidateMessage
|
||||||
|
| ErrorMessage;
|
||||||
|
|
||||||
|
interface ErrorMessage {
|
||||||
|
type: WebSocketMessageType.ERROR;
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateRoomMessage {
|
||||||
|
type: WebSocketMessageType.CREATE_ROOM;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JoinRoomMessage {
|
||||||
|
type: WebSocketMessageType.JOIN_ROOM;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SocketMessageRoomCreated extends SocketMessageBase {
|
interface LeaveRoomMessage {
|
||||||
type: SocketMessageType.ROOM_CREATED;
|
type: WebSocketMessageType.LEAVE_ROOM;
|
||||||
data: {
|
|
||||||
roomId: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SocketMessageRoomJoined extends SocketMessageBase {
|
|
||||||
type: SocketMessageType.ROOM_JOINED;
|
|
||||||
roomId: string;
|
roomId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SocketMessageRoomReady extends SocketMessageBase {
|
interface RoomCreatedMessage {
|
||||||
type: SocketMessageType.ROOM_READY;
|
type: WebSocketMessageType.ROOM_CREATED;
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RoomJoinedMessage {
|
||||||
|
type: WebSocketMessageType.ROOM_JOINED;
|
||||||
|
roomId: string;
|
||||||
|
participants: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RoomLeftMessage {
|
||||||
|
type: WebSocketMessageType.ROOM_LEFT;
|
||||||
|
roomId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RoomReadyMessage {
|
||||||
|
type: WebSocketMessageType.ROOM_READY;
|
||||||
data: {
|
data: {
|
||||||
roomId: string;
|
|
||||||
isInitiator: boolean;
|
isInitiator: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SocketMessageIceCandidate extends SocketMessageBase {
|
interface OfferMessage {
|
||||||
type: SocketMessageType.ICE_CANDIDATE;
|
type: WebSocketMessageType.WEBRTC_OFFER;
|
||||||
data: {
|
data: {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
candidate: RTCIceCandidate;
|
sdp: RTCSessionDescriptionInit;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SocketMessageOffer extends SocketMessageBase {
|
interface AnswerMessage {
|
||||||
type: SocketMessageType.OFFER;
|
type: WebSocketMessageType.WERTC_ANSWER;
|
||||||
data: {
|
data: {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
sdp: RTCSessionDescription;
|
sdp: RTCSessionDescriptionInit;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SocketMessageAnswer extends SocketMessageBase {
|
interface IceCandidateMessage {
|
||||||
type: SocketMessageType.ANSWER;
|
type: WebSocketMessageType.WEBRTC_ICE_CANDIDATE;
|
||||||
data: {
|
data: {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
sdp: RTCSessionDescription;
|
candidate: RTCIceCandidateInit;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SocketMessageError extends SocketMessageBase {
|
export interface SocketCallbacks {
|
||||||
type: SocketMessageType.ERROR;
|
onOpen: () => void;
|
||||||
data: string;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SocketMessage =
|
export class Socket {
|
||||||
| SocketMessageCreateRoom
|
public ws: WebSocket;
|
||||||
| SocketMessageJoinRoom
|
|
||||||
| SocketMessageRoomCreated
|
public addEventListener: typeof WebSocket.prototype.addEventListener;
|
||||||
| SocketMessageRoomJoined
|
public removeEventListener: typeof WebSocket.prototype.removeEventListener;
|
||||||
| SocketMessageRoomReady
|
public dispatchEvent: typeof WebSocket.prototype.dispatchEvent;
|
||||||
| SocketMessageIceCandidate
|
public close: typeof WebSocket.prototype.close;
|
||||||
| SocketMessageOffer
|
|
||||||
| SocketMessageAnswer
|
constructor(webSocket: WebSocket) {
|
||||||
| SocketMessageError;
|
this.ws = webSocket;
|
||||||
|
|
||||||
|
this.ws.addEventListener("open", () => {
|
||||||
|
console.log("WebSocket opened");
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addEventListener = this.ws.addEventListener.bind(this.ws);
|
||||||
|
this.removeEventListener = this.ws.removeEventListener.bind(this.ws);
|
||||||
|
this.dispatchEvent = this.ws.dispatchEvent.bind(this.ws);
|
||||||
|
this.close = this.ws.close.bind(this.ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
public send(message: WebSocketMessage) {
|
||||||
|
console.log("Sending message:", message);
|
||||||
|
|
||||||
|
this.ws.send(JSON.stringify(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
65
src/utils/liveMap.ts
Normal file
65
src/utils/liveMap.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
type LiveMapEntry<K, V> = V & { key: K; value: V; };
|
||||||
|
|
||||||
|
export class LiveMap<K, V extends Object> {
|
||||||
|
_map = new Map();
|
||||||
|
|
||||||
|
set(key: K, value: V): LiveMapEntry<K, V> {
|
||||||
|
if (this._map.has(key)) {
|
||||||
|
throw new Error(`Key ${key} already exists in the map`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._map.set(key, value);
|
||||||
|
|
||||||
|
// Create a wrapper object that holds both key and value for easy access, with mutation handling
|
||||||
|
let currentValueInMap: V = value;
|
||||||
|
const mapRef = this._map;
|
||||||
|
// use a dummy target object to proxy the value
|
||||||
|
const obj = new Proxy({}, {
|
||||||
|
get(target: object, prop: string | symbol, receiver: any): any {
|
||||||
|
if (prop === "key") {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
if (prop === "value") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Reflect.get(currentValueInMap as object, prop, receiver);
|
||||||
|
},
|
||||||
|
set(target, prop, newValue) {
|
||||||
|
if (prop === "value") {
|
||||||
|
value = newValue;
|
||||||
|
mapRef.set(key, value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return Reflect.set(target, prop, newValue);
|
||||||
|
},
|
||||||
|
deleteProperty(target: object, prop: string | symbol): boolean {
|
||||||
|
if (prop === "key" || prop === "value") {
|
||||||
|
return false; // Prevent deleting special properties
|
||||||
|
}
|
||||||
|
return Reflect.deleteProperty(currentValueInMap as object, prop);
|
||||||
|
},
|
||||||
|
has(target: object, prop: string | symbol): boolean {
|
||||||
|
if (prop === "key" || prop === "value") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return Reflect.has(currentValueInMap as object, prop);
|
||||||
|
},
|
||||||
|
ownKeys(target: object): Array<string | symbol> {
|
||||||
|
const keys = Reflect.ownKeys(currentValueInMap as object);
|
||||||
|
// Ensure 'key' and 'value' are always present when iterating over properties
|
||||||
|
if (!keys.includes("key")) keys.push("key");
|
||||||
|
if (!keys.includes("value")) keys.push("value");
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
}) as LiveMapEntry<K, V>;
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
get(key: K): V | undefined {
|
||||||
|
return this._map.get(key);
|
||||||
|
}
|
||||||
|
delete(key: K): boolean {
|
||||||
|
return this._map.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,10 @@ import { writable, get, type Writable } from "svelte/store";
|
|||||||
import { WebRTCPeer } from "$lib/webrtc";
|
import { WebRTCPeer } from "$lib/webrtc";
|
||||||
import { WebRTCPacketType } from "../types/webrtc";
|
import { WebRTCPacketType } from "../types/webrtc";
|
||||||
import { room } from "../stores/roomStore";
|
import { room } from "../stores/roomStore";
|
||||||
import { ConnectionState } from "../types/websocket";
|
import { ConnectionState, type Room } from "../types/websocket";
|
||||||
import { messages } from "../stores/messageStore";
|
import { messages } from "../stores/messageStore";
|
||||||
import { MessageType, type Message } from "../types/message";
|
import { MessageType, type Message } from "../types/message";
|
||||||
import { WebSocketMessageType, type WebSocketMessage } from "../stores/websocketStore";
|
import { WebSocketMessageType, type WebSocketMessage } from "../types/websocket";
|
||||||
|
|
||||||
export const error: Writable<string | null> = writable(null);
|
export const error: Writable<string | null> = writable(null);
|
||||||
export let peer: Writable<WebRTCPeer | null> = writable(null);
|
export let peer: Writable<WebRTCPeer | null> = writable(null);
|
||||||
@@ -73,15 +73,23 @@ export async function handleMessage(event: MessageEvent) {
|
|||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case WebSocketMessageType.ROOM_CREATED:
|
case WebSocketMessageType.ROOM_CREATED:
|
||||||
console.log("Room created:", message.data);
|
console.log("Room created:", message.data);
|
||||||
room.update((room) => ({ ...room, id: message.data, connectionState: ConnectionState.CONNECTED }));
|
room.update((room) => ({ ...room, id: message.data, connectionState: ConnectionState.CONNECTED, participants: 1 }));
|
||||||
return;
|
return;
|
||||||
case WebSocketMessageType.JOIN_ROOM:
|
case WebSocketMessageType.JOIN_ROOM:
|
||||||
console.log("new client joined room");
|
console.log("new client joined room");
|
||||||
|
room.update((room) => ({ ...room, participants: room.participants + 1 }));
|
||||||
return;
|
return;
|
||||||
case WebSocketMessageType.ROOM_JOINED:
|
case WebSocketMessageType.ROOM_JOINED:
|
||||||
room.update((room) => ({ ...room, connectionState: ConnectionState.CONNECTED }));
|
// TODO: if a client disconnects, somehow prove the identity of the client that left if they return. Perhaps
|
||||||
|
// TODO: use a key derived from client's public key so that the room can only be used by clients that initiated
|
||||||
|
// TODO: the connection
|
||||||
|
room.update((room) => ({ ...room, connectionState: ConnectionState.CONNECTED, participants: message.participants }));
|
||||||
console.log("Joined room");
|
console.log("Joined room");
|
||||||
return;
|
return;
|
||||||
|
case WebSocketMessageType.ROOM_LEFT:
|
||||||
|
room.update((room) => ({ ...room, participants: room.participants - 1 }));
|
||||||
|
console.log("Participant left room");
|
||||||
|
return;
|
||||||
case WebSocketMessageType.ERROR:
|
case WebSocketMessageType.ERROR:
|
||||||
console.error("Error:", message.data);
|
console.error("Error:", message.data);
|
||||||
error.set(message.data);
|
error.set(message.data);
|
||||||
@@ -100,9 +108,6 @@ export async function handleMessage(event: MessageEvent) {
|
|||||||
callbacks,
|
callbacks,
|
||||||
));
|
));
|
||||||
await get(peer)?.initialize();
|
await get(peer)?.initialize();
|
||||||
if (message.data.isInitiator) {
|
|
||||||
await get(peer)?.createOffer();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user