initial commit

This commit is contained in:
Aubrey 2024-12-17 18:52:13 -06:00
commit dcf885a7b0
No known key found for this signature in database
21 changed files with 31284 additions and 0 deletions

26
.gitignore vendored Normal file
View file

@ -0,0 +1,26 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
*.pem
jack/

1
.npmrc Normal file
View file

@ -0,0 +1 @@
engine-strict=true

4
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,4 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"files.autoSave": "onFocusChange"
}

38
README.md Normal file
View file

@ -0,0 +1,38 @@
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npx sv create
# create a new project in my-app
npx sv create my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

25
package.json Normal file
View file

@ -0,0 +1,25 @@
{
"name": "proximity-chat",
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-node": "^5.2.0",
"@sveltejs/kit": "^2.9.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@vitejs/plugin-basic-ssl": "^1.2.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^6.0.0"
},
"dependencies": {
"peerjs": "^1.5.4"
}
}

1145
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

13
src/app.d.ts vendored Normal file
View file

@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

12
src/app.html Normal file
View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

149
src/lib/binary/reader.ts Normal file
View file

@ -0,0 +1,149 @@
// import { BinaryStreamReader } from "./readerStream";
// import { AsyncReadMethod, AsyncReadMethodReturnValue, ObjectValue, ObjectValueType, ReadMethod, ReadMethodReturnValue } from "./types";
// export class BinaryReader {
// protected index: number = 0;
// constructor(
// protected readonly buffer: DataView,
// ) {}
// readBoolean(): boolean {
// return this.readUInt8() != 0;
// }
// readSInt8(): number {
// const value = this.buffer.getInt8(this.index);
// this.index++;
// return value;
// }
// readUInt8(): number {
// const value = this.buffer.getUint8(this.index);
// this.index++;
// return value;
// }
// readSInt16(): number {
// const value = this.buffer.getInt16(this.index, true);
// this.index += 2;
// return value;
// }
// readUInt16(): number {
// const value = this.buffer.getUint16(this.index, true);
// this.index += 2;
// return value;
// }
// readSInt32(): number {
// const value = this.buffer.getInt32(this.index, true);
// this.index += 4;
// return value;
// }
// readUInt32(): number {
// const value = this.buffer.getUint32(this.index, true);
// this.index += 4;
// return value;
// }
// readSInt64(): bigint {
// const value = this.buffer.getBigInt64(this.index, true);
// this.index += 8;
// return value;
// }
// readUInt64(): bigint {
// const value = this.buffer.getBigUint64(this.index, true);
// this.index += 8;
// return value;
// }
// readFloat32(): number {
// const value = this.buffer.getFloat32(this.index, true);
// this.index += 4;
// return value;
// }
// readFloat64(): number {
// const value = this.buffer.getFloat64(this.index, true);
// this.index += 8;
// return value;
// }
// readString(length: number, encoding?: string): string {
// const decoder = new TextDecoder(encoding);
// return decoder.decode(this.buffer.buffer.slice(this.index, this.index += length));
// }
// async readArray<T>(size: number, readMethod: ReadMethod<T> | AsyncReadMethod<T>): Promise<T[]> {
// const array = new Array<T>(size);
// for (let i = 0; i < size; i++) {
// array[i] = await this.read(readMethod);
// }
// return array;
// }
// async readObject(): Promise<ObjectValue> {
// return await this.readMappedShortEnum({
// [ObjectValueType.Object]: async (r: BinaryReader | BinaryStreamReader) => Object.fromEntries(await r.readArray(await r.readUInt32(), async r2 => [ await r2.readString(await r2.readUInt32()), await r2.readObject() ])),
// [ObjectValueType.Array]: async (r: BinaryReader | BinaryStreamReader) => await r.readArray(await r.readUInt32(), async (r2: BinaryReader | BinaryStreamReader) => await r2.readObject()),
// [ObjectValueType.Boolean]: async (r: BinaryReader | BinaryStreamReader) => await r.readBoolean(),
// [ObjectValueType.Null]: () => null,
// [ObjectValueType.Number]: async (r: BinaryReader | BinaryStreamReader) => await r.readFloat64(),
// [ObjectValueType.String]: async (r: BinaryReader | BinaryStreamReader) => await r.readString(await r.readUInt32()),
// })
// }
// async read<T>(readMethod: ReadMethod<T> | AsyncReadMethod<T>): Promise<T> {
// if ("deserialize" in readMethod) {
// return await readMethod.deserialize(this);
// }
// return await readMethod(this);
// }
// readShortEnum<T extends number>(): T {
// return this.readUInt8() as T;
// }
// readEnum<T extends number>(): T {
// return this.readUInt16() as T;
// }
// readDate(): Date {
// return new Date(Number(this.readUInt64()));
// }
// async readMappedEnum<T extends number, R extends Record<T, ReadMethod<any> | AsyncReadMethod<T>>>(mappedType: R): Promise<ReadMethodReturnValue<R[keyof R]> | AsyncReadMethodReturnValue<R[keyof R]>> {
// return await this.read(mappedType[this.readEnum<T>()]);
// }
// async readMappedShortEnum<T extends number, R extends Record<T, ReadMethod<any> | AsyncReadMethod<T>>>(mappedType: R): Promise<ReadMethodReturnValue<R[keyof R]> | AsyncReadMethodReturnValue<R[keyof R]>> {
// return await this.read(mappedType[this.readShortEnum<T>()]);
// }
// }

