Files
BizGaze_Remote/server/server.js
T
Sravan 63f2c588da feat(scale): swappable pub/sub layer + fix chat.js missed awaits (Phase 6)
Two things:

1. FIX a live regression the async conversion missed: chat.js calls repos via the
   lazy repos() helper (not the R. prefix), so my sweep skipped it — effectiveStatus
   / broadcastPresence read `repos().users.byId(userId)` synchronously, but that's a
   Promise now, so presence broadcasts always reported status 'active' and dropped
   last_seen. Now awaited (effectiveStatus/broadcastPresence async); touchSeen is a
   fire-and-forget UPDATE with .catch. Audited all non-R. repo calls — only chat.js
   was affected (media.js backfill was already awaited).

2. Swappable pub/sub for multi-instance real-time fan-out (the actual blocker to
   running >1 instance — not the DB). server/pubsub.js picks a backend by
   PUBSUB_BACKEND (default 'memory'). Local socket delivery is UNCHANGED; publish is
   additive — memory = no-op (zero hot-path cost, identical single-instance
   behaviour), redis = fan-out to other instances with a self-echo guard. chat.js
   pushToUser/broadcastPresence now also publish; each instance subscribes to deliver
   remote events to its local sockets. Interface is tiny so Redis is one swappable
   file (Postgres LISTEN/NOTIFY or NATS could drop in the same way — never hardwired,
   as requested). Dormant redis service added to compose behind the 'scale' profile;
   redis dep added; PUBSUB_BACKEND/REDIS_URL documented.

Validated: smoke 22/22 (memory), e2e chat delivery green. NOTE: full multi-instance
also needs distributed presence (isOnline is per-process) + meeting-signaling
sharing — chat/presence fan out via this layer; those are follow-ups.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 23:20:02 +05:30

84 lines
4.0 KiB
JavaScript

// BizGaze Connect — backend entry point.
// Thin wiring layer: HTTP request dispatch + WebSocket attach + listeners.
// All logic lives in focused modules:
// repos.js data-access (all SQL)
// bizgaze.js BizGaze identity provider
// lib.js HTTP helpers (json/readBody/parseCookies/now)
// session.js currentUser / audit
// presence.js shared in-memory live state (agents/sessions/shares)
// routes.js HTTP JSON API (/api/*, /sso)
// static.js static files + authenticated downloads (GET fallback)
// signaling.js WebSocket signaling (consent + SDP/ICE relay)
const http = require('http');
const https = require('https');
const fs = require('fs');
const path = require('path');
const { WebSocketServer } = require('ws');
const { PORT, HTTPS_PORT } = require('./config');
const { json } = require('./lib');
const routes = require('./routes');
const { handleGet } = require('./static');
const { onConnection } = require('./signaling');
// ---------- HTTP request dispatch ----------
const server = http.createServer((req, res) => {
const key = `${req.method} ${req.url.split('?')[0]}`;
// Route/static handlers are async now (DB adapter). Catch any rejection so a handler error becomes a
// 500 instead of a hung request + unhandled promise rejection.
let p;
if (routes[key]) p = routes[key](req, res);
else if (req.method === 'GET') p = handleGet(req, res); // downloads + static
else return json(res, 404, { error: 'not found' });
if (p && typeof p.catch === 'function') p.catch((e) => { try { console.error('handler error', key, e && e.message); json(res, 500, { error: 'server error' }); } catch (_) {} });
});
// ---------- WebSocket signaling ----------
const wss = new WebSocketServer({ server, path: '/ws' });
wss.on('connection', onConnection);
// Apply the DB schema BEFORE serving. For Postgres this creates the tables (async); for SQLite it's a
// no-op (the schema is already applied synchronously when db.js is required). Serving only starts once the
// store is ready, so the first request can never hit a missing table.
const db = require('./dbx');
const pubsub = require('./pubsub');
function startListening() {
server.listen(PORT, () => {
console.log(`HTTP on http://localhost:${PORT} (db=${db.name})`);
try { require('./media').backfill(); } catch (e) {} // streaming renditions for older video uploads
});
// HTTPS — required so other devices can share their screen (browsers block
// screen capture on non-secure origins). Uses cert.pem/key.pem if present.
try {
const certPath = path.join(__dirname, 'cert.pem');
const keyPath = path.join(__dirname, 'key.pem');
if (fs.existsSync(certPath) && fs.existsSync(keyPath)) {
const httpsServer = https.createServer(
{ cert: fs.readFileSync(certPath), key: fs.readFileSync(keyPath) },
(req, res) => server.emit('request', req, res)
);
const wssSecure = new WebSocketServer({ server: httpsServer, path: '/ws' });
wssSecure.on('connection', onConnection);
httpsServer.listen(HTTPS_PORT, () => {
console.log(`HTTPS on https://localhost:${HTTPS_PORT} (use this address from other devices)`);
console.log(` End user shares screen: https://<this-pc-ip>:${HTTPS_PORT}/share`);
console.log(` Technician connects: https://<this-pc-ip>:${HTTPS_PORT}/connect`);
});
} else {
console.log('(No cert.pem/key.pem found — HTTPS disabled. Other devices can view but not share their screen.)');
}
} catch (e) {
console.log('HTTPS failed to start:', e.message);
}
}
// DB schema first, then the pub/sub layer (redis connects + flushes buffered subscriptions; memory is a
// no-op), then serve. A pubsub failure must NOT block booting — degrade to local-only delivery.
db.init()
.then(() => pubsub.init().catch((e) => console.error('pubsub init failed (local-only delivery):', (e && e.message) || e)))
.then(startListening)
.catch((e) => { console.error('DB init failed:', (e && e.message) || e); process.exit(1); });
module.exports = { server };