Something went wrong. Try again.
Demo comparing html-over-the-wire w/ WS and SSE.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145// Test 4 — the honest cost of splitting the transport.//// A WebSocket is ONE ordered byte stream, so commands are applied in send// order for free. SSE+POST is two channels: N concurrent POSTs can be// reordered in flight, and the POST's own response races the SSE patch.//// (a) inversions: fire commands pipelined, count how often the server applied// them out of send order.// (b) race: how often does the POST response land BEFORE the patch it caused,// and by how much. Anything keyed off POST completion is a bug surface.
import { startServer, startProxy, sleep, summarize, table } from './lib.js'import { createClient } from '../src/client.js'
const BURST = Number(process.env.BURST ?? 10)const REPS = Number(process.env.REPS ?? 40)const RTTS = (process.env.ORD_RTTS ?? '0,25,100').split(',').map(Number)
const VARIANTS = [ { name: 'ws', arch: 'ws', tls: true }, { name: 'sse+POST h2', arch: 'sse', tls: true }, { name: 'sse+POST h1', arch: 'sse', tls: true, h1: true },]
const results = []
for (const rtt of RTTS) { for (const v of VARIANTS) { const server = await startServer({ ...v, compress: 'none', mode: 'narrow' }) const proxy = await startProxy({ targetPort: server.port, owdMs: rtt / 2 }) await sleep(200)
const c = createClient({ arch: v.arch, h1: v.h1, host: '127.0.0.1', port: proxy.port, tls: v.tls, room: '0', compress: 'none', }) await c.connect()
let inversions = 0 let totalPairs = 0 const raceGaps = [] // + means POST resolved before patch arrived let postFirst = 0 let observed = 0
// One handler for the life of the connection; each rep swaps the sink it // writes into. Re-registering per rep would leak listeners. let sink = null c.onPatch((nonces) => sink?.(nonces))
for (let rep = 0; rep < REPS; rep++) { const arrivalOrder = [] const patchAt = new Map() const waiters = new Map() const base = rep * 1000
sink = (nonces) => { for (const n of nonces) { patchAt.set(n, performance.now()) arrivalOrder.push(n) waiters.get(n)?.() } }
const postAt = new Map() const sends = [] for (let i = 0; i < BURST; i++) { const nonce = base + i + 1 const p = new Promise((res) => waiters.set(nonce, res)) // Deliberately NOT awaited: this is a user clicking fast, or several // independent components dispatching at once. sends.push( c.dispatch({ id: i, nonce }).then(() => { postAt.set(nonce, performance.now()) }), ) sends.push(p) } await Promise.race([ Promise.all(sends), new Promise((res) => setTimeout(res, 10000)), ]) await sleep(rtt + 60)
// Count order inversions between what we sent and what came back. const sent = Array.from({ length: BURST }, (_, i) => base + i + 1) const seen = arrivalOrder.filter((n) => sent.includes(n)) for (let i = 0; i < seen.length; i++) { for (let j = i + 1; j < seen.length; j++) { totalPairs++ if (seen[i] > seen[j]) inversions++ } } observed += seen.length
for (const n of sent) { const pa = patchAt.get(n) const po = postAt.get(n) if (pa != null && po != null) { raceGaps.push(pa - po) // >0: patch arrived after the POST resolved if (pa > po) postFirst++ } } }
const gap = summarize(raceGaps) results.push({ rtt, variant: v.name, inversionRate: totalPairs ? +((inversions / totalPairs) * 100).toFixed(2) : 0, delivered: observed, expected: BURST * REPS, postFirstPct: raceGaps.length ? +((postFirst / raceGaps.length) * 100).toFixed(1) : 0, gapP50: gap.p50, gapP95: gap.p95, })
c.close() await sleep(150) await proxy.close() await server.stop() await sleep(300) console.error(` done rtt=${rtt} ${v.name}`) }}
console.log(`\n=== Test 4: ordering + write/read race (bursts of ${BURST}, ${REPS} reps) ===\n`)console.log( table(results, [ { label: 'rtt', get: (r) => `${r.rtt}ms` }, { label: 'variant', get: (r) => r.variant }, { label: 'out-of-order pairs', get: (r) => `${r.inversionRate}%` }, { label: 'delivered', get: (r) => `${r.delivered}/${r.expected}` }, { label: 'ack-before-patch', get: (r) => `${r.postFirstPct}%` }, { label: 'ack→patch p50', get: (r) => `${r.gapP50} ms` }, { label: 'ack→patch p95', get: (r) => `${r.gapP95} ms` }, ]),)
console.log('\n' + JSON.stringify({ test: 'ordering', burst: BURST, reps: REPS, results }))