#!/usr/bin/env node
/**
* Hyperswarm relay test — proves game data flows through real Hyperswarm connections.
*
* Creates two sidecars with a local DHT testnet, discovers peers, and sends
* ping/pong messages through the Hyperswarm encrypted stream.
*
* This is the Node.js equivalent of the GDScript test_relay.tscn scene.
*
* Usage:
* node benchmarks/test-relay.cjs # local testnet (automatic)
* node benchmarks/test-relay.cjs real # real DHT (needs two machines)
*/
const net = require('net')
const createTestnet = require('hyperdht/testnet')
const PING_COUNT = 50
const DISCOVERY_TIMEOUT = 20000 // DHT discovery can take 10-15s on testnet
const TEST_TIMEOUT = 60000
async function runLocal () {
const testnet = await createTestnet(3)
console.log('Local DHT testnet ready')
const { startSidecar } = await import('../sidecar.js')
const s1 = await startSidecar(3200, { bootstrap: testnet.bootstrap })
const s2 = await startSidecar(3201, { bootstrap: testnet.bootstrap })
const clientA = net.createConnection(3200, '127.0.0.1')
const clientB = net.createConnection(3201, '127.0.0.1')
await Promise.all([
new Promise(r => clientA.on('connect', r)),
new Promise(r => clientB.on('connect', r))
])
let bufA = '', bufB = ''
let peerOfA = ''
let pingsSent = 0
let pongsReceived = 0
const rtts = []
function sendTo (s, m) { s.write(JSON.stringify(m) + '\n') }
function parse (buf, cb) {
let idx
while ((idx = buf.indexOf('\n')) !== -1) {
const line = buf.slice(0, idx).trim()
buf = buf.slice(idx + 1)
if (line) try { cb(JSON.parse(line)) } catch {}
}
return buf
}
// Client A: sender — discovers peer, sends pings, collects pongs
clientA.on('data', c => { bufA = parse(bufA + c.toString(), handleA) })
clientB.on('data', c => { bufB = parse(bufB + c.toString(), handleB) })
function handleA (d) {
if (d.event === 'identity') console.log(`[sender] Identity: ${d.name} (${d.publicKey.slice(0, 12)}...)`)
if (d.event === 'lobby-ready') console.log('[sender] Lobby ready, waiting for peer...')
if (d.event === 'peer-discovered') {
peerOfA = d.publicKey
console.log(`[sender] Peer discovered: ${d.name} — starting ${PING_COUNT} pings`)
sendPing()
}
if (d.event === 'relay-data' && d.data?.type === 'pong') {
const sentUs = d.data.ts
const nowUs = process.hrtime.bigint()
const rttMs = Number(nowUs - BigInt(sentUs)) / 1e6
rtts.push(rttMs)
pongsReceived++
if (pongsReceived <= 5 || pongsReceived % 10 === 0) {
console.log(` pong #${pongsReceived} RTT=${rttMs.toFixed(2)}ms`)
}
if (pongsReceived < PING_COUNT) sendPing()
else finish()
}
if (d.event === 'error') console.log('[sender] ERROR:', d.message)
}
// Client B: responder — echoes pings as pongs
function handleB (d) {
if (d.event === 'identity') console.log(`[echo] Identity: ${d.name}`)
if (d.event === 'peer-discovered') console.log(`[echo] Peer discovered: ${d.name}`)
if (d.event === 'relay-data' && d.data?.type === 'ping') {
sendTo(clientB, {
method: 'relay',
target: d.from,
data: { type: 'pong', ts: d.data.ts }
})
}
}
function sendPing () {
pingsSent++
const ts = Number(process.hrtime.bigint())
sendTo(clientA, {
method: 'relay',
target: peerOfA,
data: { type: 'ping', ts, seq: pingsSent }
})
}
async function finish () {
console.log('')
console.log('=== HYPERSWARM RELAY TEST RESULTS ===')
console.log(`Pings: ${pingsSent} Pongs: ${pongsReceived} Loss: ${((1 - pongsReceived / pingsSent) * 100).toFixed(1)}%`)
if (rtts.length > 0) {
const sorted = rtts.slice().sort((a, b) => a - b)
const avg = rtts.reduce((a, b) => a + b, 0) / rtts.length
const p50 = sorted[Math.floor(sorted.length * 0.5)]
const p95 = sorted[Math.floor(sorted.length * 0.95)]
const p99 = sorted[Math.min(Math.floor(sorted.length * 0.99), sorted.length - 1)]
const min = sorted[0]
const max = sorted[sorted.length - 1]
console.log(`RTT: avg=${avg.toFixed(2)}ms min=${min.toFixed(2)}ms max=${max.toFixed(2)}ms`)
console.log(` p50=${p50.toFixed(2)}ms p95=${p95.toFixed(2)}ms p99=${p99.toFixed(2)}ms`)
console.log('')
console.log('Path: Godot→IPC→Sidecar→Hyperswarm(Noise XX encrypted)→Sidecar→IPC→Godot')
console.log('')
console.log(pongsReceived === PING_COUNT ? 'PASS: All pongs received through Hyperswarm!' : 'PARTIAL: Some pongs lost')
}
console.log('\n--- JSON ---')
console.log(JSON.stringify({
test: 'hyperswarm-relay',
pings: pingsSent,
pongs: pongsReceived,
rtts: rtts.length > 0 ? {
avg: (rtts.reduce((a, b) => a + b, 0) / rtts.length).toFixed(3),
min: Math.min(...rtts).toFixed(3),
max: Math.max(...rtts).toFixed(3),
p50: rtts.slice().sort((a, b) => a - b)[Math.floor(rtts.length * 0.5)]?.toFixed(3),
p95: rtts.slice().sort((a, b) => a - b)[Math.floor(rtts.length * 0.95)]?.toFixed(3),
samples: rtts.length
} : null
}, null, 2))
clientA.destroy()
clientB.destroy()
await s1.close()
await s2.close()
await testnet.destroy()
process.exit(pongsReceived >= PING_COUNT ? 0 : 1)
}
// Start discovery
sendTo(clientA, { method: 'start', gameName: 'hyper-tps-relay-test' })
await new Promise(r => setTimeout(r, 1000))
sendTo(clientB, { method: 'start', gameName: 'hyper-tps-relay-test' })
// Global timeout
setTimeout(async () => {
console.log(`\nTIMEOUT after ${TEST_TIMEOUT / 1000}s — pings:${pingsSent} pongs:${pongsReceived}`)
if (pingsSent === 0) console.log('Peers never discovered each other (DHT too slow?)')
clientA.destroy(); clientB.destroy()
await s1.close(); await s2.close(); await testnet.destroy()
process.exit(1)
}, TEST_TIMEOUT)
}
runLocal().catch(e => { console.error(e); process.exit(1) })