View file

@ -0,0 +1,166 @@
import type { AsyncReadMethod } from "./types";
export class BinaryStreamReader {
chunks: { view: ArrayBuffer, index: number }[] = [];
readStack: { size: number, resolve: (value: DataView | PromiseLike<DataView>) => void, reject: (reason?: any) => void }[] = [];
constructor() { }
push(view: ArrayBuffer): void {
this.chunks.push({ view, index: 0 });
let next = this.readStack[0];
while (next && this.hasFree(next.size)) {
this.readStack.shift()!.resolve(this.get(next.size));
next = this.readStack[0];
}
}
static fromReader(reader: ReadableStreamDefaultReader): BinaryStreamReader {
const streamReader = new BinaryStreamReader();
(async () => {
while (true) {
const data = await reader.read();
console.log("got data", data);
streamReader.push(data.value);
}
})();
return streamReader;
}
private get(size: number): DataView {
const b = new Uint8Array(size);
let bHead: number = 0;
for (let i = 0; i < this.chunks.length; i++) {
const buf = this.chunks[i];
let wantedBytes = (size - bHead)
let availableBytes = (buf.view.byteLength - buf.index);
if (wantedBytes < availableBytes) {
b.set(new Uint8Array(buf.view.slice(buf.index, buf.index += wantedBytes)), bHead);
bHead += wantedBytes;
break;
} else {
b.set(new Uint8Array(buf.view.slice(buf.index, buf.index += availableBytes)), bHead);
bHead += availableBytes;
}
}
while (this.chunks[0] && this.chunks[0].view.byteLength == this.chunks[0].index) {
this.chunks.shift();
}
return new DataView(b.buffer);
}
private hasFree(size: number) {
let accumulated: number = 0;
for (let i = 0; i < this.chunks.length; i++) {
accumulated += this.chunks[i].view.byteLength - this.chunks[i].index;
if (accumulated >= size) return true;
}
return false;
}
async bytes(size: number): Promise<DataView> {
return new Promise((res, rej) => {
if (this.hasFree(size)) {
return res(this.get(size));
}
this.readStack.push({ size, resolve: res, reject: rej });
})
}
async readBoolean(): Promise<boolean> {
return await this.readUInt8() != 0;
}
async readSInt8(): Promise<number> {
return (await this.bytes(1)).getInt8(0);
}
async readUInt8(): Promise<number> {
return (await this.bytes(1)).getUint8(0);
}
async readSInt16(): Promise<number> {
return (await this.bytes(2)).getInt16(0);
}
async readUInt16(): Promise<number> {
return (await this.bytes(2)).getUint16(0);
}
async readSInt32(): Promise<number> {
return (await this.bytes(4)).getInt32(0);
}
async readUInt32(): Promise<number> {
return (await this.bytes(4)).getUint32(0);
}
async readSInt64(): Promise<bigint> {
return (await this.bytes(8)).getBigInt64(0);
}
async readUInt64(): Promise<bigint> {
return (await this.bytes(8)).getBigUint64(0);
}
async readFloat32(): Promise<number> {
return (await this.bytes(4)).getFloat32(0);
}
async readFloat64(): Promise<number> {
return (await this.bytes(8)).getFloat64(0);
}
async readString(length: number, encoding?: string): Promise<string> {
const decoder = new TextDecoder(encoding);
return decoder.decode(await this.bytes(length));
}
async readFixedString(length: number, encoding?: string): Promise<string> {
const decoder = new TextDecoder(encoding);
const bytes = await this.bytes(length);
const slice = new Uint8Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
const end = slice.findIndex(byte => byte == 0);
console.log(bytes.byteOffset, bytes.byteOffset + end, end, bytes.byteLength);
return decoder.decode(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + end));
}
async readArray<T>(size: number, readMethod: AsyncReadMethod<T, this>): Promise<T[]> {
const array = new Array<T>(size);
for (let i = 0; i < size; i++) {
array[i] = await this.read(readMethod);
}
return array;
}
async read<T>(readMethod: AsyncReadMethod<T, this>): Promise<T> {
if ("deserialize" in readMethod) {
return await readMethod.deserialize(this);
}
return await readMethod(this);
}
async readDate(): Promise<Date> {
return new Date(Number(await this.readUInt64()));
}
}

