Something went wrong. Try again.
Demo comparing html-over-the-wire w/ WS and SSE.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248// Per-session in-memory SQLite, capped in two directions:// * rows per session — a session can't grow without bound// * sessions per server — an LRU evicts the coldest DB entirely//// node:sqlite is built in as of Node 22, so this stays zero-dependency.
import { DatabaseSync } from 'node:sqlite'import { seedIssues } from './render.js'
export const ISSUE_ROWS = 240export const ISSUE_CAP = 400
export const ROW_CAP = 300export const SESSION_CAP = 40const IDLE_MS = 30 * 60 * 1000
// Two panes is the normal case; 8 leaves room for a few tabs without letting// one cookie fan a broadcast out to hundreds of streams.export const MAX_SUBS_PER_SESSION = 8
// Commands per second, per session. Well above holding down Enter — the point// is to bound the CPU amplification, since every command re-renders the whole// log and brotli-compresses it once per subscriber.const RATE_PER_SEC = 25const RATE_BURST = 60
const sessions = new Map() // sid -> Session (Map preserves insertion order = LRU)
function makeDb() { const db = new DatabaseSync(':memory:') db.exec(` CREATE TABLE msg ( id INTEGER PRIMARY KEY AUTOINCREMENT, via TEXT NOT NULL, body TEXT NOT NULL, ts INTEGER NOT NULL ); CREATE INDEX msg_id ON msg(id);
CREATE TABLE issue ( id INTEGER PRIMARY KEY, title TEXT NOT NULL, status TEXT NOT NULL, assignee TEXT NOT NULL, points INTEGER NOT NULL ); `) const ins = db.prepare('INSERT INTO issue (id,title,status,assignee,points) VALUES (?,?,?,?,?)') for (const r of seedIssues(ISSUE_ROWS)) ins.run(r.id, r.title, r.status, r.assignee, r.points) return db}
class Session { constructor(id) { this.id = id this.db = makeDb() this.createdAt = Date.now() this.lastSeen = Date.now() this.subs = new Set() // { transport, send(html, raw) } // Metrics watchers are deliberately NOT in `subs`: they must never receive // patches, or the dashboard would inflate the very numbers it reports. this.watchers = new Set() this.seq = 0 this.tokens = RATE_BURST this.rlLast = Date.now() // Grid view state lives on the server too — the client holds no state at // all, it just morphs whatever arrives. this.grid = { q: '', sort: 'id', dir: 'asc' } this.metrics = { ws: newTransportMetrics(), sse: newTransportMetrics(), } this.insert = this.db.prepare('INSERT INTO msg (via, body, ts) VALUES (?, ?, ?)') this.trim = this.db.prepare('DELETE FROM msg WHERE id <= ?') this.selectAll = this.db.prepare('SELECT * FROM msg ORDER BY id') this.selectOne = this.db.prepare('SELECT * FROM msg WHERE id = ?') this.countRows = this.db.prepare('SELECT COUNT(*) AS n FROM msg') }
append(via, body) { const ts = Date.now() const info = this.insert.run(via, body, ts) const id = Number(info.lastInsertRowid) // Keep only the newest ROW_CAP rows. if (id > ROW_CAP) this.trim.run(id - ROW_CAP) this.lastSeen = ts return this.selectOne.get(id) }
all() { return this.selectAll.all() }
// Token bucket. Returns false when the caller is going too fast. takeToken() { const now = Date.now() this.tokens = Math.min(RATE_BURST, this.tokens + ((now - this.rlLast) / 1000) * RATE_PER_SEC) this.rlLast = now if (this.tokens < 1) return false this.tokens -= 1 return true }
canSubscribe() { return this.subs.size < MAX_SUBS_PER_SESSION }
// Filtering and sorting happen in SQLite, so the client ships no logic for // either — which is the point of doing this server-side at all. issues({ q = '', sort = 'id', dir = 'asc' } = {}) { // Whitelist: `sort` reaches an ORDER BY clause and must never be raw input. const col = { id: 'id', title: 'title', status: 'status', assignee: 'assignee', points: 'points' }[sort] ?? 'id' const d = dir === 'desc' ? 'DESC' : 'ASC' const like = `%${q}%` const rows = this.db .prepare( `SELECT * FROM issue WHERE title LIKE ? OR assignee LIKE ? OR status LIKE ? ORDER BY ${col} ${d}, id ASC`, ) .all(like, like, like) return rows }
issueCount() { return this.db.prepare('SELECT COUNT(*) AS n FROM issue').get().n }
// A real write against a large view: one row changes, the whole table // re-renders. Capped like the chat log so a session stays bounded. addIssue(title) { const max = this.db.prepare('SELECT COALESCE(MAX(id), 0) AS m FROM issue').get().m const id = max + 1 const statuses = ['triage', 'open', 'in-progress', 'blocked', 'done'] const people = ['dana', 'rk', 'imani', 'wes', 'nadia', 'ofelia', 'tomas', 'jun', 'petra', 'sam'] this.db .prepare('INSERT INTO issue (id,title,status,assignee,points) VALUES (?,?,?,?,?)') .run(id, title, statuses[id % statuses.length], people[id % people.length], [1, 2, 3, 5, 8, 13][id % 6]) const n = this.issueCount() if (n > ISSUE_CAP) { this.db.prepare('DELETE FROM issue WHERE id IN (SELECT id FROM issue ORDER BY id LIMIT ?)').run(n - ISSUE_CAP) } return id }
stats() { const pc = this.db.prepare('PRAGMA page_count').get().page_count const ps = this.db.prepare('PRAGMA page_size').get().page_size return { rows: this.countRows.get().n, bytes: pc * ps, rowCap: ROW_CAP, } }
close() { for (const w of this.watchers) { try { w.close?.() } catch {} } this.watchers.clear() for (const s of this.subs) { try { s.close?.() } catch {} } this.subs.clear() try { this.db.close() } catch {} }}
function newTransportMetrics() { return { connects: 0, disconnects: 0, open: 0, patches: 0, commands: 0, rawDown: 0, // HTML bytes before compression wireDown: 0, // bytes actually put on the wire, incl. framing wireUp: 0, // bytes for the command going the other way // Marginal cost of the most recent patch. The cumulative ratio is diluted // by every cheap early patch, so this is the number that tells the story. lastPatch: 0, lastRaw: 0, }}
// Returns null when the server is at capacity and nothing can be freed. The// caller must surface that as a 503 rather than allocating anyway — otherwise// holding one connection per cookie grows the map without bound, since a// session with a live subscriber is never evictable.export function getSession(sid, { create = true } = {}) { let s = sessions.get(sid) if (s) { // Refresh LRU position. sessions.delete(sid) sessions.set(sid, s) s.lastSeen = Date.now() return s } if (!create) return null evict() if (sessions.size >= SESSION_CAP) return null s = new Session(sid) sessions.set(sid, s) return s}
function evict() { const now = Date.now() for (const [sid, s] of sessions) { if (s.subs.size === 0 && now - s.lastSeen > IDLE_MS) { s.close() sessions.delete(sid) } } // Oldest-first until we're back under the cap. Never evict one that still // has a live subscriber — that would drop someone's open demo. If every // session is occupied we stop here and getSession() refuses to create more. while (sessions.size >= SESSION_CAP) { let victim = null for (const [sid, s] of sessions) { if (s.subs.size === 0) { victim = sid break } } if (!victim) break sessions.get(victim).close() sessions.delete(victim) }}
export function serverStats() { let rows = 0 let bytes = 0 for (const s of sessions.values()) { const st = s.stats() rows += st.rows bytes += st.bytes } return { sessions: sessions.size, sessionCap: SESSION_CAP, rows, bytes }}