diff --git a/auxilary.ts b/auxilary.ts index 1c556b1..d1e1587 100644 --- a/auxilary.ts +++ b/auxilary.ts @@ -1,38 +1,40 @@ -export class TwoWayMap { - private mapAtoB = new Map>(); - private mapBtoA = new Map>(); +import { WS } from "./index"; + +export class SubscriptionMap { + private channelsPerClient = new Map>(); + private clientsPerChannel = new Map>(); // prepare - private prepareA(a: A) { - if (this.mapAtoB.has(a)) return; - this.mapAtoB.set(a, new Set()); + private prepareChannelSet(ws: WS) { + if (this.channelsPerClient.has(ws)) return; + this.channelsPerClient.set(ws, new Set()); } - private prepareB(b: B) { - if (this.mapBtoA.has(b)) return; - this.mapBtoA.set(b, new Set()); + private prepareClientSet(channel: string) { + if (this.clientsPerChannel.has(channel)) return; + this.clientsPerChannel.set(channel, new Set()); } // utility - private getListOnA(a: A) { - return this.mapAtoB.get(a); + getChannelList(ws: WS) { + return this.channelsPerClient.get(ws); } - private getListOnB(b: B) { - return this.mapBtoA.get(b); + getClientList(channel: string) { + return this.clientsPerChannel.get(channel); } - // public - set(a: A, b: B) { - this.prepareA(a); - this.prepareB(b); + // modifications + set(ws: WS, channel: string) { + this.prepareChannelSet(ws); + this.prepareClientSet(channel); - this.getListOnA(a)?.add(b); - this.getListOnB(b)?.add(a); + this.getChannelList(ws)?.add(channel); + this.getClientList(channel)?.add(ws); } - delete(a: A, b: B) { - this.getListOnA(a)?.delete(b); - this.getListOnB(b)?.delete(a); + delete(ws: WS, channel: string) { + this.getChannelList(ws)?.delete(channel); + this.getClientList(channel)?.delete(ws); } } diff --git a/index.ts b/index.ts index 06799c1..158db3b 100644 --- a/index.ts +++ b/index.ts @@ -1,10 +1,16 @@ +import { forgetConnection, processMessage } from "./websocket-handler"; + import Path from "path"; +import { ServerWebSocket } from "bun"; // CONFIG const staticDir = "static/dist"; const defaultPage = "index.html"; const errorPage = "404.html"; +// TYPES +export type WS = ServerWebSocket; + // SERVER const server = Bun.serve({ fetch(request, server) { @@ -27,9 +33,11 @@ const server = Bun.serve({ websocket: { message(ws, message) { - + processMessage(ws, message.toString()); + }, + close(ws, code, reason) { + forgetConnection(ws); }, - close(ws, code, reason) {}, }, }); diff --git a/static/dist/index.js b/static/dist/index.js index c6ddc55..fd30e2b 100644 --- a/static/dist/index.js +++ b/static/dist/index.js @@ -1,5 +1,27 @@ (() => { // node_modules/bloatless-react/index.ts + var State = class { + _value; + _bindings = /* @__PURE__ */ new Set(); + constructor(initialValue) { + this._value = initialValue; + } + get value() { + return this._value; + } + set value(newValue) { + if (this._value == newValue) return; + this._value = newValue; + this.callSubscriptions(); + } + callSubscriptions() { + this._bindings.forEach((fn) => fn(this._value)); + } + subscribe(fn) { + this._bindings.add(fn); + fn(this._value); + } + }; function createElement(tagName, attributes = {}, ...children) { const element = document.createElement(tagName); if (attributes != null) @@ -58,7 +80,36 @@ // src/index.tsx var ws = new WebSocket(`ws://${window.location.host}`); - ws.onopen = () => ws.send("Hello!"); - ws.onmessage = (data) => console.log(data); - document.querySelector("main").append(/* @__PURE__ */ createElement("h1", null, "Hello there!")); + ws.addEventListener("message", (message) => { + receivedMessage.value = message.data; + }); + function send(object) { + const stringified = JSON.stringify(object); + ws.send(stringified); + } + var receivedMessage = new State(""); + var subscriptionChannel = new State(""); + var messageChannel = new State(""); + var messageBody = new State(""); + function subscribe() { + send({ subscribeChannel: subscriptionChannel.value }); + } + function unsubscribe() { + send({ unsubscribeChannel: subscriptionChannel.value }); + } + function sendMessage() { + send({ + messageChannel: messageChannel.value, + messageBody: messageBody.value + }); + } + document.querySelector("main").append( + /* @__PURE__ */ createElement("article", null, /* @__PURE__ */ createElement("header", null, "UDN Test"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("div", { class: "tile" }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, "Received message"), /* @__PURE__ */ createElement("p", { class: "secondary", "subscribe:innerText": receivedMessage }))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, "Subscribe to channel"), /* @__PURE__ */ createElement( + "input", + { + placeholder: "my-channel", + "bind:value": subscriptionChannel + } + ))), /* @__PURE__ */ createElement("div", { class: "flex-row width-input justify-end" }, /* @__PURE__ */ createElement("button", { class: "danger width-50", "on:click": unsubscribe }, "Unsubscribe"), /* @__PURE__ */ createElement("button", { class: "primary width-50", "on:click": subscribe }, "Subscribe", /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_forward"))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, "Channel"), /* @__PURE__ */ createElement("input", { placeholder: "my-channel", "bind:value": messageChannel }))), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, "Message"), /* @__PURE__ */ createElement("input", { placeholder: "Hello, world!", "bind:value": messageBody }))), /* @__PURE__ */ createElement("div", { class: "flex-row width-input justify-end" }, /* @__PURE__ */ createElement("button", { class: "primary width-50", "on:click": sendMessage }, "Send", /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_forward"))))) + ); })(); diff --git a/static/src/index.tsx b/static/src/index.tsx index 9cb40d2..d525990 100644 --- a/static/src/index.tsx +++ b/static/src/index.tsx @@ -1,8 +1,95 @@ import * as React from "bloatless-react"; +// WS const ws = new WebSocket(`ws://${window.location.host}`); -ws.onopen = () => ws.send("Hello!") -ws.onmessage = (data) => console.log(data); +ws.addEventListener("message", (message) => { + receivedMessage.value = message.data; +}); -document.querySelector("main")!.append(

Hello there!

); +function send(object: any) { + const stringified = JSON.stringify(object); + ws.send(stringified); +} + +// STATE +const receivedMessage = new React.State(""); + +const subscriptionChannel = new React.State(""); +const messageChannel = new React.State(""); +const messageBody = new React.State(""); + +function subscribe() { + send({ subscribeChannel: subscriptionChannel.value }); +} + +function unsubscribe() { + send({ unsubscribeChannel: subscriptionChannel.value }); +} + +function sendMessage() { + send({ + messageChannel: messageChannel.value, + messageBody: messageBody.value, + }); +} + +// UI +document.querySelector("main")!.append( +
+
UDN Test
+
+
+
+ Received message +

+
+
+ +
+ + + +
+ + +
+ +
+ + + + + +
+ +
+
+
+); diff --git a/websocket-handler.ts b/websocket-handler.ts index 020211f..5408cb7 100644 --- a/websocket-handler.ts +++ b/websocket-handler.ts @@ -1,42 +1,77 @@ // TYPES -// requests -interface IncomingMessage { - //sent on new connections - connectionType?: ConnectionType; +import { SubscriptionMap } from "./auxilary"; +import { WS } from "./index"; + +// requests +export interface IncomingMessage { // subscribing to channel - channelSubscription?: string; + subscribeChannel?: string; + unsubscribeChannel?: string; //sending message messageChannel?: string; messageBody?: string; } -// connections -enum ConnectionType { - Server = "server", - Client = "client", -} - // TRACKING -const serversOnEntireNetwork = new Set(); +const subscriptions = new SubscriptionMap(); -const socketsPerChannel = new Map>(); -const channelsPerSocket = new Map>(); - -export function trackServer(ws: WebSocket): void { - serversOnEntireNetwork.add(ws); +export function trackSubscription(ws: WS, channel: string): void { + subscriptions.set(ws, channel); + confirmSubscription(ws, channel); } -export function trackSubscription(channel: string, ws: WebSocket): void { - if (!socketsPerChannel.has(channel)) socketsPerChannel.set(channel, new Set()); - if (!channelsPerSocket.has(ws)) channelsPerSocket.set(ws, new Set()); - - socketsPerChannel.get(channel)!.add(ws); - channelsPerSocket.get(ws)!.add(channel); +export function removeSubscription(ws: WS, channel: string): void { + subscriptions.delete(ws, channel); + confirmSubscription(ws, channel); } -export function forgetConnection(ws: WebSocket): void { - serversOnEntireNetwork.delete(ws); - serversOnEntireNetwork.delete(ws); +export function forgetConnection(ws: WS): void { + const channels = subscriptions.getChannelList(ws); + channels?.forEach((channel) => { + subscriptions.delete(ws, channel); + }); +} + +// PROCESS MESSAGE +export function processMessage(ws: WS, messageString: string): void { + const messageObject: IncomingMessage = parseMessage(messageString); + + if (typeof messageObject.subscribeChannel == "string") { + trackSubscription(ws, messageObject.subscribeChannel); + } else if (typeof messageObject.unsubscribeChannel == "string") { + removeSubscription(ws, messageObject.unsubscribeChannel); + } else if ( + typeof messageObject.messageChannel == "string" && + typeof messageObject.messageBody == "string" + ) { + sendMessage(messageObject.messageChannel, messageObject.messageBody); + } +} + +// MESSAGING +export function stringifyMessage(messageObject: Object): string { + return JSON.stringify(messageObject); +} + +export function parseMessage(messageString: string): Object { + return JSON.parse(messageString); +} + +export function sendMessage(channel: string, body: string): void { + const messageObject = { channel, body }; + const messageString = stringifyMessage(messageObject); + + const subscribers = subscriptions.getClientList(channel); + subscribers?.forEach((ws) => ws.send(messageString)); +} + +function confirmSubscription(ws: WS, channel: string): void { + const messageObject = { + channel: channel, + subscribed: subscriptions.getChannelList(ws)?.has(channel), + }; + const messageString = stringifyMessage(messageObject); + ws.send(messageString); }