18
src/lib/binary/types.ts Normal file
View file

@ -0,0 +1,18 @@
type Method<T, R, F extends string> = ((reader: R) => T) | { [key in F]: (reader: R) => T };
export type ReadMethod<T, R> = Method<T, R, "deserialize">;
export type WriteMethod<W> = Method<void, W, "serialize">;
export type AsyncReadMethod<T, R> = Method<T | Promise<T>, R, "deserialize">;
export type ReadMethodReturnValue<X> = X extends ReadMethod<infer Y, any> ? (Y extends Promise<infer Z> ? Z : Y) : never;
export enum ObjectValueType {
Object,
Array,
String,
Number,
Boolean,
Null,
}
export type ObjectValue = { [key: string]: ObjectValue } | ObjectValue[] | null | string | number | boolean

130
src/lib/binary/writer.ts Normal file
View file

@ -0,0 +1,130 @@
import type { ObjectValue, ObjectValueType, WriteMethod } from "./types";
export class BinaryWriter {
protected index: number = 0;
protected buffer: DataView;
constructor(baseSize?: number) {
this.buffer = new DataView(new ArrayBuffer(baseSize ?? 0));
}
protected request(size: number) {
let needSize = size - (this.buffer.byteLength - this.index);
if (needSize > 0) {
let newBuffer = new Uint8Array(this.buffer.byteLength + needSize);
newBuffer.set(new Uint8Array(this.buffer.buffer), 0);
this.buffer = new DataView(newBuffer.buffer);
}
}
writeBoolean(value: boolean): void {
this.writeUInt8(value ? 1 : 0);
}
writeSInt8(value: number): void {
this.request(1);
this.buffer.setInt8(this.index, value);
this.index++;
}
writeUInt8(value: number): void {
this.request(1);
this.buffer.setUint8(this.index, value);
this.index++;
}
writeSInt16(value: number): void {
this.request(2);
this.buffer.setInt16(this.index, value);
this.index += 2;
}
writeUInt16(value: number): void {
this.request(2);
this.buffer.setUint16(this.index, value);
this.index += 2;
}
writeSInt32(value: number): void {
this.request(4);
this.buffer.setInt32(this.index, value);
this.index += 4;
}
writeUInt32(value: number): void {
this.request(4);
this.buffer.setUint32(this.index, value);
this.index += 4;
}
writeSInt64(value: bigint): void {
this.request(8);
this.buffer.setBigInt64(this.index, value);
this.index += 8;
}
writeUInt64(value: bigint): void {
this.request(8);
this.buffer.setBigUint64(this.index, value);
this.index += 8;
}
writeFloat32(value: number): void {
this.request(4);
this.buffer.setFloat32(this.index, value);
this.index += 4;
}
writeFloat64(value: number): void {
this.request(8);
this.buffer.setFloat64(this.index, value);
this.index += 8;
}
writeString(string: string): void {
const encoder = new TextEncoder();
const encoded = encoder.encode(string);
this.request(encoded.byteLength);
new Uint8Array(this.buffer.buffer).set(encoded, this.index);
this.index += encoded.byteLength;
}
writeArray(array: WriteMethod<BinaryWriter>[]): void {
for (let i = 0; i < array.length; i++) {
this.write(array[i]);
}
}
write(writeMethod: WriteMethod<BinaryWriter>): void {
if ("serialize" in writeMethod) {
writeMethod.serialize(this);
return;
}
writeMethod(this);
}
}

