change mailbox behaviour to use reception confirmations

This commit is contained in:
marlon-erler
2026-03-31 22:02:13 +00:00
parent 959dc4d288
commit ec020e6358
21 changed files with 2772 additions and 2476 deletions
+38 -38
View File
@@ -5,22 +5,22 @@ The UDN project aims to provide decentralized and localhost-able infrastructure
# Contents
- [How it works](#how-it-works)
- [Channels](#channels)
- [Scalability](#scalability)
- [Example](#example)
- [Channels](#channels)
- [Scalability](#scalability)
- [Example](#example)
- [User's guide](#users-guide)
- [Getting Connected](#getting-connected)
- [Using Apps](#using-apps)
- [Getting Apps](#getting-apps)
- [Getting Connected](#getting-connected)
- [Using Apps](#using-apps)
- [Getting Apps](#getting-apps)
- [Administrator's Guide](#administrators-guide)
- [Getting Started](#getting-started)
- [Configuration](#configuration)
- [TLS](#tls)
- [Connecting Multiple Servers](#connecting-multiple-servers)
- [Getting Started](#getting-started)
- [Configuration](#configuration)
- [TLS](#tls)
- [Connecting Multiple Servers](#connecting-multiple-servers)
- [App Developer's Guide](#app-developers-guide)
- [Guidelines](#guidelines)
- [The Recommended Way](#the-recommended-way)
- [The Manual Way](#the-manual-way)
- [Guidelines](#guidelines)
- [The Recommended Way](#the-recommended-way)
- [The Manual Way](#the-manual-way)
# How it works
@@ -40,14 +40,14 @@ Subscriptions and messages are handled by a server:
```md
**Josh** --(subscribe to "my-channel")-> **Server**
**Josh** <-(subscription confirmed )-- **Server**
**Josh** <-(subscription confirmed )-- **Server**
**Bob** --(subscribe to "my-channel")-> **Server**
**Bob** <-(subscription confirmed )-- **Server**
**Bob** --(subscribe to "my-channel")-> **Server**
**Bob** <-(subscription confirmed )-- **Server**
**Josh** --("hello" on "my-channel" )-> **Server**
**Josh** <-("hello" on "my-channel" )-- **Server**
**Bob** <-("hello" on "my-channel" )-- **Server**
**Josh** --("hello" on "my-channel" )-> **Server**
**Josh** <-("hello" on "my-channel" )-- **Server**
**Bob** <-("hello" on "my-channel" )-- **Server**
```
## Scalability
@@ -79,36 +79,36 @@ Then, John will send his messages on both "s1-and-s2" and "car-warranty". As alw
# John and Bob subscribe:
**John** --(subscribe to "car-warranty")-> **S1**
**John** <-(subscription confirmed )-- **S1**
**John** <-(subscription confirmed )-- **S1**
**Bob** --(subscribe to "car-warranty")-> **S2**
**Bob** <-(subscription confirmed )-- **S2**
**Bob** --(subscribe to "car-warranty")-> **S2**
**Bob** <-(subscription confirmed )-- **S2**
---
# The servers subscribe:
**S1** --(subscribe to "s1-and-s2" )-> **S2**
**S2** --(subscribe to "s1-and-s2" )-> **S1**
**S1** --(subscribe to "s1-and-s2" )-> **S2**
**S2** --(subscribe to "s1-and-s2" )-> **S1**
---
# Sending the Message:
John sends the message to his server
**John** --("hello" on: )-> **S1**
( - "s1-and-s2" )
( - "car-warranty" )
**John** --("hello" on: )-> **S1**
( - "s1-and-s2" )
( - "car-warranty" )
S2 subscribed to "s1-and-s2", so S1 sends this message to S2
**S1** --("hello" on: )-> **S2**
( - "s1-and-s2" )
( - "car-warranty" )
**S1** --("hello" on: )-> **S2**
( - "s1-and-s2" )
( - "car-warranty" )
Bob subscribed to "car-warranty", so S2 sends this message to Bob
**S2** --("hello" on: )-> **Bob**
( - "s1-and-s2" )
( - "car-warranty" )
**S2** --("hello" on: )-> **Bob**
( - "s1-and-s2" )
( - "car-warranty" )
```
This requires John to know of "s1-and-s2" and, obviously, Bob to subscribe to "car-warranty". Jane and Alice could agree on their own primary channel and use "s1-and-s2" just like John did.
@@ -139,16 +139,16 @@ As you can see, there are many ways to connect multiple servers and communicate
- `http://` or `https://` (the secure variant) are protocols to request data and receive a single response
- To send messages, apps use so-called WebSockets instead. Their protocol is `ws://` or `wss://` (the secure variant)
- You will need to **replace `http` with `ws`**:
- `http://192.168.0.100:3000` -> `ws://192.168.0.100:3000`
- `https://192.168.0.200:3000` -> `wss://192.168.0.200:3000`
- `http://192.168.0.100:3000` -> `ws://192.168.0.100:3000`
- `https://192.168.0.200:3000` -> `wss://192.168.0.200:3000`
## Getting Apps
Anyone can develop apps for the UDN project. Examples of such apps are listed below. If you have developed an app for the UDN project and you want your app to be listed here, create an issue.
| App Name | Description | Links |
| -- | -- | -- |
| Comms | Messenger for UDN | [more info](https://github.com/marlon-erler/udn-comms/) - [install](https://udn-comms.onrender.com/)
| App Name | Description | Links |
| -------- | ----------------- | ---------------------------------------------------------------------------------------------------- |
| Comms | Messenger for UDN | [more info](https://github.com/marlon-erler/udn-comms/) - [install](https://udn-comms.onrender.com/) |
---
+17 -17
View File
@@ -2,30 +2,30 @@
const configPath = "config.json";
export const defaultConfig = {
port: 3000,
connectedServers: [],
subscribedChannels: [],
port: 3000,
connectedServers: [],
subscribedChannels: [],
};
// METHODS
export async function createConfig() {
const configString = JSON.stringify(defaultConfig, null, 4);
await Bun.write(configPath, configString);
const configString = JSON.stringify(defaultConfig, null, 4);
await Bun.write(configPath, configString);
}
export async function getConfig(): Promise<typeof defaultConfig> {
try {
const configString = await Bun.file(configPath).text();
const configObject = JSON.parse(configString);
try {
const configString = await Bun.file(configPath).text();
const configObject = JSON.parse(configString);
// validate config file
Object.keys(defaultConfig).forEach((key) => {
if (!configObject[key]) throw null;
});
// validate config file
Object.keys(defaultConfig).forEach((key) => {
if (!configObject[key]) throw null;
});
return configObject;
} catch {
await createConfig();
return defaultConfig;
}
return configObject;
} catch {
await createConfig();
return defaultConfig;
}
}
+116 -115
View File
@@ -1,21 +1,21 @@
#!/usr/bin/env bun
import {
connectServers,
forgetConnection,
getWebSocketStats,
processMessage,
reconnectServers,
removeExpiredMailboxes,
trackConnection,
connectServers,
forgetConnection,
getWebSocketStats,
processMessage,
reconnectServers,
removeExpiredMailboxes,
trackConnection,
} from "./websocket-handler";
import {
createFileResponse,
getVersionNumber,
writeError,
writeStatNumber,
writeStatString,
writeSuccess,
createFileResponse,
getVersionNumber,
writeError,
writeStatNumber,
writeStatString,
writeSuccess,
} from "./utility";
import Colors from "colors";
@@ -35,120 +35,121 @@ export type WS = ServerWebSocket<unknown> | WebSocket;
// MAIN
async function main() {
// CONFIG
const config = await getConfig();
const versionNumber = await getVersionNumber();
// CONFIG
const config = await getConfig();
const versionNumber = await getVersionNumber();
// SERVER
const server = Bun.serve({
port: config.port,
// SERVER
const server = Bun.serve({
port: config.port,
fetch(request, server) {
// allow websockets
server.upgrade(request);
fetch(request, server) {
// allow websockets
server.upgrade(request);
// respond
const requestPath = new URL(request.url).pathname;
switch (requestPath) {
case "/":
return createFileResponse(DEFAULT_PAGE);
case "/stats":
return new Response(JSON.stringify(getAllStats()));
default:
return createFileResponse(requestPath);
}
},
// respond
const requestPath = new URL(request.url).pathname;
switch (requestPath) {
case "/":
return createFileResponse(DEFAULT_PAGE);
case "/stats":
return new Response(JSON.stringify(getAllStats()));
default:
return createFileResponse(requestPath);
}
},
websocket: {
open(ws) {
try {
trackConnection(ws);
} catch (error) {
writeError(error);
}
},
message(ws, message) {
try {
processMessage(ws, message.toString(), config);
} catch (error) {
writeError(error);
}
},
close(ws) {
try {
forgetConnection(ws);
} catch (error) {
writeError(error);
}
},
},
websocket: {
open(ws) {
try {
trackConnection(ws);
} catch (error) {
writeError(error);
}
},
message(ws, message) {
try {
processMessage(ws, message.toString(), config);
} catch (error) {
writeError(error);
}
},
close(ws) {
try {
forgetConnection(ws);
} catch (error) {
writeError(error);
}
},
},
tls: await (async () => {
const keyFile = Bun.file("./key.pem");
const certFile = Bun.file("./cert.pem");
tls: await (async () => {
const keyFile = Bun.file("./key.pem");
const certFile = Bun.file("./cert.pem");
if (
(await keyFile.exists()) == false ||
(await certFile.exists()) == false
)
return undefined;
if (
(await keyFile.exists()) == false ||
(await certFile.exists()) == false
)
return undefined;
return {
key: keyFile,
cert: certFile,
passphrase: process.env.TLS_PASSPHRASE,
};
})(),
});
return {
key: keyFile,
cert: certFile,
passphrase: process.env.TLS_PASSPHRASE,
};
})(),
});
// OTHER SERVERS
connectServers(config);
// OTHER SERVERS
connectServers(config);
// AUDIT
function getServerStats(): [string, string][] {
return [
["server url", server.url.toString()],
["server ip address", IP.address()],
["server version", versionNumber],
];
}
function getAllStats(): [string, string | number][] {
return [...getServerStats(), ...getWebSocketStats()];
}
// CLI
function updateCLI() {
console.clear();
console.log(Colors.bold.bgWhite("UNIVERSAL DECENTRALIZED NETWORK"));
console.log(
Colors.yellow("relevant output is available in the 'logs' file")
);
console.log();
console.log(new Date().toLocaleString());
console.log();
getServerStats().forEach((entry) => writeStatString(...entry));
console.log();
getWebSocketStats().forEach((entry) => writeStatNumber(...entry));
console.log();
}
// LOOP
setInterval(() => {
if (process.env.DEV != "1") {
updateCLI();
reconnectServers(config);
// AUDIT
function getServerStats(): [string, string][] {
return [
["server url", server.url.toString()],
["server ip address", IP.address()],
["server version", versionNumber],
];
}
removeExpiredMailboxes();
}, 2000);
function getAllStats(): [string, string | number][] {
return [...getServerStats(), ...getWebSocketStats()];
}
// FINISH
writeSuccess(`###\nstarted up ${new Date().toLocaleString()}`);
// CLI
function updateCLI() {
return;
console.clear();
console.log(Colors.bold.bgWhite("UNIVERSAL DECENTRALIZED NETWORK"));
console.log(
Colors.yellow("relevant output is available in the 'logs' file"),
);
console.log();
console.log(new Date().toLocaleString());
console.log();
getServerStats().forEach((entry) => writeStatString(...entry));
console.log();
getWebSocketStats().forEach((entry) => writeStatNumber(...entry));
console.log();
}
// LOOP
setInterval(() => {
if (process.env.DEV != "1") {
updateCLI();
reconnectServers(config);
}
removeExpiredMailboxes();
}, 2000);
// FINISH
writeSuccess(`###\nstarted up ${new Date().toLocaleString()}`);
}
main();
+748 -748
View File
File diff suppressed because it is too large Load Diff
+23 -23
View File
@@ -1,25 +1,25 @@
{
"name": "udn",
"version": "1.0.0-beta14",
"description": "A websocket-based infrastructure for decentralized networks ",
"bin": {
"start": "index.ts"
},
"repository": {
"type": "git",
"url": "git+https://github.com/marlon-erler/universal-decentralized-network.git"
},
"author": "marlon-erler",
"license": "SEE LICENSE IN LICENSE",
"bugs": {
"url": "https://github.com/marlon-erler/universal-decentralized-network/issues"
},
"homepage": "https://github.com/marlon-erler/universal-decentralized-network#readme",
"dependencies": {
"@types/bun": "^1.1.6",
"@types/ip": "^1.1.3",
"colors": "^1.4.0",
"express": "^4.19.2",
"ip": "^2.0.1"
}
"name": "udn",
"version": "1.0.0-beta15",
"description": "A websocket-based infrastructure for decentralized networks ",
"bin": {
"start": "index.ts"
},
"repository": {
"type": "git",
"url": "git+https://github.com/marlon-erler/universal-decentralized-network.git"
},
"author": "marlon-erler",
"license": "SEE LICENSE IN LICENSE",
"bugs": {
"url": "https://github.com/marlon-erler/universal-decentralized-network/issues"
},
"homepage": "https://github.com/marlon-erler/universal-decentralized-network#readme",
"dependencies": {
"@types/bun": "^1.1.6",
"@types/ip": "^1.1.3",
"colors": "^1.4.0",
"express": "^4.19.2",
"ip": "^2.0.1"
}
}
+27 -27
View File
@@ -1,34 +1,34 @@
<html lang="en">
<head>
<title>UDN Server</title>
<head>
<title>UDN Server</title>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no"
/>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no"
/>
<meta
name="theme-color"
content="hsla(0, 0%, 100%, 100%)"
media="(prefers-color-scheme: light)"
/>
<meta
name="theme-color"
content="black"
media="(prefers-color-scheme: dark)"
/>
<meta
name="theme-color"
content="hsla(0, 0%, 100%, 100%)"
media="(prefers-color-scheme: light)"
/>
<meta
name="theme-color"
content="black"
media="(prefers-color-scheme: dark)"
/>
<link rel="stylesheet" href="/styles.css" />
<link rel="stylesheet" href="/index.css" />
<link rel="manifest" href="/manifest.json" />
<link rel="icon" href="/icon.png" />
<script src="/support.js" defer></script>
<link rel="stylesheet" href="/styles.css" />
<link rel="stylesheet" href="/index.css" />
<link rel="manifest" href="/manifest.json" />
<link rel="icon" href="/icon.png" />
<script src="/support.js" defer></script>
<script src="/index.js" defer></script>
</head>
<script src="/index.js" defer></script>
</head>
<body>
<main></main>
</body>
<body>
<main></main>
</body>
</html>
+797 -547
View File
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -1,10 +1,10 @@
{
"name": "UDN Server",
"display": "standalone",
"icons": [
{
"src": "icon.png",
"sizes": "any"
}
]
"name": "UDN Server",
"display": "standalone",
"icons": [
{
"src": "icon.png",
"sizes": "any"
}
]
}
+401 -396
View File
File diff suppressed because it is too large Load Diff
+22 -19
View File
@@ -1,31 +1,34 @@
// TABS
document.querySelector("main").addEventListener("scroll", () => {
const links = document.querySelectorAll(".tab-link");
links.forEach((element) => element.removeAttribute("active"));
const links = document.querySelectorAll(".tab-link");
links.forEach((element) => element.removeAttribute("active"));
const pageWidth = document.querySelector("main > article").offsetWidth;
const scroll = document.querySelector("main").scrollLeft;
const currentPage = Math.round(scroll / pageWidth);
const pageWidth = document.querySelector("main > article").offsetWidth;
const scroll = document.querySelector("main").scrollLeft;
const currentPage = Math.round(scroll / pageWidth);
if (links[currentPage]) links[currentPage].setAttribute("active", "");
if (links[currentPage]) links[currentPage].setAttribute("active", "");
});
// SERVICE WORKER
async function registerServiceWorker() {
if ("serviceWorker" in navigator) {
try {
const registration = await navigator.serviceWorker.register("/sw.js", {
scope: "/",
});
if (registration.installing) {
setTimeout(() => window.location.reload(), 500);
} else if (registration.active) {
console.log("Service worker active");
}
} catch (error) {
console.error(`Could not install service worker: ${error}`);
if ("serviceWorker" in navigator) {
try {
const registration = await navigator.serviceWorker.register(
"/sw.js",
{
scope: "/",
},
);
if (registration.installing) {
setTimeout(() => window.location.reload(), 500);
} else if (registration.active) {
console.log("Service worker active");
}
} catch (error) {
console.error(`Could not install service worker: ${error}`);
}
}
}
}
registerServiceWorker();
+17 -17
View File
@@ -1,38 +1,38 @@
const CACHE_VERSION = "v1";
async function handleRequest(event) {
const responsePromise = getResponse(event);
event.respondWith(responsePromise);
const responsePromise = getResponse(event);
event.respondWith(responsePromise);
}
async function getResponse(event) {
try {
return await fetchAndCache(event);
} catch {
return await getFromCache(event);
}
try {
return await fetchAndCache(event);
} catch {
return await getFromCache(event);
}
}
async function fetchAndCache(event) {
const request = event.request;
const request = event.request;
const response = await fetch(request);
const response = await fetch(request);
const cache = await caches.open(CACHE_VERSION);
await cache.put(request, response.clone());
const cache = await caches.open(CACHE_VERSION);
await cache.put(request, response.clone());
return response;
return response;
}
async function getFromCache(event) {
const request = event.request;
const request = event.request;
const cache = await caches.open(CACHE_VERSION);
const response = await cache.match(request);
const cache = await caches.open(CACHE_VERSION);
const response = await cache.match(request);
return response;
return response;
}
self.addEventListener("fetch", (event) => {
handleRequest(event);
handleRequest(event);
});
+8 -8
View File
@@ -1,10 +1,10 @@
{
"dependencies": {
"bloatless-react": "^1.2.6",
"esbuild": "latest",
"udn-frontend": "^1.0.6"
},
"scripts": {
"build": "esbuild src/index.tsx --bundle --outdir=dist"
}
"dependencies": {
"bloatless-react": "^1.2.6",
"esbuild": "latest",
"udn-frontend": "^1.0.6"
},
"scripts": {
"build": "esbuild src/index.tsx --bundle --outdir=dist"
}
}
+31 -31
View File
@@ -7,41 +7,41 @@ import { isDisconnected } from "./model";
import { translations } from "./translations";
document.body.prepend(
<menu>
<a class="tab-link" href="#info-screen" active>
<span class="icon">info</span>
{translations.info}
</a>
<a class="tab-link" href="#main-screen">
<span class="icon">settings</span>
{translations.settings}
</a>
<a class="tab-link" href="#message-screen">
<span class="icon">chat</span>
{translations.messages}
</a>
</menu>
<menu>
<a class="tab-link" href="#info-screen" active>
<span class="icon">info</span>
{translations.info}
</a>
<a class="tab-link" href="#main-screen">
<span class="icon">settings</span>
{translations.settings}
</a>
<a class="tab-link" href="#message-screen">
<span class="icon">chat</span>
{translations.messages}
</a>
</menu>,
);
document
.querySelector("main")!
.append(InfoScreen(), MainScreen(), MessageScreen());
.querySelector("main")!
.append(InfoScreen(), MainScreen(), MessageScreen());
document.body.append(
<div class="modal" toggle:open={isDisconnected}>
<div>
<main>
<div
class="flex-column align-center justify-center width-100 height-100"
style="gap: 1rem"
>
<span class="icon error" style="font-size: 3rem">
signal_disconnected
</span>
<h1 class="error">{translations.disconnected}</h1>
<p class="secondary">{translations.reconnecting}</p>
<div class="modal" toggle:open={isDisconnected}>
<div>
<main>
<div
class="flex-column align-center justify-center width-100 height-100"
style="gap: 1rem"
>
<span class="icon error" style="font-size: 3rem">
signal_disconnected
</span>
<h1 class="error">{translations.disconnected}</h1>
<p class="secondary">{translations.reconnecting}</p>
</div>
</main>
</div>
</main>
</div>
</div>
</div>,
);
+18 -15
View File
@@ -5,19 +5,22 @@ import { statHTMLString, updateStats } from "./model";
import { translations } from "./translations";
export function InfoScreen() {
return (
<article id="info-screen">
<header>
{translations.serverInfo}
<span>
<button on:click={updateStats}>
<span class="icon">refresh</span>
</button>
</span>
</header>
<div>
<div class="flex-column gap" subscribe:innerHTML={statHTMLString}></div>
</div>
</article>
);
return (
<article id="info-screen">
<header>
{translations.serverInfo}
<span>
<button on:click={updateStats}>
<span class="icon">refresh</span>
</button>
</span>
</header>
<div>
<div
class="flex-column gap"
subscribe:innerHTML={statHTMLString}
></div>
</div>
</article>
);
}
+109 -103
View File
@@ -1,122 +1,128 @@
import * as React from "bloatless-react";
import {
deleteMailbox,
isDisconnected,
isMailboxDisonnected,
isMessageEmpty,
lastReceivedMessage,
newMessageBody,
newMessageChannel,
requestMailbox,
sendMessage,
subscribe,
subscriptionChannel,
unsubscribe,
deleteMailbox,
isDisconnected,
isMailboxDisonnected,
isMessageEmpty,
lastReceivedMessage,
newMessageBody,
newMessageChannel,
requestMailbox,
sendMessage,
subscribe,
subscriptionChannel,
unsubscribe,
} from "./model";
import { translations } from "./translations";
export function MainScreen() {
// UI
return (
<article id="main-screen">
<header>{translations.settings}</header>
<div>
<div class="tile width-input">
<div>
<b>{translations.messageLastReceived}</b>
<p class="secondary" subscribe:innerText={lastReceivedMessage}></p>
</div>
</div>
// UI
return (
<article id="main-screen">
<header>{translations.settings}</header>
<div>
<div class="tile width-input">
<div>
<b>{translations.messageLastReceived}</b>
<p
class="secondary"
subscribe:innerText={lastReceivedMessage}
></p>
</div>
</div>
<hr></hr>
<hr></hr>
<div class="tile width-input">
<div>
<b>{translations.mailbox}</b>
<p class="secondary" toggle:hidden={isMailboxDisonnected}>
{translations.mailboxConnected}
</p>
</div>
</div>
<div class="tile width-input">
<div>
<b>{translations.mailbox}</b>
<p
class="secondary"
toggle:hidden={isMailboxDisonnected}
>
{translations.mailboxConnected}
</p>
</div>
</div>
<div class="flex-row width-input justify-end">
<button
class="danger width-50"
on:click={deleteMailbox}
toggle:disabled={isMailboxDisonnected}
>
{translations.deleteMailbox}
</button>
<button
class="primary width-50"
on:click={requestMailbox}
toggle:disabled={isDisconnected}
>
{translations.requestMailbox}
<span class="icon">inbox</span>
</button>
</div>
<div class="flex-row width-input justify-end">
<button
class="danger width-50"
on:click={deleteMailbox}
toggle:disabled={isMailboxDisonnected}
>
{translations.deleteMailbox}
</button>
<button
class="primary width-50"
on:click={requestMailbox}
toggle:disabled={isDisconnected}
>
{translations.requestMailbox}
<span class="icon">inbox</span>
</button>
</div>
<hr></hr>
<hr></hr>
<label class="tile">
<div>
<span>{translations.subscribeToChannel}</span>
<input
placeholder={translations.channel_placeholder}
bind:value={subscriptionChannel}
on:enter={subscribe}
></input>
</div>
</label>
<label class="tile">
<div>
<span>{translations.subscribeToChannel}</span>
<input
placeholder={translations.channel_placeholder}
bind:value={subscriptionChannel}
on:enter={subscribe}
></input>
</div>
</label>
<div class="flex-row width-input justify-end">
<button class="danger width-50" on:click={unsubscribe}>
{translations.unsubscribe}
</button>
<button class="primary width-50" on:click={subscribe}>
{translations.subscribe}
<span class="icon">arrow_forward</span>
</button>
</div>
<div class="flex-row width-input justify-end">
<button class="danger width-50" on:click={unsubscribe}>
{translations.unsubscribe}
</button>
<button class="primary width-50" on:click={subscribe}>
{translations.subscribe}
<span class="icon">arrow_forward</span>
</button>
</div>
<hr></hr>
<hr></hr>
<label class="tile">
<div>
<span>{translations.channel}</span>
<input
placeholder={translations.channel_placeholder}
bind:value={newMessageChannel}
on:enter={sendMessage}
></input>
</div>
</label>
<label class="tile">
<div>
<span>{translations.channel}</span>
<input
placeholder={translations.channel_placeholder}
bind:value={newMessageChannel}
on:enter={sendMessage}
></input>
</div>
</label>
<label class="tile">
<div>
<span>{translations.message}</span>
<input
placeholder={translations.message_placeholder}
bind:value={newMessageBody}
on:enter={sendMessage}
></input>
</div>
</label>
<label class="tile">
<div>
<span>{translations.message}</span>
<input
placeholder={translations.message_placeholder}
bind:value={newMessageBody}
on:enter={sendMessage}
></input>
</div>
</label>
<div class="flex-row width-input justify-end">
<button
class="primary width-50"
on:click={sendMessage}
toggle:disabled={isMessageEmpty}
>
{translations.send}
<span class="icon">send</span>
</button>
</div>
</div>
</article>
);
<div class="flex-row width-input justify-end">
<button
class="primary width-50"
on:click={sendMessage}
toggle:disabled={isMessageEmpty}
>
{translations.send}
<span class="icon">send</span>
</button>
</div>
</div>
</article>
);
}
+39 -39
View File
@@ -1,51 +1,51 @@
import * as React from "bloatless-react";
import {
Message,
isMessageEmpty,
messages,
newMessageBody,
sendMessage,
Message,
isMessageEmpty,
messages,
newMessageBody,
sendMessage,
} from "./model";
import { translations } from "./translations";
const convertMessageToElement: React.ListItemConverter<Message> = (message) => {
return (
<div class="tile" style="flex: 0">
<div>
<span class="secondary">{message.channel}</span>
<b>{message.body}</b>
</div>
</div>
);
return (
<div class="tile" style="flex: 0">
<div>
<span class="secondary">{message.channel}</span>
<b>{message.body}</b>
</div>
</div>
);
};
export function MessageScreen() {
return (
<article id="message-screen">
<header>{translations.messagesReceived}</header>
<div>
<div
class="flex-column gap"
subscribe:children={[messages, convertMessageToElement]}
></div>
</div>
<footer>
<input
style="max-width: unset"
placeholder={translations.message_placeholder_generic}
bind:value={newMessageBody}
on:enter={sendMessage}
></input>
<button
class="primary"
on:click={sendMessage}
toggle:disabled={isMessageEmpty}
>
<span class="icon">send</span>
</button>
</footer>
</article>
);
return (
<article id="message-screen">
<header>{translations.messagesReceived}</header>
<div>
<div
class="flex-column gap"
subscribe:children={[messages, convertMessageToElement]}
></div>
</div>
<footer>
<input
style="max-width: unset"
placeholder={translations.message_placeholder_generic}
bind:value={newMessageBody}
on:enter={sendMessage}
></input>
<button
class="primary"
on:click={sendMessage}
toggle:disabled={isMessageEmpty}
>
<span class="icon">send</span>
</button>
</footer>
</article>
);
}
+55 -50
View File
@@ -4,7 +4,10 @@ import UDNFrontend from "udn-frontend";
import { translations } from "./translations";
export class Message {
constructor(public channel: string, public body: string) {}
constructor(
public channel: string,
public body: string,
) {}
}
// STATE
@@ -18,28 +21,30 @@ export const isMailboxDisonnected = new React.State(true);
// messages
export const messages = new React.ListState<Message>();
export const lastReceivedMessage = new React.State(
translations.noMessagesReceived
translations.noMessagesReceived,
);
// stats
export const statHTMLString = new React.State<string>("");
export async function updateStats() {
const response = await fetch(
`${window.location.protocol}//${window.location.host}/stats`
);
const data: [string, string][] = await response.json();
let html = "";
data.forEach((item) => {
const tile = (
<div class="tile">
<span class="secondary flex width-100">{item[0].toString()}</span>
<b>{item[1].toString()}</b>
</div>
const response = await fetch(
`${window.location.protocol}//${window.location.host}/stats`,
);
html += tile.outerHTML;
});
statHTMLString.value = html;
const data: [string, string][] = await response.json();
let html = "";
data.forEach((item) => {
const tile = (
<div class="tile">
<span class="secondary flex width-100">
{item[0].toString()}
</span>
<b>{item[1].toString()}</b>
</div>
);
html += tile.outerHTML;
});
statHTMLString.value = html;
}
// channels
@@ -49,79 +54,79 @@ export const newMessageChannel = new React.State("");
export const newMessageBody = new React.State("");
export const isMessageEmpty = React.createProxyState(
[newMessageChannel, newMessageBody],
() => newMessageChannel.value == "" || newMessageBody.value == ""
[newMessageChannel, newMessageBody],
() => newMessageChannel.value == "" || newMessageBody.value == "",
);
// UDN Frontend
const UDN = new UDNFrontend();
UDN.onconnect = () => {
isDisconnected.value = false;
updateStats();
isDisconnected.value = false;
updateStats();
if (mailboxId.value != "") {
UDN.connectMailbox(mailboxId.value);
}
if (mailboxId.value != "") {
UDN.connectMailbox(mailboxId.value);
}
};
UDN.onmessage = (data) => {
lastReceivedMessage.value = JSON.stringify(data, null, 4);
lastReceivedMessage.value = JSON.stringify(data, null, 4);
const { messageChannel, messageBody } = data;
if (messageChannel && messageBody) {
const messageObject = new Message(messageChannel, messageBody);
if (messageBody) messages.add(messageObject);
}
const { messageChannel, messageBody } = data;
if (messageChannel && messageBody) {
const messageObject = new Message(messageChannel, messageBody);
if (messageBody) messages.add(messageObject);
}
};
UDN.onmailboxcreate = (id) => {
console.log(id);
mailboxId.value = id;
UDN.connectMailbox(id);
console.log(id);
mailboxId.value = id;
UDN.connectMailbox(id);
};
UDN.onmailboxconnect = () => {
isMailboxDisonnected.value = false;
isMailboxDisonnected.value = false;
};
UDN.onmailboxdelete = () => {
isMailboxDisonnected.value = true;
}
isMailboxDisonnected.value = true;
};
UDN.ondisconnect = () => {
isDisconnected.value = true;
isMailboxDisonnected.value = true;
setTimeout(connect, 1000);
isDisconnected.value = true;
isMailboxDisonnected.value = true;
setTimeout(connect, 1000);
};
function connect() {
UDN.connect(
`${window.location.protocol.replace("http", "ws")}//${window.location.host}`
);
UDN.connect(
`${window.location.protocol.replace("http", "ws")}//${window.location.host}`,
);
}
connect();
// METHODS
export function subscribe() {
UDN.subscribe(subscriptionChannel.value);
UDN.subscribe(subscriptionChannel.value);
}
export function unsubscribe() {
UDN.unsubscribe(subscriptionChannel.value);
UDN.unsubscribe(subscriptionChannel.value);
}
export function sendMessage() {
if (isMessageEmpty.value == true) return;
UDN.sendMessage(newMessageChannel.value, newMessageBody.value);
newMessageBody.value = "";
if (isMessageEmpty.value == true) return;
UDN.sendMessage(newMessageChannel.value, newMessageBody.value);
newMessageBody.value = "";
}
export function requestMailbox() {
UDN.requestMailbox();
UDN.requestMailbox();
}
export function deleteMailbox() {
console.log("test");
UDN.deleteMailbox(mailboxId.value);
console.log("test");
UDN.deleteMailbox(mailboxId.value);
}
+71 -71
View File
@@ -1,79 +1,79 @@
// STATIC
const staticTextEnglish = {
mailbox: "Mailbox",
requestMailbox: "Request",
deleteMailbox: "Delete",
mailboxConnected: "Mailbox active",
channel: "Channel",
channel_placeholder: "my-channel",
disconnected: "Server disconnected",
reconnecting: "Reconnecting...",
info: "Info",
message: "Message",
messages: "Messages",
message_placeholder: "Hello, world!",
message_placeholder_generic: "Type a message...",
noMessagesReceived: "No messages received",
messageLastReceived: "Last received message",
messagesReceived: "Received Messages",
send: "Send",
serverInfo: "Server Info",
settings: "Settings",
subscribeToChannel: "Subscribe to channel",
subscribe: "Subscribe",
unsubscribe: "Unubscribe",
mailbox: "Mailbox",
requestMailbox: "Request",
deleteMailbox: "Delete",
mailboxConnected: "Mailbox active",
channel: "Channel",
channel_placeholder: "my-channel",
disconnected: "Server disconnected",
reconnecting: "Reconnecting...",
info: "Info",
message: "Message",
messages: "Messages",
message_placeholder: "Hello, world!",
message_placeholder_generic: "Type a message...",
noMessagesReceived: "No messages received",
messageLastReceived: "Last received message",
messagesReceived: "Received Messages",
send: "Send",
serverInfo: "Server Info",
settings: "Settings",
subscribeToChannel: "Subscribe to channel",
subscribe: "Subscribe",
unsubscribe: "Unubscribe",
};
const allTranslations: { [key: string]: typeof staticTextEnglish } = {
en: staticTextEnglish,
es: {
mailbox: "Buzón",
requestMailbox: "Solicitar",
deleteMailbox: "Eliminar",
mailboxConnected: "Buzón activado",
channel: "Canal",
channel_placeholder: "mi-canal",
disconnected: "Sin coneción",
reconnecting: "Conectando...",
info: "Info",
message: "Mensaje",
messages: "Mensajes",
message_placeholder: "Hola!",
message_placeholder_generic: "Nuevo mensaje",
noMessagesReceived: "Sin mensajes",
messageLastReceived: "Último mensaje",
messagesReceived: "Todos los Mensajes",
send: "Enviar",
serverInfo: "Información",
settings: "Configuración",
subscribeToChannel: "Suscribirse a un canal",
subscribe: "Suscribirse",
unsubscribe: "Cancelar",
},
de: {
mailbox: "Briefkasten",
requestMailbox: "Beantragen",
deleteMailbox: "Löschen",
mailboxConnected: "Briefkasten aktiv",
channel: "Kanal",
channel_placeholder: "mein-kanal",
disconnected: "Verbindung getrennt",
reconnecting: "Verbindung wird hergestellt...",
info: "Daten",
message: "Nachricht",
messages: "Nachrichten",
message_placeholder: "Hallo!",
message_placeholder_generic: "Neue Nachricht",
noMessagesReceived: "Keine Nachrichten",
messageLastReceived: "Letzte Nachricht",
messagesReceived: "Empfangene Nachrichten",
send: "Senden",
serverInfo: "Serverdaten",
settings: "Einstellungen",
subscribeToChannel: "Kanal beitreten",
subscribe: "Beitreten",
unsubscribe: "Verlassen",
},
en: staticTextEnglish,
es: {
mailbox: "Buzón",
requestMailbox: "Solicitar",
deleteMailbox: "Eliminar",
mailboxConnected: "Buzón activado",
channel: "Canal",
channel_placeholder: "mi-canal",
disconnected: "Sin coneción",
reconnecting: "Conectando...",
info: "Info",
message: "Mensaje",
messages: "Mensajes",
message_placeholder: "Hola!",
message_placeholder_generic: "Nuevo mensaje",
noMessagesReceived: "Sin mensajes",
messageLastReceived: "Último mensaje",
messagesReceived: "Todos los Mensajes",
send: "Enviar",
serverInfo: "Información",
settings: "Configuración",
subscribeToChannel: "Suscribirse a un canal",
subscribe: "Suscribirse",
unsubscribe: "Cancelar",
},
de: {
mailbox: "Briefkasten",
requestMailbox: "Beantragen",
deleteMailbox: "Löschen",
mailboxConnected: "Briefkasten aktiv",
channel: "Kanal",
channel_placeholder: "mein-kanal",
disconnected: "Verbindung getrennt",
reconnecting: "Verbindung wird hergestellt...",
info: "Daten",
message: "Nachricht",
messages: "Nachrichten",
message_placeholder: "Hallo!",
message_placeholder_generic: "Neue Nachricht",
noMessagesReceived: "Keine Nachrichten",
messageLastReceived: "Letzte Nachricht",
messagesReceived: "Empfangene Nachrichten",
send: "Senden",
serverInfo: "Serverdaten",
settings: "Einstellungen",
subscribeToChannel: "Kanal beitreten",
subscribe: "Beitreten",
unsubscribe: "Verlassen",
},
};
const language = navigator.language.substring(0, 2);
+17 -11
View File
@@ -8,7 +8,7 @@ import Crypto from "crypto";
import { WS } from "./index";
export interface Subscriber {
send: (message: string) => void;
send: (message: string, compress?: boolean, id?: string) => void;
}
export class Mailbox implements Subscriber {
@@ -35,7 +35,7 @@ export class Mailbox implements Subscriber {
{
assignedMailboxId: this.id,
},
ws
ws,
);
}
@@ -46,7 +46,7 @@ export class Mailbox implements Subscriber {
{
connectedMailboxId: this.id,
},
this.ws
this.ws,
);
}
@@ -57,7 +57,7 @@ export class Mailbox implements Subscriber {
}
// storage
messages = new Set<string>();
messages = new Map<string, string>();
// websocket
private _ws: WS | undefined;
@@ -90,13 +90,13 @@ export class Mailbox implements Subscriber {
}
// sending
send(message: string): void {
if (this._ws?.readyState == 1) {
this._ws.send(message);
this.dateLastUsed = new Date();
} else {
this.messages.add(message);
}
send(message: string, _?: boolean, uuid?: string): void {
this._ws?.send(message);
console.log(this.id, uuid, message, "sending");
if (!uuid) return;
this.messages.set(uuid, message);
}
sendUnreadMessages(): void {
@@ -107,6 +107,12 @@ export class Mailbox implements Subscriber {
this.messages.delete(message);
});
}
checkMessageReceivedConfirmation(uuid: string): void {
if (!this.messages.has(uuid)) return;
this.dateLastUsed = new Date();
this.messages.delete(uuid);
}
}
export class SubscriptionMap {
+25 -25
View File
@@ -3,67 +3,67 @@ import { PACKAGE_FILE, STATIC_DIR, WS } from ".";
import Colors from "colors";
import Fs from "fs/promises";
import Path from "path";
import {
WebSocketMessage,
} from "./websocket-handler";
import { WebSocketMessage } from "./websocket-handler";
// CONFIG
const STAT_INDENT = 25;
// MISC
export function doIfIsString(data: any, cb: (data: string) => void): void {
if (typeof data != "string" || data == "") return;
cb(data);
if (typeof data != "string" || data == "") return;
cb(data);
}
export async function getVersionNumber(): Promise<string> {
try {
const packageString = await Bun.file(PACKAGE_FILE).text();
const packageObject = JSON.parse(packageString);
return packageObject.version;
} catch {
return "unknown";
}
try {
const packageString = await Bun.file(PACKAGE_FILE).text();
const packageObject = JSON.parse(packageString);
return packageObject.version;
} catch {
return "unknown";
}
}
// SERVER
export function createFileResponse(requestPath: string): Response {
const fullPath = Path.join(STATIC_DIR, requestPath);
return new Response(Bun.file(fullPath));
const fullPath = Path.join(STATIC_DIR, requestPath);
return new Response(Bun.file(fullPath));
}
// OUTPUT
async function writeToLogFile(message: string): Promise<void> {
await Fs.appendFile("logs", `${message}\n`);
await Fs.appendFile("logs", `${message}\n`);
}
// stats
export function writeStatString(label: string, value: string): void {
console.log(`${label.padEnd(STAT_INDENT, " ")} ${Colors.bold.green(value)}`);
console.log(
`${label.padEnd(STAT_INDENT, " ")} ${Colors.bold.green(value)}`,
);
}
export function writeStatNumber(label: string, value: number): void {
console.log(
`${label.padEnd(STAT_INDENT, " ")} ${Colors.bold.blue(value.toString())}`
);
console.log(
`${label.padEnd(STAT_INDENT, " ")} ${Colors.bold.blue(value.toString())}`,
);
}
// logging
export function writeError(message: any): void {
console.log(Colors.bold.red(message));
writeToLogFile(message);
console.log(Colors.bold.red(message));
writeToLogFile(message);
}
export function writeSuccess(message: string): void {
console.log(Colors.bold.green(message));
writeToLogFile(message);
console.log(Colors.bold.green(message));
writeToLogFile(message);
}
// MESSAGES
export function stringifyMessage(messageObject: WebSocketMessage): string {
return JSON.stringify(messageObject);
return JSON.stringify(messageObject);
}
export function parseMessage(messageString: string): Object {
return JSON.parse(messageString);
return JSON.parse(messageString);
}
+182 -165
View File
@@ -1,10 +1,10 @@
import { Mailbox, Subscriber, SubscriptionMap } from "./subscriptionHandler";
import {
doIfIsString,
parseMessage,
stringifyMessage,
writeError,
writeSuccess,
doIfIsString,
parseMessage,
stringifyMessage,
writeError,
writeSuccess,
} from "./utility";
import Crypto from "crypto";
@@ -13,27 +13,30 @@ import { defaultConfig } from "./config-handler";
// requests
export interface WebSocketMessage {
uuid?: string;
uuid?: string;
// connecting to server
requestingServerConnection?: boolean;
// connecting to server
requestingServerConnection?: boolean;
// mailbox
requestingMailboxSetup?: boolean;
requestedMailbox?: string;
deletingMailbox?: string;
assignedMailboxId?: string; // sent by server
connectedMailboxId?: string; // sent by server
deletedMailbox?: string; // sent by server
// mailbox
requestingMailboxSetup?: boolean;
requestedMailbox?: string;
deletingMailbox?: string;
assignedMailboxId?: string; // sent by server
connectedMailboxId?: string; // sent by server
deletedMailbox?: string; // sent by server
// subscribing to channel
subscribeChannel?: string;
unsubscribeChannel?: string;
subscribed?: boolean; // sent by server
// subscribing to channel
subscribeChannel?: string;
unsubscribeChannel?: string;
subscribed?: boolean; // sent by server
//sending message
messageChannel?: string;
messageBody?: string;
//sending message
messageChannel?: string;
messageBody?: string;
// confirming message
confirmingMessageReceived?: boolean;
}
// TRACKING
@@ -49,219 +52,233 @@ export const serversConnected = new Set<WebSocket>();
export const serversDisconnected = new Set<string>();
export function getServerCount() {
return serversConnected.size + serversConnectedAsClient.size;
return serversConnected.size + serversConnectedAsClient.size;
}
export function trackConnection(ws: WS) {
clientsConnected.add(ws);
clientsConnected.add(ws);
}
export function forgetConnection(ws: WS): void {
clientsConnected.delete(ws);
serversConnectedAsClient.delete(ws);
subscriptionMap.deleteSubscriber(ws);
clientsConnected.delete(ws);
serversConnectedAsClient.delete(ws);
subscriptionMap.deleteSubscriber(ws);
}
export function trackSubscription(
subscriber: Subscriber,
channel: string
subscriber: Subscriber,
channel: string,
): void {
subscriptionMap.set(subscriber, channel);
confirmSubscription(subscriber, channel);
subscriptionMap.set(subscriber, channel);
confirmSubscription(subscriber, channel);
}
export function forgetSubscription(
subscriber: Subscriber,
channel: string
subscriber: Subscriber,
channel: string,
): void {
subscriptionMap.delete(subscriber, channel);
confirmSubscription(subscriber, channel);
subscriptionMap.delete(subscriber, channel);
confirmSubscription(subscriber, channel);
}
// AUDIT
export function getWebSocketStats(): [string, number][] {
return [
["channels", subscriptionMap.subscribersPerChannel.size],
["clients connected", clientsConnected.size],
["mailboxes", mailboxes.size],
["servers connected", getServerCount()],
["servers disconnected", serversDisconnected.size],
];
return [
["channels", subscriptionMap.subscribersPerChannel.size],
["clients connected", clientsConnected.size],
["mailboxes", mailboxes.size],
["servers connected", getServerCount()],
["servers disconnected", serversDisconnected.size],
];
}
// OTHER SERVERS
export function connectServers(config: typeof defaultConfig): void {
config.connectedServers.forEach((serverAddress) => {
connectServer(serverAddress, config);
});
config.connectedServers.forEach((serverAddress) => {
connectServer(serverAddress, config);
});
}
export function reconnectServers(config: typeof defaultConfig): void {
[...serversDisconnected.values()].forEach((address) => {
connectServer(address, config);
});
[...serversDisconnected.values()].forEach((address) => {
connectServer(address, config);
});
}
export function connectServer(
address: string,
config: typeof defaultConfig
address: string,
config: typeof defaultConfig,
): void {
try {
// connect
const ws = new WebSocket(address);
serversConnected.add(ws);
try {
// connect
const ws = new WebSocket(address);
serversConnected.add(ws);
// listen
ws.addEventListener("open", () => {
serversDisconnected.delete(address);
// listen
ws.addEventListener("open", () => {
serversDisconnected.delete(address);
writeSuccess(`connected to server at "${address}".`);
writeSuccess(`connected to server at "${address}".`);
subscribeChannels(ws, config);
requestServerConnection(ws);
});
subscribeChannels(ws, config);
requestServerConnection(ws);
});
ws.addEventListener("message", (message) => {
processMessage(ws, message.data.toString(), config);
});
ws.addEventListener("message", (message) => {
processMessage(ws, message.data.toString(), config);
});
ws.addEventListener("close", () => {
serversConnected.delete(ws);
serversDisconnected.add(address);
writeError(`server at "${address}" disconnected`);
});
} catch (error) {
writeError(`failed to connect to server at "${address}": ${error}`);
}
ws.addEventListener("close", () => {
serversConnected.delete(ws);
serversDisconnected.add(address);
writeError(`server at "${address}" disconnected`);
});
} catch (error) {
writeError(`failed to connect to server at "${address}": ${error}`);
}
}
function subscribeChannels(ws: WS, config: typeof defaultConfig): void {
config.subscribedChannels.forEach((channel) => {
subscribeChannel(ws, channel);
});
config.subscribedChannels.forEach((channel) => {
subscribeChannel(ws, channel);
});
}
export function subscribeChannel(server: WS, channel: string): void {
const message: WebSocketMessage = {
subscribeChannel: channel,
};
const messageString = stringifyMessage(message);
server.send(messageString);
const message: WebSocketMessage = {
subscribeChannel: channel,
};
const messageString = stringifyMessage(message);
server.send(messageString);
}
// PROCESS MESSAGE
export function processMessage(
ws: WS,
messageString: string,
config: typeof defaultConfig
ws: WS,
messageString: string,
config: typeof defaultConfig,
): void {
// MESSAGE ID
// check id
const messageObject: WebSocketMessage = parseMessage(messageString);
if (!messageObject.uuid) {
// assign if no id
messageObject.uuid = Crypto.randomUUID();
} else {
// ignore if already seen
if (knownMessageIds.has(messageObject.uuid)) return;
}
// remember message
knownMessageIds.add(messageObject.uuid);
// MESSAGE ID
// check id
const messageObject: WebSocketMessage = parseMessage(messageString);
if (!messageObject.uuid) {
// assign if no id
messageObject.uuid = Crypto.randomUUID();
} else if (messageObject.confirmingMessageReceived == true) {
// received confirmation
mailboxes.forEach((mailbox) => {
if (mailbox.ws != ws) return;
return mailbox.checkMessageReceivedConfirmation(
messageObject.uuid!,
);
});
} else {
// ignore if already seen
if (knownMessageIds.has(messageObject.uuid)) return;
}
// remember message
knownMessageIds.add(messageObject.uuid);
// SUBSCRIPTIONS
doIfIsString(messageObject.subscribeChannel, (subscribeChannel) => {
trackSubscription(ws, subscribeChannel);
});
doIfIsString(messageObject.unsubscribeChannel, (unsubscribeChannel) => {
forgetSubscription(ws, unsubscribeChannel);
});
// SUBSCRIPTIONS
doIfIsString(messageObject.subscribeChannel, (subscribeChannel) => {
trackSubscription(ws, subscribeChannel);
});
doIfIsString(messageObject.unsubscribeChannel, (unsubscribeChannel) => {
forgetSubscription(ws, unsubscribeChannel);
});
// MESSAGE
doIfIsString(messageObject.messageChannel, (messageChannel) => {
if (messageObject.messageBody == undefined) return;
sendMessage(messageChannel, messageObject.messageBody, messageObject.uuid!);
});
// MESSAGE
doIfIsString(messageObject.messageChannel, (messageChannel) => {
if (messageObject.messageBody == undefined) return;
sendMessage(
messageChannel,
messageObject.messageBody,
messageObject.uuid!,
);
});
// MAILBOX
if (messageObject.requestingMailboxSetup == true) {
const mailbox = new Mailbox(ws);
mailboxes.set(mailbox.id, mailbox);
}
doIfIsString(messageObject.requestedMailbox, (requestedMailbox) => {
const mailbox = mailboxes.get(requestedMailbox);
if (!mailbox)
return sendWebSocketMessage(
{ deletedMailbox: requestedMailbox},
ws
);
mailbox.ws = ws;
});
doIfIsString(messageObject.deletingMailbox, (deletingMailbox) => {
const mailbox = mailboxes.get(deletingMailbox);
if (!mailbox) return;
removeMailbox(mailbox);
});
// MAILBOX
if (messageObject.requestingMailboxSetup == true) {
const mailbox = new Mailbox(ws);
mailboxes.set(mailbox.id, mailbox);
}
doIfIsString(messageObject.requestedMailbox, (requestedMailbox) => {
const mailbox = mailboxes.get(requestedMailbox);
if (!mailbox)
return sendWebSocketMessage(
{ deletedMailbox: requestedMailbox },
ws,
);
mailbox.ws = ws;
});
doIfIsString(messageObject.deletingMailbox, (deletingMailbox) => {
const mailbox = mailboxes.get(deletingMailbox);
if (!mailbox) return;
removeMailbox(mailbox);
});
// OTHER SERVER
if (messageObject.requestingServerConnection == true) {
clientsConnected.delete(ws);
serversConnectedAsClient.add(ws);
subscribeChannels(ws, config);
}
// OTHER SERVER
if (messageObject.requestingServerConnection == true) {
clientsConnected.delete(ws);
serversConnectedAsClient.add(ws);
subscribeChannels(ws, config);
}
}
// MAILBOX
export function removeExpiredMailboxes() {
mailboxes.forEach((mailbox) => {
if (mailbox.isExpired) removeMailbox(mailbox);
});
mailboxes.forEach((mailbox) => {
if (mailbox.isExpired) removeMailbox(mailbox);
});
}
export function removeMailbox(mailbox: Mailbox) {
mailbox.delete();
mailboxes.delete(mailbox.id);
mailbox.delete();
mailboxes.delete(mailbox.id);
}
// MESSAGING
export function sendWebSocketMessage(
messageObject: WebSocketMessage,
destination: Subscriber
messageObject: WebSocketMessage,
destination: Subscriber,
): void {
const messageString = JSON.stringify(messageObject);
destination.send(messageString);
const messageString = JSON.stringify(messageObject);
destination.send(messageString, false, messageObject.uuid);
}
export function sendMessage(
messageChannel: string,
messageBody: string,
uuid: string
messageChannel: string,
messageBody: string,
uuid: string,
): void {
const channels = messageChannel.split("/");
subscriptionMap.forwardMessage(channels, {
uuid,
messageChannel,
messageBody,
});
const channels = messageChannel.split("/");
subscriptionMap.forwardMessage(channels, {
uuid,
messageChannel,
messageBody,
});
}
function requestServerConnection(ws: WS): void {
const messageObject = {
requestingServerConnection: true,
};
const messageString = stringifyMessage(messageObject);
ws.send(messageString);
const messageObject = {
requestingServerConnection: true,
};
const messageString = stringifyMessage(messageObject);
ws.send(messageString);
}
function confirmSubscription(
subscriber: Subscriber,
messageChannel: string
subscriber: Subscriber,
messageChannel: string,
): void {
const messageObject: WebSocketMessage = {
messageChannel,
subscribed: subscriptionMap.getChannelList(subscriber)?.has(messageChannel)
? true
: false,
};
const messageString = stringifyMessage(messageObject);
subscriber.send(messageString);
const messageObject: WebSocketMessage = {
messageChannel,
subscribed: subscriptionMap
.getChannelList(subscriber)
?.has(messageChannel)
? true
: false,
};
const messageString = stringifyMessage(messageObject);
subscriber.send(messageString);
}