trails/apps/planner/app/lib/yjs-server.ts
Ullrich Schäfer 464ba0f64d
Add Planner session management with Yjs WebSocket (#1)
- Yjs WebSocket server at /sync/:sessionId for real-time collaboration
- Session creation API (POST /api/sessions) with optional GPX initialization
- Session join page with Planner layout (header, map area, sidebar)
- In-memory session store (PostgreSQL persistence deferred to infra setup)
- Custom production server entry with WebSocket upgrade handling
- Vite path aliases (~/) for clean imports
- Fix tsconfig for apps: noEmit, exclude build dir

Tasks completed: 4.1, 4.3, 4.4, 4.5
Deferred: 4.2 (PostgreSQL), 4.6-4.8 (need client-side Yjs from Group 6)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:26:03 +01:00

78 lines
1.9 KiB
TypeScript

import { WebSocketServer, type WebSocket } from "ws";
import * as Y from "yjs";
import type { IncomingMessage, Server } from "node:http";
const docs = new Map<string, Y.Doc>();
export function getOrCreateDoc(sessionId: string): Y.Doc {
let doc = docs.get(sessionId);
if (!doc) {
doc = new Y.Doc();
docs.set(sessionId, doc);
}
return doc;
}
export function deleteDoc(sessionId: string): boolean {
const doc = docs.get(sessionId);
if (doc) {
doc.destroy();
docs.delete(sessionId);
return true;
}
return false;
}
export function getDocCount(): number {
return docs.size;
}
export function setupYjsWebSocket(server: Server): WebSocketServer {
const wss = new WebSocketServer({ noServer: true });
server.on("upgrade", (request, socket, head) => {
const url = new URL(request.url ?? "", `http://${request.headers.host}`);
const match = url.pathname.match(/^\/sync\/(.+)$/);
if (!match) {
socket.destroy();
return;
}
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit("connection", ws, request, match[1]);
});
});
wss.on("connection", (ws: WebSocket, _request: IncomingMessage, sessionId: string) => {
const doc = getOrCreateDoc(sessionId);
// Send current state to new client
const state = Y.encodeStateAsUpdate(doc);
ws.send(state);
// Listen for updates from this client
ws.on("message", (data: Buffer) => {
try {
const update = new Uint8Array(data);
Y.applyUpdate(doc, update);
// Broadcast to all other clients in this session
for (const client of wss.clients) {
if (client !== ws && client.readyState === ws.OPEN) {
client.send(data);
}
}
} catch (_e) {
// Ignore malformed updates
}
});
ws.on("close", () => {
// Clean up empty sessions
// (In production, this would check participant count)
});
});
return wss;
}