forked from syntaxbullet/AuroraBot-discord
feat: implement websocket realtime data streaming
This commit is contained in:
@@ -33,4 +33,47 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
updateUptime();
|
updateUptime();
|
||||||
// Update every second
|
// Update every second
|
||||||
setInterval(updateUptime, 1000);
|
setInterval(updateUptime, 1000);
|
||||||
|
|
||||||
|
// WebSocket Connection
|
||||||
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
const wsUrl = `${protocol}//${window.location.host}/ws`;
|
||||||
|
|
||||||
|
function connectWs() {
|
||||||
|
const ws = new WebSocket(wsUrl);
|
||||||
|
const statusIndicator = document.querySelector(".status-indicator");
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log("WS Connected");
|
||||||
|
if (statusIndicator) statusIndicator.classList.add("online");
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(event.data);
|
||||||
|
if (msg.type === "HEARTBEAT") {
|
||||||
|
console.log("Heartbeat:", msg.data);
|
||||||
|
// Sync uptime?
|
||||||
|
// We can optionally verify if client clock is drifting, but let's keep it simple.
|
||||||
|
} else if (msg.type === "WELCOME") {
|
||||||
|
console.log(msg.message);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("WS Parse Error", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
console.log("WS Disconnected");
|
||||||
|
if (statusIndicator) statusIndicator.classList.remove("online");
|
||||||
|
// Retry in 5s
|
||||||
|
setTimeout(connectWs, 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (err) => {
|
||||||
|
console.error("WS Error", err);
|
||||||
|
ws.close();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
connectWs();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,17 +4,61 @@ import type { Server } from "bun";
|
|||||||
|
|
||||||
export class WebServer {
|
export class WebServer {
|
||||||
private static server: Server<unknown> | null = null;
|
private static server: Server<unknown> | null = null;
|
||||||
|
private static heartbeatInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
public static start() {
|
public static start(port?: number) {
|
||||||
this.server = Bun.serve({
|
this.server = Bun.serve({
|
||||||
port: env.PORT || 3000,
|
port: port ?? (typeof env.PORT === "string" ? parseInt(env.PORT) : 3000),
|
||||||
fetch: router,
|
fetch: (req, server) => {
|
||||||
|
const url = new URL(req.url);
|
||||||
|
if (url.pathname === "/ws") {
|
||||||
|
// Upgrade the request to a WebSocket
|
||||||
|
// We pass dummy data for now
|
||||||
|
if (server.upgrade(req, { data: undefined })) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return new Response("WebSocket upgrade failed", { status: 500 });
|
||||||
|
}
|
||||||
|
return router(req);
|
||||||
|
},
|
||||||
|
websocket: {
|
||||||
|
open(ws) {
|
||||||
|
// console.log("ws: client connected");
|
||||||
|
ws.subscribe("status-updates");
|
||||||
|
ws.send(JSON.stringify({ type: "WELCOME", message: "Connected to Aurora WebSocket" }));
|
||||||
|
},
|
||||||
|
message(ws, message) {
|
||||||
|
// Handle incoming messages if needed
|
||||||
|
},
|
||||||
|
close(ws) {
|
||||||
|
// console.log("ws: client disconnected");
|
||||||
|
ws.unsubscribe("status-updates");
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`🌐 Web server listening on http://localhost:${this.server.port}`);
|
console.log(`🌐 Web server listening on http://localhost:${this.server.port}`);
|
||||||
|
|
||||||
|
// Start a heartbeat loop
|
||||||
|
this.heartbeatInterval = setInterval(() => {
|
||||||
|
if (this.server) {
|
||||||
|
const uptime = process.uptime();
|
||||||
|
this.server.publish("status-updates", JSON.stringify({
|
||||||
|
type: "HEARTBEAT",
|
||||||
|
data: {
|
||||||
|
uptime,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static stop() {
|
public static stop() {
|
||||||
|
if (this.heartbeatInterval) {
|
||||||
|
clearInterval(this.heartbeatInterval);
|
||||||
|
this.heartbeatInterval = null;
|
||||||
|
}
|
||||||
if (this.server) {
|
if (this.server) {
|
||||||
this.server.stop();
|
this.server.stop();
|
||||||
console.log("🛑 Web server stopped");
|
console.log("🛑 Web server stopped");
|
||||||
|
|||||||
54
src/web/websocket.test.ts
Normal file
54
src/web/websocket.test.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { describe, expect, it, afterAll, beforeAll } from "bun:test";
|
||||||
|
import { WebServer } from "./server";
|
||||||
|
|
||||||
|
describe("WebSocket Server", () => {
|
||||||
|
// Start server on a random port
|
||||||
|
const port = 0;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
WebServer.start(port);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
WebServer.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept websocket connection and send welcome message", async () => {
|
||||||
|
// We need to know the actual port assigned by Bun if we passed 0.
|
||||||
|
// But WebServer stores it in private static server.
|
||||||
|
// We can't access it easily unless we expose it or use a known port.
|
||||||
|
// Let's rely on the fact that if we pass 0, we can't easily know the port without exposing it.
|
||||||
|
// So for this test, let's pick a random high port to avoid conflict: 40000 + Math.floor(Math.random() * 1000)
|
||||||
|
|
||||||
|
// Actually, let's restart with a known port 8081 for testing
|
||||||
|
WebServer.stop();
|
||||||
|
const testPort = 8081;
|
||||||
|
WebServer.start(testPort);
|
||||||
|
|
||||||
|
const ws = new WebSocket(`ws://localhost:${testPort}/ws`);
|
||||||
|
|
||||||
|
const messagePromise = new Promise<any>((resolve) => {
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
resolve(JSON.parse(event.data as string));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const msg = await messagePromise;
|
||||||
|
expect(msg.type).toBe("WELCOME");
|
||||||
|
expect(msg.message).toContain("Connected");
|
||||||
|
|
||||||
|
ws.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject non-ws upgrade requests on /ws endpoint via http", async () => {
|
||||||
|
const testPort = 8081;
|
||||||
|
// Just a normal fetch to /ws should fail with 426 Upgrade Required usually,
|
||||||
|
// but our implementation returns "WebSocket upgrade failed" 500 or undefined -> 101 Switching Protocols if valid.
|
||||||
|
// If we send a normal GET request to /ws without Upgrade headers, server.upgrade(req) returns false.
|
||||||
|
// So it returns status 500 "WebSocket upgrade failed" based on our code.
|
||||||
|
|
||||||
|
const res = await fetch(`http://localhost:${testPort}/ws`);
|
||||||
|
expect(res.status).toBe(500);
|
||||||
|
expect(await res.text()).toBe("WebSocket upgrade failed");
|
||||||
|
});
|
||||||
|
});
|
||||||
42
tickets/2026-01-07-websocket-realtime-data.md
Normal file
42
tickets/2026-01-07-websocket-realtime-data.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# 2026-01-07-websocket-realtime-data
|
||||||
|
|
||||||
|
**Status:** Done
|
||||||
|
**Created:** 2026-01-07
|
||||||
|
**Tags:** feature, websocket, realtime, research
|
||||||
|
|
||||||
|
## 1. Context & User Story
|
||||||
|
* **As a:** Developer
|
||||||
|
* **I want to:** implement a WebSocket connection between the frontend and the Aurora server
|
||||||
|
* **So that:** I can stream real-time data (profiling, logs, events) to the dashboard without manual page refreshes.
|
||||||
|
|
||||||
|
## 2. Technical Requirements
|
||||||
|
### Data Model Changes
|
||||||
|
- [ ] N/A
|
||||||
|
|
||||||
|
### API / Interface
|
||||||
|
- [x] **Endpoint:** `/ws` (Upgrade Upgrade: websocket).
|
||||||
|
- [x] **Protocol:** Define a simple JSON message format (e.g., `{ type: "UPDATE", data: { ... } }`).
|
||||||
|
|
||||||
|
## 3. Constraints & Validations (CRITICAL)
|
||||||
|
- **Bun Support:** Use Bun's native `Bun.serve({ websocket: { ... } })` capabilities if possible.
|
||||||
|
- **Security:** Ensure that the WebSocket endpoint is not publicly abusable (consider simple token or origin check if necessary, though internal usage is primary context for now).
|
||||||
|
- **Performance:** Do not flood the client. Throttle updates if necessary.
|
||||||
|
|
||||||
|
## 4. Acceptance Criteria
|
||||||
|
1. [x] Server accepts WebSocket connections on `/ws`.
|
||||||
|
2. [x] Client (`script.js`) successfully connects to the WebSocket.
|
||||||
|
3. [x] Server sends a "Hello" or "Ping" packet.
|
||||||
|
4. [x] Client receives and logs the packet.
|
||||||
|
5. [x] (Stretch) Stream basic uptime or heartbeat every 5 seconds.
|
||||||
|
|
||||||
|
## 5. Implementation Plan
|
||||||
|
- [x] Modify `src/web/server.ts` to handle `websocket` upgrade in `Bun.serve`.
|
||||||
|
- [x] Create a message handler object/function to manage connected clients.
|
||||||
|
- [x] Update `src/web/public/script.js` to initialize `WebSocket`.
|
||||||
|
- [x] Test connection stability.
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
- Enabled `websocket` in `Bun.serve` within `src/web/server.ts`.
|
||||||
|
- Implemented a heartbeat mechanism broadcasting `HEARTBEAT` events every 5s.
|
||||||
|
- Updated `script.js` to auto-connect, handle reconnects, and update a visual "online" indicator.
|
||||||
|
- Added `src/web/websocket.test.ts` to verify protocol upgrades and messaging.
|
||||||
Reference in New Issue
Block a user