Clean working version: deployed TypeScript backend + JSX frontend

- Replace old JS backend with working TypeScript backend from deployed containers
- Backend: Fastify + better-sqlite3 + Yjs CRDT, 17 TS source files, 13 test files
- Frontend: React JSX with fabric.js CAD canvas, vite build, nginx serving
- Fix nginx proxy port 5000→3001 to match backend
- Fix docker-compose port mapping 5000→3001
- Fix vite dev proxy port 5000→3001
- Add backend healthcheck to docker-compose
- Update index.html title to 'Web CAD', lang to 'de'
- Add .a0/ to .gitignore
- Remove old JS backend files (models, routes, config, middleware, seed)
- Remove tracked build artifacts (backend/public/)
This commit is contained in:
Agent Zero
2026-06-28 01:24:31 +02:00
parent 249d5fbdb6
commit 8fa6f795c0
60 changed files with 8988 additions and 3708 deletions
+137
View File
@@ -0,0 +1,137 @@
/**
* Yjs WebSocket Server Real-time collaboration with LevelDB persistence
*/
import * as Y from 'yjs';
import { LeveldbPersistence } from 'y-leveldb';
import type { FastifyInstance } from 'fastify';
import type { WebSocket } from '@fastify/websocket';
const PERSISTENCE_DIR = process.env.YJS_PERSISTENCE_DIR || '/tmp/yjs-documents';
// Document store: docName → Y.Doc
const docs = new Map<string, Y.Doc>();
// Connection tracking: docName → Set<WebSocket>
const connections = new Map<string, Set<WebSocket>>();
let persistence: LeveldbPersistence | null = null;
let persistenceReady: Promise<void> | null = null;
export async function initYjsPersistence(): Promise<void> {
try {
persistence = new LeveldbPersistence(PERSISTENCE_DIR);
console.log(`Yjs persistence initialized at ${PERSISTENCE_DIR}`);
} catch (err) {
console.warn('Yjs persistence failed to init (non-fatal):', err);
}
}
export async function closeYjsPersistence(): Promise<void> {
if (persistence) {
await persistence.destroy();
persistence = null;
}
}
async function getOrCreateDoc(docName: string): Promise<Y.Doc> {
if (docs.has(docName)) {
return docs.get(docName)!;
}
const doc = new Y.Doc();
docs.set(docName, doc);
// Load persisted state if available
if (persistence && persistenceReady) {
try {
await persistenceReady;
const persistedYdoc = await persistence.getYDoc(docName);
const update = Y.encodeStateAsUpdate(persistedYdoc);
if (update.length > 0) {
Y.applyUpdate(doc, update);
}
} catch (err) {
console.warn(`Failed to load doc ${docName}:`, err);
}
}
// Persist updates to LevelDB
doc.on('update', (update: Uint8Array) => {
if (persistence) {
persistence.storeUpdate(docName, update).catch((err: any) => {
console.warn(`Failed to persist update for ${docName}:`, err);
});
}
});
return doc;
}
function broadcastUpdate(docName: string, update: Uint8Array, exclude?: WebSocket): void {
const conns = connections.get(docName);
if (!conns) return;
for (const ws of conns) {
if (ws !== exclude && ws.readyState === 1 /* OPEN */) {
try {
ws.send(update);
} catch {
// Connection error — will be cleaned up on close
}
}
}
}
export function registerYjsWebSocket(fastify: FastifyInstance): void {
fastify.get('/ws/collab/:docName', { websocket: true }, async (socket: WebSocket, request: any) => {
const docName = request.params.docName as string;
if (!docName) {
socket.close(4000, 'Missing docName');
return;
}
const doc = await getOrCreateDoc(docName);
// Track connection
if (!connections.has(docName)) {
connections.set(docName, new Set());
}
connections.get(docName)!.add(socket);
// Send current document state to new client
const stateUpdate = Y.encodeStateAsUpdate(doc);
if (stateUpdate.length > 0 && socket.readyState === 1) {
socket.send(stateUpdate);
}
// Listen for updates from this client
socket.on('message', (data: Buffer) => {
try {
const update = new Uint8Array(data);
Y.applyUpdate(doc, update);
broadcastUpdate(docName, update, socket);
} catch (err) {
console.warn(`Invalid update from client for ${docName}:`, err);
}
});
// Clean up on disconnect
socket.on('close', () => {
const conns = connections.get(docName);
if (conns) {
conns.delete(socket);
if (conns.size === 0) {
connections.delete(docName);
// Optionally keep doc in memory for a while
// For now, remove it to free memory
docs.delete(docName);
}
}
});
socket.on('error', () => {
const conns = connections.get(docName);
if (conns) {
conns.delete(socket);
}
});
});
}