feat: implement websocket realtime data streaming

This commit is contained in:
syntaxbullet
2026-01-07 13:25:41 +01:00
parent ff23f22337
commit ac4025e179
4 changed files with 186 additions and 3 deletions

View File

@@ -33,4 +33,47 @@ document.addEventListener("DOMContentLoaded", () => {
updateUptime();
// Update every second
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();
});

View File

@@ -4,17 +4,61 @@ import type { Server } from "bun";
export class WebServer {
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({
port: env.PORT || 3000,
fetch: router,
port: port ?? (typeof env.PORT === "string" ? parseInt(env.PORT) : 3000),
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}`);
// 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() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
if (this.server) {
this.server.stop();
console.log("🛑 Web server stopped");

54
src/web/websocket.test.ts Normal file
View 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");
});
});

View 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.