file uploads and bug fixes
This commit is contained in:
9
bun.lock
9
bun.lock
@@ -9,8 +9,11 @@
|
|||||||
"@noble/ciphers": "^1.3.0",
|
"@noble/ciphers": "^1.3.0",
|
||||||
"@noble/curves": "^1.9.0",
|
"@noble/curves": "^1.9.0",
|
||||||
"@sveltejs/adapter-node": "^5.3.1",
|
"@sveltejs/adapter-node": "^5.3.1",
|
||||||
|
"@types/streamsaver": "^2.0.5",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
|
"i": "^0.3.7",
|
||||||
"polka": "^0.5.2",
|
"polka": "^0.5.2",
|
||||||
|
"streamsaver": "^2.0.6",
|
||||||
"ts-mls": "^1.1.0",
|
"ts-mls": "^1.1.0",
|
||||||
"ws": "^8.18.3",
|
"ws": "^8.18.3",
|
||||||
},
|
},
|
||||||
@@ -242,6 +245,8 @@
|
|||||||
|
|
||||||
"@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="],
|
"@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="],
|
||||||
|
|
||||||
|
"@types/streamsaver": ["@types/streamsaver@2.0.5", "", {}, "sha512-93o0zjV8swEhR2YI57h/2ytbJF8bJh7sI9GNB02TLJHdM4fWDxZuChwfWhyD8vt2ub4kw4rsfZ0C0yAUX+3gcg=="],
|
||||||
|
|
||||||
"@types/trouter": ["@types/trouter@3.1.4", "", {}, "sha512-4YIL/2AvvZqKBWenjvEpxpblT2KGO6793ipr5QS7/6DpQ3O3SwZGgNGWezxf3pzeYZc24a2pJIrR/+Jxh/wYNQ=="],
|
"@types/trouter": ["@types/trouter@3.1.4", "", {}, "sha512-4YIL/2AvvZqKBWenjvEpxpblT2KGO6793ipr5QS7/6DpQ3O3SwZGgNGWezxf3pzeYZc24a2pJIrR/+Jxh/wYNQ=="],
|
||||||
|
|
||||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||||
@@ -290,6 +295,8 @@
|
|||||||
|
|
||||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
|
"i": ["i@0.3.7", "", {}, "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q=="],
|
||||||
|
|
||||||
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
||||||
|
|
||||||
"is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="],
|
"is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="],
|
||||||
@@ -368,6 +375,8 @@
|
|||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"streamsaver": ["streamsaver@2.0.6", "", {}, "sha512-LK4e7TfCV8HzuM0PKXuVUfKyCB1FtT9L0EGxsFk5Up8njj0bXK8pJM9+Wq2Nya7/jslmCQwRK39LFm55h7NBTw=="],
|
||||||
|
|
||||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||||
|
|
||||||
"svelte": ["svelte@5.38.8", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-UDpTbM/iuZ4MaMnn4ODB3rf5JKDyPOi5oJcopP0j7YHQ9BuJtsAqsR71r2N6AnJf7ygbalTJU5y8eSWGAQZjlQ=="],
|
"svelte": ["svelte@5.38.8", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-UDpTbM/iuZ4MaMnn4ODB3rf5JKDyPOi5oJcopP0j7YHQ9BuJtsAqsR71r2N6AnJf7ygbalTJU5y8eSWGAQZjlQ=="],
|
||||||
|
|||||||
@@ -29,8 +29,11 @@
|
|||||||
"@noble/ciphers": "^1.3.0",
|
"@noble/ciphers": "^1.3.0",
|
||||||
"@noble/curves": "^1.9.0",
|
"@noble/curves": "^1.9.0",
|
||||||
"@sveltejs/adapter-node": "^5.3.1",
|
"@sveltejs/adapter-node": "^5.3.1",
|
||||||
|
"@types/streamsaver": "^2.0.5",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
|
"i": "^0.3.7",
|
||||||
"polka": "^0.5.2",
|
"polka": "^0.5.2",
|
||||||
|
"streamsaver": "^2.0.6",
|
||||||
"ts-mls": "^1.1.0",
|
"ts-mls": "^1.1.0",
|
||||||
"ws": "^8.18.3"
|
"ws": "^8.18.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -75,11 +75,6 @@ async function joinRoom(roomId: string, socket: Socket): Promise<ServerRoom | un
|
|||||||
room.push(socket);
|
room.push(socket);
|
||||||
|
|
||||||
socket.addEventListener('close', (ev) => {
|
socket.addEventListener('close', (ev) => {
|
||||||
room = rooms.get(roomId)
|
|
||||||
if (!room) {
|
|
||||||
throw new Error("Room not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
room.notifyAll({ type: WebSocketMessageType.ROOM_LEFT, roomId });
|
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
|
||||||
@@ -108,6 +103,34 @@ async function joinRoom(roomId: string, socket: Socket): Promise<ServerRoom | un
|
|||||||
return room;
|
return room;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function leaveRoom(roomId: string, socket: Socket): ServerRoom | undefined {
|
||||||
|
let room = rooms.get(roomId);
|
||||||
|
console.log(room?.length);
|
||||||
|
|
||||||
|
// should be unreachable
|
||||||
|
if (!room) {
|
||||||
|
socket.send({ type: WebSocketMessageType.ERROR, data: `Room ${roomId} does not exist` });
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (room.length == 1) {
|
||||||
|
// give a 5 second grace period before deleting the room
|
||||||
|
setTimeout(() => {
|
||||||
|
if (rooms.get(roomId)?.length === 1) {
|
||||||
|
console.log("Room is empty, deleting");
|
||||||
|
deleteRoom(roomId);
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
room.set(room.filter(client => client !== socket));
|
||||||
|
|
||||||
|
socket.send({ type: WebSocketMessageType.ROOM_LEFT, roomId });
|
||||||
|
|
||||||
|
return room;
|
||||||
|
}
|
||||||
|
|
||||||
function deleteRoom(roomId: string) {
|
function deleteRoom(roomId: string) {
|
||||||
rooms.delete(roomId);
|
rooms.delete(roomId);
|
||||||
}
|
}
|
||||||
@@ -165,6 +188,21 @@ export function confgiureWebsocketServer(wss: WebSocketServer) {
|
|||||||
|
|
||||||
// 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({ type: WebSocketMessageType.ROOM_JOINED, roomId: message.roomId, participants: room.length });
|
socket.send({ type: WebSocketMessageType.ROOM_JOINED, roomId: message.roomId, participants: room.length });
|
||||||
|
break;
|
||||||
|
case WebSocketMessageType.LEAVE_ROOM:
|
||||||
|
if (!message.roomId) {
|
||||||
|
socket.send({ type: WebSocketMessageType.ERROR, data: 'Invalid message' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rooms.get(message.roomId) == undefined) {
|
||||||
|
socket.send({ type: WebSocketMessageType.ERROR, data: 'Invalid roomId' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
room = await leaveRoom(message.roomId, socket);
|
||||||
|
if (!room) return;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case WebSocketMessageType.WEBRTC_OFFER:
|
case WebSocketMessageType.WEBRTC_OFFER:
|
||||||
case WebSocketMessageType.WERTC_ANSWER:
|
case WebSocketMessageType.WERTC_ANSWER:
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -8,26 +8,55 @@
|
|||||||
peer,
|
peer,
|
||||||
keyExchangeDone,
|
keyExchangeDone,
|
||||||
} from "../utils/webrtcUtil";
|
} from "../utils/webrtcUtil";
|
||||||
import { messages } from "../stores/messageStore";
|
import {
|
||||||
|
advertisedOffers,
|
||||||
|
fileRequestIds,
|
||||||
|
messages,
|
||||||
|
receivedOffers,
|
||||||
|
} from "../stores/messageStore";
|
||||||
import { WebRTCPacketType } from "../types/webrtc";
|
import { WebRTCPacketType } from "../types/webrtc";
|
||||||
import { ConnectionState, type Room } 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";
|
||||||
|
import { WebBuffer } from "../utils/buffer";
|
||||||
|
|
||||||
let inputMessage: Writable<string> = writable("");
|
let inputMessage: Writable<string> = writable("");
|
||||||
let inputFile = writable(null);
|
let inputFile: Writable<FileList | null | undefined> = writable(null);
|
||||||
let inputFileElement: HTMLInputElement | null = $state(null);
|
let inputFileElement: HTMLInputElement | null = $state(null);
|
||||||
|
let initialConnectionCompleteCount = writable(0);
|
||||||
let initialConnectionComplete = derived(
|
let initialConnectionComplete = derived(
|
||||||
[isRTCConnected, dataChannelReady, keyExchangeDone],
|
initialConnectionCompleteCount,
|
||||||
(values: Array<boolean>) => values.every((value) => value),
|
(value) => value === 3,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO: is this the most elegant way to do this?
|
||||||
|
isRTCConnected.subscribe((value) => {
|
||||||
|
if (value) {
|
||||||
|
$initialConnectionCompleteCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dataChannelReady.subscribe((value) => {
|
||||||
|
if (value) {
|
||||||
|
$initialConnectionCompleteCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
keyExchangeDone.subscribe((value) => {
|
||||||
|
if (value) {
|
||||||
|
$initialConnectionCompleteCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const { room }: { room: Writable<Room> } = $props();
|
const { room }: { room: Writable<Room> } = $props();
|
||||||
|
|
||||||
room.subscribe((newRoom) => {
|
room.subscribe((newRoom) => {
|
||||||
console.log("Room changed:", newRoom);
|
console.log("Room changed:", newRoom);
|
||||||
if (newRoom.id !== $room?.id) {
|
if (newRoom.id !== $room?.id) {
|
||||||
messages.set([]);
|
messages.set([]);
|
||||||
|
isRTCConnected.set(false);
|
||||||
|
dataChannelReady.set(false);
|
||||||
|
keyExchangeDone.set(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -37,15 +66,69 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$inputFile && !$inputMessage) {
|
let messageBuf: Uint8Array<ArrayBuffer> | undefined = undefined;
|
||||||
|
|
||||||
|
if (!$inputFile && !$inputMessage.trim()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if ($inputFile != null && $inputFile[0] !== undefined) {
|
if ($inputFile != null && $inputFile[0] !== undefined) {
|
||||||
// $messages = [...$messages, `You: ${$inputFile[0].name}`];
|
// fileSize + fileNameSize + fileNameLen + id + textLen + header
|
||||||
// $peer.send($inputFile[0]);
|
let messageLen =
|
||||||
// $inputFile = null;
|
8 +
|
||||||
// }
|
$inputFile[0].name.length +
|
||||||
|
2 +
|
||||||
|
8 +
|
||||||
|
$inputMessage.length +
|
||||||
|
1;
|
||||||
|
let messageBuf = new WebBuffer(new ArrayBuffer(messageLen));
|
||||||
|
|
||||||
|
let fileId = new WebBuffer(
|
||||||
|
crypto.getRandomValues(new Uint8Array(8)).buffer,
|
||||||
|
).readBigInt64LE();
|
||||||
|
$advertisedOffers.set(fileId, $inputFile[0]);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"Advertised file:",
|
||||||
|
fileId,
|
||||||
|
$inputFile[0].size,
|
||||||
|
$inputFile[0].name,
|
||||||
|
$inputFile[0].name.length,
|
||||||
|
);
|
||||||
|
|
||||||
|
messageBuf.writeInt8(MessageType.FILE_OFFER);
|
||||||
|
messageBuf.writeBigInt64LE(BigInt($inputFile[0].size));
|
||||||
|
messageBuf.writeInt16LE($inputFile[0].name.length);
|
||||||
|
messageBuf.writeString($inputFile[0].name);
|
||||||
|
messageBuf.writeBigInt64LE(fileId);
|
||||||
|
messageBuf.writeString($inputMessage);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"Sending file offer",
|
||||||
|
new Uint8Array(messageBuf.buffer),
|
||||||
|
);
|
||||||
|
|
||||||
|
$messages = [
|
||||||
|
...$messages,
|
||||||
|
{
|
||||||
|
initiator: true,
|
||||||
|
type: MessageType.FILE_OFFER,
|
||||||
|
data: {
|
||||||
|
fileSize: BigInt($inputFile[0].size),
|
||||||
|
fileNameSize: $inputFile[0].name.length,
|
||||||
|
fileName: $inputFile[0].name,
|
||||||
|
id: fileId,
|
||||||
|
text: $inputMessage === "" ? null : $inputMessage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
$inputFile = null;
|
||||||
|
$inputMessage = "";
|
||||||
|
|
||||||
|
$peer.send(messageBuf.buffer, WebRTCPacketType.MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ($inputMessage) {
|
if ($inputMessage) {
|
||||||
$messages = [
|
$messages = [
|
||||||
@@ -56,17 +139,46 @@
|
|||||||
data: $inputMessage,
|
data: $inputMessage,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
$peer.send(
|
|
||||||
new TextEncoder().encode(
|
let newMessageBuf = new ArrayBuffer(1 + $inputMessage.length);
|
||||||
JSON.stringify({
|
messageBuf = new Uint8Array(newMessageBuf);
|
||||||
type: MessageType.TEXT,
|
|
||||||
data: $inputMessage,
|
messageBuf[0] = MessageType.TEXT;
|
||||||
}),
|
messageBuf.set(new TextEncoder().encode($inputMessage), 1);
|
||||||
).buffer,
|
|
||||||
WebRTCPacketType.MESSAGE,
|
|
||||||
);
|
|
||||||
$inputMessage = "";
|
$inputMessage = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!messageBuf) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$peer.send(messageBuf.buffer, WebRTCPacketType.MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadFile(id: bigint) {
|
||||||
|
if (!$peer) {
|
||||||
|
console.error("Peer not initialized");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = $receivedOffers.get(id);
|
||||||
|
if (!file) {
|
||||||
|
console.error("Unknown file id:", id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let requesterId = new WebBuffer(
|
||||||
|
crypto.getRandomValues(new Uint8Array(8)).buffer,
|
||||||
|
).readBigInt64LE();
|
||||||
|
let fileRequestBuf = new WebBuffer(new ArrayBuffer(1 + 8 + 8));
|
||||||
|
fileRequestBuf.writeInt8(MessageType.FILE_REQUEST);
|
||||||
|
fileRequestBuf.writeBigInt64LE(id);
|
||||||
|
fileRequestBuf.writeBigInt64LE(requesterId);
|
||||||
|
|
||||||
|
$fileRequestIds.set(requesterId, id);
|
||||||
|
|
||||||
|
$peer.send(fileRequestBuf.buffer, WebRTCPacketType.MESSAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
let canCloseLoadingOverlay = writable(false);
|
let canCloseLoadingOverlay = writable(false);
|
||||||
@@ -85,6 +197,29 @@
|
|||||||
|
|
||||||
inputFileElement.click();
|
inputFileElement.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function autogrow(node: HTMLElement) {
|
||||||
|
function resize() {
|
||||||
|
// 1. Temporarily reset height to calculate the new scrollHeight
|
||||||
|
node.style.height = "0px";
|
||||||
|
// 2. Set the height to the scrollHeight, which represents the full content height
|
||||||
|
node.style.height = `${node.scrollHeight}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call resize initially in case the textarea already has content
|
||||||
|
resize();
|
||||||
|
|
||||||
|
// Add an event listener to resize on every input
|
||||||
|
node.addEventListener("input", resize);
|
||||||
|
|
||||||
|
// Return a destroy method to clean up the event listener when the component is unmounted
|
||||||
|
return {
|
||||||
|
update: resize,
|
||||||
|
destroy() {
|
||||||
|
node.removeEventListener("input", resize);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
@@ -101,16 +236,16 @@
|
|||||||
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 relative whitespace-break-spaces wrap-anywhere"
|
||||||
>
|
>
|
||||||
{#if !$initialConnectionComplete || $room.connectionState === ConnectionState.RECONNECTING || $room.participants !== 2 || !$canCloseLoadingOverlay}
|
{#if !$initialConnectionComplete || $room.connectionState === ConnectionState.RECONNECTING || $room.participants !== 2 || $dataChannelReady === false || !$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 z-10 text-center"
|
||||||
>
|
>
|
||||||
{#if !$isRTCConnected}
|
{#if !$isRTCConnected}
|
||||||
<p>Waiting for peer to connect...</p>
|
<p>Waiting for peer to connect...</p>
|
||||||
{:else if !$dataChannelReady}
|
{:else if !$dataChannelReady && !$initialConnectionComplete}
|
||||||
<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>
|
||||||
@@ -118,7 +253,7 @@
|
|||||||
<p>
|
<p>
|
||||||
Disconnect from peer, attempting to reconnecting...
|
Disconnect from peer, attempting to reconnecting...
|
||||||
</p>
|
</p>
|
||||||
{:else if $room.participants !== 2}
|
{:else if $room.participants !== 2 || $dataChannelReady === false}
|
||||||
<p>
|
<p>
|
||||||
Peer has disconnected, waiting for other peer to
|
Peer has disconnected, waiting for other peer to
|
||||||
reconnect...
|
reconnect...
|
||||||
@@ -130,7 +265,7 @@
|
|||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
{#if !$keyExchangeDone || $room.participants !== 2 || $room.connectionState === ConnectionState.RECONNECTING}
|
{#if !$keyExchangeDone || $room.participants !== 2 || $dataChannelReady === false || $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"
|
||||||
@@ -174,20 +309,59 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#each $messages as msg}
|
{#each $messages as msg}
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<p class="break-keep">
|
<p class="whitespace-nowrap">
|
||||||
{#if msg.initiator}
|
{#if msg.initiator}
|
||||||
You:
|
You:
|
||||||
{:else}
|
{:else}
|
||||||
Peer:
|
Peer:
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
|
||||||
{#if msg.type === MessageType.TEXT}
|
{#if msg.type === MessageType.TEXT}
|
||||||
{msg.data}
|
<p>{msg.data}</p>
|
||||||
{:else}
|
{:else if msg.type === MessageType.FILE_OFFER}
|
||||||
Unknown message type: {msg.type}
|
<div class="flex flex-col w-full mb-2">
|
||||||
{/if}
|
{#if msg.data.text !== null}
|
||||||
|
<p>
|
||||||
|
{msg.data.text}
|
||||||
</p>
|
</p>
|
||||||
|
{/if}
|
||||||
|
<div
|
||||||
|
class="flex flex-col p-2 relative w-8/12 bg-gray-600 rounded"
|
||||||
|
>
|
||||||
|
<h2 class="text-lg font-semibold my-1">
|
||||||
|
{msg.data.fileName}
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm">
|
||||||
|
{msg.data.fileSize} bytes
|
||||||
|
</p>
|
||||||
|
<!-- as the initiator, we cant send ourselves a file -->
|
||||||
|
{#if !msg.initiator}
|
||||||
|
<button
|
||||||
|
onclick={() =>
|
||||||
|
downloadFile(msg.data.id)}
|
||||||
|
class="absolute right-2 bottom-2 p-1 border border-gray-500 text-gray-100 hover:bg-gray-800/70 transition-colors rounded disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><path
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2M7 11l5 5l5-5m-5-7v12"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p>Unknown message type: {msg.type}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -198,32 +372,106 @@
|
|||||||
class="absolute opacity-0 -top-[9999px] -left-[9999px]"
|
class="absolute opacity-0 -top-[9999px] -left-[9999px]"
|
||||||
/>
|
/>
|
||||||
<div class="flex gap-2 w-full flex-row">
|
<div class="flex gap-2 w-full flex-row">
|
||||||
<input
|
<div
|
||||||
type="text"
|
class="border rounded border-gray-600 flex-grow flex flex-col bg-gray-700"
|
||||||
bind:value={$inputMessage}
|
>
|
||||||
onkeyup={(e) => e.key === "Enter" && sendMessage()}
|
{#if $inputFile}
|
||||||
disabled={!$isRTCConnected ||
|
<div class="flex flex-row gap-2 p-2">
|
||||||
!$dataChannelReady ||
|
<div
|
||||||
!$keyExchangeDone ||
|
class="p-2 flex flex-col gap-2 w-48 border rounded-md border-gray-600 relative"
|
||||||
$room.connectionState === ConnectionState.RECONNECTING}
|
>
|
||||||
placeholder="Type your message..."
|
<div class="w-full flex justify-center">
|
||||||
class="flex-grow p-2 rounded bg-gray-700 border border-gray-600 text-gray-100 placeholder-gray-400
|
<svg
|
||||||
focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
/>
|
width="128"
|
||||||
|
height="128"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><g
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="1"
|
||||||
|
><path
|
||||||
|
d="M14 3v4a1 1 0 0 0 1 1h4"
|
||||||
|
/><path
|
||||||
|
d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2"
|
||||||
|
/></g
|
||||||
|
></svg
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="text-sm whitespace-nowrap overflow-hidden text-ellipsis"
|
||||||
|
>
|
||||||
|
{$inputFile[0].name}
|
||||||
|
</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onclick={pickFile}
|
onclick={() => {
|
||||||
disabled={!$isRTCConnected ||
|
$inputFile = null;
|
||||||
!$dataChannelReady ||
|
}}
|
||||||
!$keyExchangeDone ||
|
class="absolute right-2 top-2 p-1 border border-gray-600 text-gray-100 hover:bg-gray-800/70 transition-colors rounded disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed"
|
||||||
$room.connectionState === ConnectionState.RECONNECTING}
|
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="16"
|
width="16"
|
||||||
height="16"
|
height="16"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><path
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M18 6L6 18M6 6l12 12"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr class="border-gray-600" />
|
||||||
|
{/if}
|
||||||
|
<div
|
||||||
|
class="flex flex-row focus-within:ring-2 focus-within:ring-blue-500 rounded"
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
bind:value={$inputMessage}
|
||||||
|
cols="1"
|
||||||
|
use:autogrow={$inputMessage}
|
||||||
|
onkeydown={(e) => {
|
||||||
|
if (
|
||||||
|
e.key === "Enter" &&
|
||||||
|
!e.getModifierState("Shift")
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!$isRTCConnected ||
|
||||||
|
!$dataChannelReady ||
|
||||||
|
!$keyExchangeDone ||
|
||||||
|
$room.connectionState ===
|
||||||
|
ConnectionState.RECONNECTING}
|
||||||
|
placeholder="Type your message..."
|
||||||
|
class="flex-grow p-2 bg-gray-700 rounded text-gray-100 placeholder-gray-400 min-h-12
|
||||||
|
focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed resize-none leading-8"
|
||||||
|
></textarea>
|
||||||
|
<div class="flex flex-row gap-2 p-2 h-fit mt-auto">
|
||||||
|
<button
|
||||||
|
onclick={pickFile}
|
||||||
|
disabled={!$isRTCConnected ||
|
||||||
|
!$dataChannelReady ||
|
||||||
|
!$keyExchangeDone ||
|
||||||
|
$room.connectionState ===
|
||||||
|
ConnectionState.RECONNECTING}
|
||||||
|
aria-label="Pick file"
|
||||||
|
class="not-disabled:hover:bg-gray-800/70 h-fit p-1 text-gray-100 transition-colors rounded disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><path
|
><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><path
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -239,13 +487,30 @@
|
|||||||
disabled={!$isRTCConnected ||
|
disabled={!$isRTCConnected ||
|
||||||
!$dataChannelReady ||
|
!$dataChannelReady ||
|
||||||
!$keyExchangeDone ||
|
!$keyExchangeDone ||
|
||||||
$room.connectionState === ConnectionState.RECONNECTING}
|
$room.connectionState ===
|
||||||
class="px-4 py-2 bg-blue-600 not-disabled:hover:bg-blue-700 text-white rounded disabled:opacity-50 disabled:cursor-not-allowed"
|
ConnectionState.RECONNECTING}
|
||||||
|
class="not-disabled:hover:bg-gray-800/70 h-fit p-1 text-gray-100 transition-colors rounded disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><path
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M10 14L21 3m0 0l-6.5 18a.55.55 0 0 1-1 0L10 14l-7-3.5a.55.55 0 0 1 0-1z"
|
||||||
|
/></svg
|
||||||
>
|
>
|
||||||
Send
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ export class WebRTCPeer {
|
|||||||
iceServers: this.iceServers,
|
iceServers: this.iceServers,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.peer.onicecandidateerror = (event) => {
|
||||||
|
console.error("ICE candidate error:", event);
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Initialize ICE candidates
|
// 1. Initialize ICE candidates
|
||||||
this.peer.onicecandidate = (event) => {
|
this.peer.onicecandidate = (event) => {
|
||||||
if (event.candidate) {
|
if (event.candidate) {
|
||||||
@@ -93,14 +97,12 @@ export class WebRTCPeer {
|
|||||||
channel.binaryType = "arraybuffer";
|
channel.binaryType = "arraybuffer";
|
||||||
|
|
||||||
channel.onopen = async () => {
|
channel.onopen = async () => {
|
||||||
console.log('data channel open');
|
this.callbacks.onDataChannelStateChange(true);
|
||||||
this.callbacks.onDataChannelOpen();
|
|
||||||
this.callbacks.onKeyExchangeDone();
|
|
||||||
|
|
||||||
await this.generateKeyPair();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.isInitiator) {
|
if (this.isInitiator) {
|
||||||
|
await this.generateKeyPair();
|
||||||
|
|
||||||
let groupId = crypto.getRandomValues(new Uint8Array(24));
|
let groupId = crypto.getRandomValues(new Uint8Array(24));
|
||||||
this.clientState = await createGroup(groupId, this.keyPackage!.publicPackage, this.keyPackage!.privatePackage, [], this.cipherSuite!);
|
this.clientState = await createGroup(groupId, this.keyPackage!.publicPackage, this.keyPackage!.privatePackage, [], this.cipherSuite!);
|
||||||
|
|
||||||
@@ -136,6 +138,7 @@ export class WebRTCPeer {
|
|||||||
console.log("parsed data", data, encrypted, type);
|
console.log("parsed data", data, encrypted, type);
|
||||||
|
|
||||||
if (type === WebRTCPacketType.GROUP_OPEN) {
|
if (type === WebRTCPacketType.GROUP_OPEN) {
|
||||||
|
await this.generateKeyPair();
|
||||||
await this.startKeyExchange();
|
await this.startKeyExchange();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -179,6 +182,7 @@ export class WebRTCPeer {
|
|||||||
|
|
||||||
this.send(encodedWelcomeBuf, WebRTCPacketType.WELCOME);
|
this.send(encodedWelcomeBuf, WebRTCPacketType.WELCOME);
|
||||||
this.encyptionReady = true;
|
this.encyptionReady = true;
|
||||||
|
this.callbacks.onKeyExchangeDone();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -202,6 +206,7 @@ export class WebRTCPeer {
|
|||||||
|
|
||||||
console.log("Joined group", this.clientState);
|
console.log("Joined group", this.clientState);
|
||||||
this.encyptionReady = true;
|
this.encyptionReady = true;
|
||||||
|
this.callbacks.onKeyExchangeDone();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,11 +236,12 @@ export class WebRTCPeer {
|
|||||||
data: data.buffer,
|
data: data.buffer,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.callbacks.onMessage(message);
|
this.callbacks.onMessage(message, this);
|
||||||
};
|
};
|
||||||
|
|
||||||
channel.onclose = () => {
|
channel.onclose = () => {
|
||||||
console.log('data channel closed');
|
this.callbacks.onDataChannelStateChange(false);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
channel.onerror = (error) => {
|
channel.onerror = (error) => {
|
||||||
|
|||||||
@@ -7,6 +7,12 @@
|
|||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<link rel="icon" href={favicon} />
|
<link rel="icon" href={favicon} />
|
||||||
|
<script
|
||||||
|
src="https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.0.2/dist/ponyfill.min.js"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
src="https://cdn.jsdelivr.net/npm/streamsaver@2.0.3/StreamSaver.min.js"
|
||||||
|
></script>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
|||||||
@@ -38,6 +38,12 @@
|
|||||||
type: WebSocketMessageType.LEAVE_ROOM,
|
type: WebSocketMessageType.LEAVE_ROOM,
|
||||||
roomId: $room.id,
|
roomId: $room.id,
|
||||||
});
|
});
|
||||||
|
$peer?.close();
|
||||||
|
peer.set(null);
|
||||||
|
room.update((room) => ({
|
||||||
|
...room,
|
||||||
|
connectionState: ConnectionState.DISCONNECTED,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
$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
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
|
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
{#if $error}
|
{#if $error}
|
||||||
<p>Whoops! That room doesn't exist.</p>
|
<p>Hm. Something went wrong: {$error.toLocaleLowerCase()}</p>
|
||||||
{:else if $room.connectionState !== ConnectionState.CONNECTED && $room.connectionState !== ConnectionState.RECONNECTING}
|
{:else if $room.connectionState !== ConnectionState.CONNECTED && $room.connectionState !== ConnectionState.RECONNECTING}
|
||||||
<p>Connecting to server...</p>
|
<p>Connecting to server...</p>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -2,3 +2,7 @@ import { writable, type Writable } from "svelte/store";
|
|||||||
import type { Message } from "../types/message";
|
import type { Message } from "../types/message";
|
||||||
|
|
||||||
export let messages: Writable<Message[]> = writable([]);
|
export let messages: Writable<Message[]> = writable([]);
|
||||||
|
export let advertisedOffers = writable(new Map<bigint, File>());
|
||||||
|
export let receivedOffers = writable(new Map<bigint, { name: string, size: bigint }>());
|
||||||
|
// maps request id to received file id
|
||||||
|
export let fileRequestIds: Writable<Map<bigint, bigint>> = writable(new Map());
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export const webSocketConnected = writable(false);
|
|||||||
|
|
||||||
function createSocket(): Socket {
|
function createSocket(): Socket {
|
||||||
if (!browser) {
|
if (!browser) {
|
||||||
|
// this only occurs on the server, which we dont care about because its not a client that can actually connect to the websocket server
|
||||||
|
// @ts-ignore
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
export enum MessageType {
|
export enum MessageType {
|
||||||
// chat packets
|
// chat packets
|
||||||
TEXT = 0,
|
TEXT,
|
||||||
// user offers to send a file
|
// user offers to send a file
|
||||||
FILE_OFFER = 1,
|
FILE_OFFER,
|
||||||
// user downloads a file offered by the peer
|
// user downloads a file offered by the peer
|
||||||
FILE_REQUEST = 2,
|
FILE_REQUEST,
|
||||||
|
|
||||||
// file packets
|
// file packets
|
||||||
FILE = 3,
|
FILE,
|
||||||
|
FILE_ACK,
|
||||||
|
FILE_DONE,
|
||||||
|
|
||||||
ERROR = 255
|
ERROR = 255
|
||||||
}
|
}
|
||||||
@@ -16,6 +18,7 @@ export type Message =
|
|||||||
| TextMessage
|
| TextMessage
|
||||||
| FileOfferMessage
|
| FileOfferMessage
|
||||||
| FileRequestMessage
|
| FileRequestMessage
|
||||||
|
| FileAckMessage
|
||||||
| FileMessage
|
| FileMessage
|
||||||
| ErrorMessage;
|
| ErrorMessage;
|
||||||
|
|
||||||
@@ -32,28 +35,51 @@ export interface TextMessage extends BaseMessage {
|
|||||||
export interface FileOfferMessage extends BaseMessage {
|
export interface FileOfferMessage extends BaseMessage {
|
||||||
type: MessageType.FILE_OFFER;
|
type: MessageType.FILE_OFFER;
|
||||||
data: {
|
data: {
|
||||||
|
// 64 bit file size. chunked at 1024 bytes
|
||||||
|
fileSize: bigint;
|
||||||
|
|
||||||
|
// 16 bit file name size
|
||||||
|
fileNameSize: number;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
fileSize: number;
|
// 64bit randomly generated id to identify the file so that multiple files with the same name can be uploaded
|
||||||
// randomly generated to identify the file so that multiple files with the same name can be uploaded
|
id: bigint;
|
||||||
id: string;
|
text: string | null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRequestMessage extends BaseMessage {
|
export interface FileRequestMessage extends BaseMessage {
|
||||||
type: MessageType.FILE_REQUEST;
|
type: MessageType.FILE_REQUEST;
|
||||||
data: {
|
data: {
|
||||||
id: string;
|
// 64 bit file id
|
||||||
|
id: bigint;
|
||||||
|
// 64 bit requester id
|
||||||
|
requesterId: bigint;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FileAckMessage extends BaseMessage {
|
||||||
|
type: MessageType.FILE_ACK;
|
||||||
|
// the request id
|
||||||
|
id: bigint;
|
||||||
|
}
|
||||||
|
|
||||||
// ----- file packets -----
|
// ----- file packets -----
|
||||||
export interface FileMessage extends BaseMessage {
|
export interface FileMessage extends BaseMessage {
|
||||||
type: MessageType.FILE;
|
type: MessageType.FILE;
|
||||||
data: {
|
data: {
|
||||||
id: string;
|
// the request id
|
||||||
fileName: string;
|
id: bigint;
|
||||||
fileSize: number;
|
// no file metadata is sent here, because we already know all of it from the request id
|
||||||
data: ArrayBuffer;
|
// comes down in 16MB chunks
|
||||||
|
data: Blob;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileDoneMessage extends BaseMessage {
|
||||||
|
type: MessageType.FILE_DONE;
|
||||||
|
data: {
|
||||||
|
// the request id
|
||||||
|
id: bigint;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import type { WebRTCPeer } from "$lib/webrtc";
|
||||||
|
|
||||||
export interface WebRTCPeerCallbacks {
|
export interface WebRTCPeerCallbacks {
|
||||||
onConnected: () => void;
|
onConnected: () => void;
|
||||||
onMessage: (message: { type: WebRTCPacketType, data: ArrayBuffer }) => void;
|
onMessage: (message: { type: WebRTCPacketType, data: ArrayBuffer }, webRtcPeer: WebRTCPeer) => void;
|
||||||
onDataChannelOpen: () => void;
|
onDataChannelStateChange: (state: boolean) => void;
|
||||||
onKeyExchangeDone: () => void;
|
onKeyExchangeDone: () => void;
|
||||||
onNegotiationNeeded: () => void;
|
onNegotiationNeeded: () => void;
|
||||||
onError: (error: any) => void;
|
onError: (error: any) => void;
|
||||||
@@ -17,6 +19,8 @@ export enum WebRTCPacketType {
|
|||||||
MESSAGE = 0,
|
MESSAGE = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const CHUNK_SIZE = 16 * 1024 * 1024;
|
||||||
|
|
||||||
export interface WebRTCPacket {
|
export interface WebRTCPacket {
|
||||||
encrypted: boolean; // 1 bit
|
encrypted: boolean; // 1 bit
|
||||||
type: WebRTCPacketType; // 7 bits
|
type: WebRTCPacketType; // 7 bits
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
export enum ConnectionState {
|
export enum ConnectionState {
|
||||||
CONNECTING,
|
CONNECTING,
|
||||||
RECONNECTING,
|
RECONNECTING,
|
||||||
|
|||||||
197
src/utils/buffer.ts
Normal file
197
src/utils/buffer.ts
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
// nodejs like buffer class for browser
|
||||||
|
export class WebBuffer {
|
||||||
|
private data: Uint8Array<ArrayBuffer>;
|
||||||
|
// the number of bytes read from the buffer, this allows for you to read the buffer without having to specify the offset every time
|
||||||
|
private count = 0;
|
||||||
|
private dataView: DataView;
|
||||||
|
|
||||||
|
constructor(data: ArrayBuffer) {
|
||||||
|
this.data = new Uint8Array(data);
|
||||||
|
this.dataView = new DataView(data);
|
||||||
|
|
||||||
|
return new Proxy(this, {
|
||||||
|
get(target, prop, receiver) {
|
||||||
|
// Check if the property is a string that represents a valid number (array index)
|
||||||
|
if (typeof prop === 'string' && /^\d+$/.test(prop)) {
|
||||||
|
const index = parseInt(prop, 10);
|
||||||
|
// Delegate array-like access to the underlying Uint8Array
|
||||||
|
return target.data[index];
|
||||||
|
}
|
||||||
|
// For all other properties (methods like slice, getters like length, etc.),
|
||||||
|
// use the default property access behavior on the target object.
|
||||||
|
return Reflect.get(target, prop, receiver);
|
||||||
|
},
|
||||||
|
set(target, prop, value, receiver) {
|
||||||
|
// Check if the property is a string that represents a valid number (array index)
|
||||||
|
if (typeof prop === 'string' && /^\d+$/.test(prop)) {
|
||||||
|
const index = parseInt(prop, 10);
|
||||||
|
// Delegate array-like assignment to the underlying Uint8Array
|
||||||
|
target.data[index] = value;
|
||||||
|
return true; // Indicate success
|
||||||
|
}
|
||||||
|
// For all other properties, use the default property assignment behavior.
|
||||||
|
return Reflect.set(target, prop, value, receiver);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[index: number]: number;
|
||||||
|
|
||||||
|
get length(): number {
|
||||||
|
return this.data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
get buffer(): ArrayBuffer {
|
||||||
|
return this.data.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
slice(start: number, end?: number): WebBuffer {
|
||||||
|
return new WebBuffer(this.data.slice(start, end).buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
set(data: number, offset: number) {
|
||||||
|
this.dataView.setUint8(offset, data);
|
||||||
|
// this.data.set(data, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
read(length?: number, offset?: number): Uint8Array {
|
||||||
|
if (length === undefined) {
|
||||||
|
length = this.length - this.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset === undefined) {
|
||||||
|
offset = this.count;
|
||||||
|
this.count += length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.data.slice(offset, offset + length);
|
||||||
|
}
|
||||||
|
|
||||||
|
write(data: Uint8Array, offset?: number) {
|
||||||
|
if (offset === undefined) {
|
||||||
|
offset = this.count;
|
||||||
|
this.count += data.byteLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < data.byteLength; i++) {
|
||||||
|
this.dataView.setUint8(offset + i, data[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readInt8(offset?: number): number {
|
||||||
|
if (offset === undefined) {
|
||||||
|
offset = this.count;
|
||||||
|
this.count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.dataView.getUint8(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeInt8(value: number, offset?: number) {
|
||||||
|
if (offset === undefined) {
|
||||||
|
offset = this.count;
|
||||||
|
this.count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataView.setUint8(offset, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
readInt16LE(offset?: number): number {
|
||||||
|
if (offset === undefined) {
|
||||||
|
offset = this.count;
|
||||||
|
this.count += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.dataView.getInt16(offset, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeInt16LE(value: number, offset?: number) {
|
||||||
|
if (offset === undefined) {
|
||||||
|
offset = this.count;
|
||||||
|
this.count += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataView.setInt16(offset, value, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
readInt32LE(offset?: number): number {
|
||||||
|
if (offset === undefined) {
|
||||||
|
offset = this.count;
|
||||||
|
this.count += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.dataView.getInt32(offset, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeInt32LE(value: number, offset?: number) {
|
||||||
|
if (offset === undefined) {
|
||||||
|
offset = this.count;
|
||||||
|
this.count += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataView.setInt32(offset, value, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
readBigInt64LE(offset?: number): bigint {
|
||||||
|
if (offset === undefined) {
|
||||||
|
offset = this.count;
|
||||||
|
this.count += 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.dataView.getBigInt64(offset, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeBigInt64LE(value: bigint, offset?: number) {
|
||||||
|
if (offset === undefined) {
|
||||||
|
offset = this.count;
|
||||||
|
this.count += 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataView.setBigInt64(offset, value, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no length is specified, reads until the end of the buffer
|
||||||
|
readString(length?: number, offset?: number): string {
|
||||||
|
if (length === undefined) {
|
||||||
|
length = this.length - this.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset === undefined) {
|
||||||
|
offset = this.count;
|
||||||
|
this.count += length;
|
||||||
|
}
|
||||||
|
|
||||||
|
let textDeccoder = new TextDecoder();
|
||||||
|
let readTextBuf = this.data.slice(offset, offset + length);
|
||||||
|
let value = textDeccoder.decode(readTextBuf);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeString(value: string, offset?: number) {
|
||||||
|
if (offset === undefined) {
|
||||||
|
offset = this.count;
|
||||||
|
this.count += value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
let textEncoder = new TextEncoder();
|
||||||
|
let textBuf = textEncoder.encode(value);
|
||||||
|
|
||||||
|
this.data.set(textBuf, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// lets you peek at the next byte without advancing the read pointer
|
||||||
|
peek(): number {
|
||||||
|
return this.data[this.count];
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
// Return an iterator over the values of the underlying Uint8Array
|
||||||
|
return this.data.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: Add Symbol.toStringTag for better console output
|
||||||
|
get [Symbol.toStringTag]() {
|
||||||
|
return 'WebBuffer';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { writable, get, type Writable } from "svelte/store";
|
import { writable, get, type Writable } from "svelte/store";
|
||||||
import { WebRTCPeer } from "$lib/webrtc";
|
import { WebRTCPeer } from "$lib/webrtc";
|
||||||
import { WebRTCPacketType } from "../types/webrtc";
|
import { CHUNK_SIZE, WebRTCPacketType } from "../types/webrtc";
|
||||||
import { room } from "../stores/roomStore";
|
import { room } from "../stores/roomStore";
|
||||||
import { ConnectionState, type Room } from "../types/websocket";
|
import { ConnectionState, type Room } from "../types/websocket";
|
||||||
import { messages } from "../stores/messageStore";
|
import { advertisedOffers, fileRequestIds, messages, receivedOffers } from "../stores/messageStore";
|
||||||
import { MessageType, type Message } from "../types/message";
|
import { MessageType, type Message } from "../types/message";
|
||||||
import { WebSocketMessageType, type WebSocketMessage } from "../types/websocket";
|
import { WebSocketMessageType, type WebSocketMessage } from "../types/websocket";
|
||||||
|
import { WebBuffer } from "./buffer";
|
||||||
|
|
||||||
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);
|
||||||
@@ -13,41 +14,225 @@ export let isRTCConnected: Writable<boolean> = writable(false);
|
|||||||
export let dataChannelReady: Writable<boolean> = writable(false);
|
export let dataChannelReady: Writable<boolean> = writable(false);
|
||||||
export let keyExchangeDone: Writable<boolean> = writable(false);
|
export let keyExchangeDone: Writable<boolean> = writable(false);
|
||||||
|
|
||||||
|
let downloadStream: WritableStream<Uint8Array> | undefined;
|
||||||
|
let downloadWriter: WritableStreamDefaultWriter<Uint8Array<ArrayBufferLike>> | undefined;
|
||||||
|
|
||||||
|
let fileAck: Map<bigint, Writable<boolean>> = new Map();
|
||||||
|
|
||||||
|
function beforeUnload(event: BeforeUnloadEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.returnValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPageHide(event: PageTransitionEvent) {
|
||||||
|
if (event.persisted) {
|
||||||
|
// page is frozen, but not closed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (downloadWriter && !downloadWriter.closed) {
|
||||||
|
downloadWriter.abort();
|
||||||
|
}
|
||||||
|
if (downloadStream) {
|
||||||
|
downloadStream.getWriter().abort();
|
||||||
|
}
|
||||||
|
downloadStream = undefined;
|
||||||
|
downloadWriter = undefined;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
const callbacks = {
|
const callbacks = {
|
||||||
onConnected: () => {
|
onConnected: () => {
|
||||||
console.log("Connected to peer");
|
console.log("Connected to peer");
|
||||||
isRTCConnected.set(true);
|
isRTCConnected.set(true);
|
||||||
},
|
},
|
||||||
//! TODO: come up with a more complex room system. This is largely for testing purposes
|
//! TODO: come up with a more complex room system. This is largely for testing purposes
|
||||||
onMessage: (message: { type: WebRTCPacketType, data: ArrayBuffer }) => {
|
onMessage: async (message: { type: WebRTCPacketType, data: ArrayBuffer }, webRtcPeer: WebRTCPeer) => {
|
||||||
console.log("WebRTC Received message:", message);
|
console.log("WebRTC Received message:", message);
|
||||||
// if (typeof message === 'object' && message instanceof Blob) {
|
if (message.type !== WebRTCPacketType.MESSAGE) {
|
||||||
// // download the file
|
return;
|
||||||
// const url = URL.createObjectURL(message);
|
}
|
||||||
// const a = document.createElement('a');
|
|
||||||
// a.href = url;
|
|
||||||
// a.download = message.name;
|
|
||||||
// document.body.appendChild(a);
|
|
||||||
// a.click();
|
|
||||||
// setTimeout(() => {
|
|
||||||
// document.body.removeChild(a);
|
|
||||||
// window.URL.revokeObjectURL(url);
|
|
||||||
// }, 100);
|
|
||||||
// }
|
|
||||||
|
|
||||||
console.log("Received message:", message);
|
console.log("Received message:", message.type, new Uint8Array(message.data));
|
||||||
|
|
||||||
// TODO: fixup
|
let messageBuf = new WebBuffer(message.data);
|
||||||
if (message.type === WebRTCPacketType.MESSAGE) {
|
console.log("manually extracted type:", messageBuf[0]);
|
||||||
|
|
||||||
|
let messageType = messageBuf[0] as MessageType;
|
||||||
|
let messageData = messageBuf.slice(1);
|
||||||
let textDecoder = new TextDecoder();
|
let textDecoder = new TextDecoder();
|
||||||
let json: Message = JSON.parse(textDecoder.decode(message.data));
|
|
||||||
json.initiator = false;
|
console.log("Received message:", messageType, messageData);
|
||||||
messages.set([...get(messages), json]);
|
|
||||||
|
switch (messageType) {
|
||||||
|
case MessageType.TEXT:
|
||||||
|
messages.set([...get(messages), {
|
||||||
|
initiator: false,
|
||||||
|
type: messageType,
|
||||||
|
data: textDecoder.decode(messageData.buffer),
|
||||||
|
}]);
|
||||||
|
break;
|
||||||
|
case MessageType.FILE_OFFER:
|
||||||
|
let fileSize = messageData.readBigInt64LE();
|
||||||
|
let fileNameSize = messageData.readInt16LE();
|
||||||
|
let fileName = messageData.readString(fileNameSize);
|
||||||
|
let id = messageData.readBigInt64LE();
|
||||||
|
|
||||||
|
get(receivedOffers).set(id, { name: fileName, size: fileSize });
|
||||||
|
|
||||||
|
messages.set([...get(messages), {
|
||||||
|
initiator: false,
|
||||||
|
type: messageType,
|
||||||
|
data: {
|
||||||
|
fileSize,
|
||||||
|
fileNameSize,
|
||||||
|
fileName,
|
||||||
|
id,
|
||||||
|
text: messageData.peek() ? messageData.readString() : null,
|
||||||
|
}
|
||||||
|
}]);
|
||||||
|
break;
|
||||||
|
case MessageType.FILE_REQUEST:
|
||||||
|
// the id that coresponds to our file offer
|
||||||
|
let offerId = messageData.readBigInt64LE();
|
||||||
|
if (!get(advertisedOffers).has(offerId)) {
|
||||||
|
console.error("Unknown file offer id:", offerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetFile = get(advertisedOffers).get(offerId)!;
|
||||||
|
let fileStream = targetFile.stream();
|
||||||
|
let fileReader = fileStream.getReader();
|
||||||
|
|
||||||
|
let idleTimeout = setTimeout(() => {
|
||||||
|
console.error("Timed out waiting for file ack");
|
||||||
|
fileReader.cancel();
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
// the id we send the file data with
|
||||||
|
let fileRequestId = messageData.readBigInt64LE();
|
||||||
|
let fileChunk = await fileReader.read();
|
||||||
|
|
||||||
|
// reactive variable to track if the peer received the chunk
|
||||||
|
fileAck.set(fileRequestId, writable(false));
|
||||||
|
|
||||||
|
function sendChunk() {
|
||||||
|
if (!fileChunk.value) {
|
||||||
|
clearTimeout(idleTimeout);
|
||||||
|
fileReader.cancel();
|
||||||
|
console.error("Chunk not set");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// header + id + data
|
||||||
|
let fileBuf = new WebBuffer(new Uint8Array(1 + 8 + fileChunk.value.byteLength).buffer);
|
||||||
|
|
||||||
|
fileBuf.writeInt8(MessageType.FILE);
|
||||||
|
fileBuf.writeBigInt64LE(fileRequestId);
|
||||||
|
fileBuf.write(fileChunk.value);
|
||||||
|
webRtcPeer.send(fileBuf.buffer, WebRTCPacketType.MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendChunk();
|
||||||
|
|
||||||
|
let unsubscribe = fileAck.get(fileRequestId)!.subscribe(async (value) => {
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileChunk = await fileReader.read();
|
||||||
|
|
||||||
|
if (fileChunk.done) {
|
||||||
|
// send the done message
|
||||||
|
let fileDoneBuf = new WebBuffer(new ArrayBuffer(1 + 8));
|
||||||
|
fileDoneBuf.writeInt8(MessageType.FILE_DONE);
|
||||||
|
fileDoneBuf.writeBigInt64LE(fileRequestId);
|
||||||
|
webRtcPeer.send(fileDoneBuf.buffer, WebRTCPacketType.MESSAGE);
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
fileReader.cancel();
|
||||||
|
fileAck.delete(fileRequestId);
|
||||||
|
clearTimeout(idleTimeout);
|
||||||
|
unsubscribe();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendChunk();
|
||||||
|
fileAck.get(fileRequestId)!.set(false);
|
||||||
|
clearTimeout(idleTimeout);
|
||||||
|
idleTimeout = setTimeout(() => {
|
||||||
|
console.error("Timed out waiting for file ack");
|
||||||
|
fileReader.cancel();
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
console.log("Received file request");
|
||||||
|
break;
|
||||||
|
case MessageType.FILE:
|
||||||
|
let requestId = messageData.readBigInt64LE();
|
||||||
|
let receivedOffserId = get(fileRequestIds).get(requestId);
|
||||||
|
if (!receivedOffserId) {
|
||||||
|
console.error("Received file message for unknown file id:", requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = get(receivedOffers).get(receivedOffserId);
|
||||||
|
if (!file) {
|
||||||
|
console.error("Unknown file id:", requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloadStream === undefined) {
|
||||||
|
window.addEventListener("pagehide", onPageHide);
|
||||||
|
window.addEventListener("beforeunload", beforeUnload);
|
||||||
|
downloadStream = window.streamSaver.createWriteStream(file.name, { size: Number(file.size) });
|
||||||
|
downloadWriter = downloadStream.getWriter();
|
||||||
|
}
|
||||||
|
|
||||||
|
await downloadWriter!.write(new Uint8Array(messageData.read()));
|
||||||
|
|
||||||
|
let fileAckBuf = new WebBuffer(new ArrayBuffer(1 + 8));
|
||||||
|
fileAckBuf.writeInt8(MessageType.FILE_ACK);
|
||||||
|
fileAckBuf.writeBigInt64LE(requestId);
|
||||||
|
webRtcPeer.send(fileAckBuf.buffer, WebRTCPacketType.MESSAGE);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case MessageType.FILE_DONE:
|
||||||
|
console.log("Received file done");
|
||||||
|
let fileDoneId = messageData.readBigInt64LE();
|
||||||
|
if (!get(fileRequestIds).has(fileDoneId)) {
|
||||||
|
console.error("Unknown file done id:", fileDoneId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.removeEventListener("pagehide", onPageHide);
|
||||||
|
window.removeEventListener("beforeunload", beforeUnload);
|
||||||
|
|
||||||
|
if (downloadWriter) {
|
||||||
|
downloadWriter.close();
|
||||||
|
downloadWriter = undefined;
|
||||||
|
downloadStream = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case MessageType.FILE_ACK:
|
||||||
|
console.log("Received file ack");
|
||||||
|
let fileAckId = messageData.readBigInt64LE();
|
||||||
|
if (!fileAck.has(fileAckId)) {
|
||||||
|
console.error("Unknown file ack id:", fileAckId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileAck.get(fileAckId)!.set(true);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn("Unhandled message type:", messageType);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDataChannelOpen: () => {
|
onDataChannelStateChange: (state: boolean) => {
|
||||||
console.log("Data channel open");
|
console.log(`Data channel ${state ? "open" : "closed"}`);
|
||||||
dataChannelReady.set(true);
|
dataChannelReady.set(state);
|
||||||
},
|
},
|
||||||
onKeyExchangeDone: async () => {
|
onKeyExchangeDone: async () => {
|
||||||
console.log("Key exchange done");
|
console.log("Key exchange done");
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { webSocketServer } from './src/websocket.ts';
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [tailwindcss(), sveltekit(), webSocketServer],
|
plugins: [tailwindcss(), sveltekit(), webSocketServer],
|
||||||
server: {
|
server: {
|
||||||
allowedHosts: ['.trycloudflare.com'],
|
allowedHosts: true,
|
||||||
},
|
},
|
||||||
ssr: {
|
ssr: {
|
||||||
// ts-mls is problematic, make vite bundle it
|
// ts-mls is problematic, make vite bundle it
|
||||||
|
|||||||
Reference in New Issue
Block a user