49
src/lib/client.svelte.ts Normal file
View file

@ -0,0 +1,49 @@
import Peer from "peerjs";
import { readHello } from "./protocol/hello";
import { BinaryStreamReader } from "./binary/readerStream";
export class Client {
static async connect() {
const transport = new WebTransport(`https://${location.hostname}:4433`, { allowPooling: false });
await transport.ready;
const stream = await transport.createBidirectionalStream();
const peer = new Peer();
await new Promise((res) => {
peer.on("open", () => {
console.log("open!", peer.id)
res(void 0);
});
});
await stream.writable.getWriter().write(new TextEncoder().encode(peer.id));
const reader = BinaryStreamReader.fromReader(stream.readable.getReader());
const players = $state(await readHello(reader));
return new Client(players, peer, transport, stream);
}
private constructor(
public players: Player[],
private peer: Peer,
private transport: WebTransport,
private stream: WebTransportBidirectionalStream
) {
}
get id() {
return this.peer.id;
}
closed() {
// return transport.closed;
}
}
export class Player {
constructor(public name: string, public id: number, public position: number[], public stage: string) { }
}

24
src/lib/protocol/hello.ts Normal file
View file

@ -0,0 +1,24 @@
import { BinaryStreamReader } from "../binary/readerStream";
import { Player } from "../client.svelte";
export async function readHello(reader: BinaryStreamReader) {
console.log("reading");
const count = await reader.readUInt8();
console.log("reading", count);
const players = await reader.readArray(count, async (reader) => {
const name = await reader.readFixedString(0x20);
console.log(name);
const id = await reader.readUInt32();
const position = await reader.readArray(3, async reader => await reader.readFloat32());
const stage = await reader.readFixedString(0x40);
return new Player(
name,
id,
position,
stage,
);
});
return players;
}

View file

@ -0,0 +1 @@
export { } from "./hello";

36
src/routes/+page.svelte Normal file
View file

@ -0,0 +1,36 @@
<script lang="ts">
import { onMount } from "svelte";
import { Client } from "$lib/client.svelte.ts";
import { BinaryStreamReader } from "$lib/binary/readerStream";
let name = $state("none");
let client: Client = undefined as any;
let players = $derived(client?.players ?? []);
async function a() {
client = await Client.connect();
console.log(client);
}
onMount(a);
</script>
<div>
I am <input type="text" maxlength="32" list="connectedPlayers" />
</div>
<datalist>
{#each players as player}
<option>{player.name}</option>
{/each}
</datalist>
<div>
Connected players - {players.length}<br />
{#each players as player}
<pre class="name">{player.name}</pre>
{/each}
</div>
<style>
.name {
font-style: italic;
}
</style>

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

18
svelte.config.js Normal file
View file

@ -0,0 +1,18 @@
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;

22
tsconfig.json Normal file
View file

@ -0,0 +1,22 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
},
"include": [
"./webtransport.d.ts"
]
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

15
vite.config.ts Normal file
View file

@ -0,0 +1,15 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { readFileSync } from 'node:fs';
export default defineConfig({
server: {
https: {
key: readFileSync('key.pem'),
cert: readFileSync('cert.pem'),
},
},
plugins: [
sveltekit(),
]
});

29392
webtransport.d.ts vendored Normal file

File diff suppressed because it is too large Load diff