add channel communication
This commit is contained in:
+24
-22
@@ -1,38 +1,40 @@
|
||||
export class TwoWayMap<A, B> {
|
||||
private mapAtoB = new Map<A, Set<B>>();
|
||||
private mapBtoA = new Map<B, Set<A>>();
|
||||
import { WS } from "./index";
|
||||
|
||||
export class SubscriptionMap {
|
||||
private channelsPerClient = new Map<WS, Set<string>>();
|
||||
private clientsPerChannel = new Map<string, Set<WS>>();
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<unknown>;
|
||||
|
||||
// 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) {},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Vendored
+54
-3
@@ -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")))))
|
||||
);
|
||||
})();
|
||||
|
||||
+90
-3
@@ -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(<h1>Hello there!</h1>);
|
||||
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(
|
||||
<article>
|
||||
<header>UDN Test</header>
|
||||
<div>
|
||||
<div class="tile">
|
||||
<div>
|
||||
<b>Received message</b>
|
||||
<p class="secondary" subscribe:innerText={receivedMessage}></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr></hr>
|
||||
|
||||
<label class="tile">
|
||||
<div>
|
||||
<span>Subscribe to channel</span>
|
||||
<input
|
||||
placeholder="my-channel"
|
||||
bind:value={subscriptionChannel}
|
||||
></input>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div class="flex-row width-input justify-end">
|
||||
<button class="danger width-50" on:click={unsubscribe}>
|
||||
Unsubscribe
|
||||
</button>
|
||||
<button class="primary width-50" on:click={subscribe}>
|
||||
Subscribe
|
||||
<span class="icon">arrow_forward</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr></hr>
|
||||
|
||||
<label class="tile">
|
||||
<div>
|
||||
<span>Channel</span>
|
||||
<input placeholder="my-channel" bind:value={messageChannel}></input>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="tile">
|
||||
<div>
|
||||
<span>Message</span>
|
||||
<input placeholder="Hello, world!" bind:value={messageBody}></input>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div class="flex-row width-input justify-end">
|
||||
<button class="primary width-50" on:click={sendMessage}>
|
||||
Send
|
||||
<span class="icon">arrow_forward</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
|
||||
+61
-26
@@ -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<WebSocket>();
|
||||
const subscriptions = new SubscriptionMap();
|
||||
|
||||
const socketsPerChannel = new Map<string, Set<WebSocket>>();
|
||||
const channelsPerSocket = new Map<WebSocket, Set<string>>();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user