// Layer 6 — Test Sidecar Instance
//
// Stripped-down sidecar for integration tests.
// Uses DHT testnet bootstrap (no public DHT).
// Connects to Hyperswarm topic, exchanges metadata (name, encryptionKey, hosting, gamePort).
//
// Usage: node layer6_sidecar.js <index> <bootstrap_file> <socket_path>
import Hyperswarm from 'hyperswarm'
import DHT from 'hyperdht'
import { createHash } from 'crypto'
import net from 'net'
import fs from 'fs'
const index = parseInt(process.argv[2] || '0', 10)
const bootstrapFile = process.argv[3] || '/tmp/layer_bootstrap.txt'
const socketPath = process.argv[4] || `/tmp/layer6_sidecar_${index}.sock`
const peerInfoFile = `/tmp/layer6_peer_${index}.json`
const GAME_TOPIC = 'hyper-game:test'
// Read bootstrap
const bootstrapAddrs = fs.readFileSync(bootstrapFile, 'utf8').trim().split(',')
const bootstrap = bootstrapAddrs.map(addr => {
const [host, port] = addr.split(':')
return { host, port: parseInt(port, 10) }
})
console.log(`[sidecar-${index}] bootstrap: ${bootstrapAddrs.join(', ')}`)
// Identity
const kp = DHT.keyPair()
const identity = {
name: `Player-${index}`,
publicKey: kp.publicKey.toString('hex'),
encryptionKey: '',
hosting: false,
gamePort: 0
}
console.log(`[sidecar-${index}] pk: ${identity.publicKey.slice(0, 12)}...`)
// Peer tracking
const peers = new Map()
let godotSocket = null
let closed = false
function send (obj) {
if (!godotSocket || godotSocket.destroyed) return
godotSocket.write(JSON.stringify(obj) + '\n')
}
// Hyperswarm discovery
const swarm = new Hyperswarm({ keyPair: kp, bootstrap })
const topic = createHash('sha256').update(GAME_TOPIC).digest()
swarm.on('connection', (conn, info) => {
const remoteKey = info.publicKey.toString('hex')
console.log(`[sidecar-${index}] connection from ${remoteKey.slice(0, 12)}...`)
// Send our metadata
conn.write(JSON.stringify(identity) + '\n')
let buf = ''
conn.on('data', (chunk) => {
buf += chunk.toString()
let idx
while ((idx = buf.indexOf('\n')) !== -1) {
const line = buf.slice(0, idx).trim()
buf = buf.slice(idx + 1)
if (!line) continue
try {
const meta = JSON.parse(line)
if (meta.publicKey && meta.name) {
const existing = peers.get(meta.publicKey)
if (!existing) {
peers.set(meta.publicKey, { ...meta, conn, info })
send({ event: 'peer-discovered', ...meta })
console.log(`[sidecar-${index}] discovered: ${meta.name} (${meta.publicKey.slice(0, 12)}...) hosting=${meta.hosting}`)
} else {
// Update
Object.assign(existing, meta)
send({ event: 'peer-updated', ...meta })
}
}
if (meta.requestAddress) {
const raw = conn.rawStream
if (raw) {
send({ event: 'peer-address', publicKey: meta.from || remoteKey, host: raw.remoteHost || '127.0.0.1', port: meta.gamePort || 9514 })
}
}
} catch {}
}
})
conn.on('close', () => {
for (const [pk, p] of peers) {
if (p.conn === conn) {
peers.delete(pk)
send({ event: 'peer-gone', publicKey: pk })
break
}
}
})
conn.on('error', () => {})
})
const discovery = swarm.join(topic, { server: true, client: true })
await discovery.flushed()
console.log(`[sidecar-${index}] topic joined, waiting for peers...`)
// Write peer info
fs.writeFileSync(peerInfoFile, JSON.stringify(identity))
// IPC server for Godot
try { fs.unlinkSync(socketPath) } catch {}
const server = net.createServer((socket) => {
if (godotSocket) { socket.destroy(); return }
godotSocket = socket
let buffer = ''
socket.on('data', (chunk) => {
buffer += chunk.toString()
let idx
while ((idx = buffer.indexOf('\n')) !== -1) {
const line = buffer.slice(0, idx).trim()
buffer = buffer.slice(idx + 1)
if (!line) continue
try {
const msg = JSON.parse(line)
handleMessage(msg)
} catch {}
}
})
socket.on('close', () => { godotSocket = null })
socket.on('error', () => { godotSocket = null })
})
server.listen(socketPath, () => {
console.log(`[sidecar-${index}] IPC on ${socketPath}`)
send({ event: 'identity', ...identity })
})
function handleMessage (msg) {
switch (msg.method) {
case 'host': {
identity.hosting = true
identity.gamePort = msg.gamePort || 19518
identity.encryptionKey = msg.encryptionKey || ''
// Broadcast update to all peers
for (const [, peer] of peers) {
if (peer.conn && !peer.conn.destroyed) {
peer.conn.write(JSON.stringify(identity) + '\n')
}
}
console.log(`[sidecar-${index}] hosting on port ${identity.gamePort}`)
// Update peer info file
fs.writeFileSync(peerInfoFile, JSON.stringify(identity))
break
}
case 'stop-hosting': {
identity.hosting = false
identity.encryptionKey = ''
for (const [, peer] of peers) {
if (peer.conn && !peer.conn.destroyed) {
peer.conn.write(JSON.stringify(identity) + '\n')
}
}
break
}
case 'join': {
const pk = msg.publicKey
const peer = peers.get(pk)
if (!peer) {
send({ event: 'error', message: `Unknown peer: ${pk}` })
return
}
const raw = peer.conn?.rawStream
if (raw) {
send({ event: 'peer-address', publicKey: pk, host: raw.remoteHost || '127.0.0.1', port: peer.gamePort || 19518 })
}
break
}
case 'stats': {
send({
event: 'stats',
identity,
peers: Array.from(peers.entries()).map(([pk, p]) => ({
publicKey: pk.slice(0, 12),
name: p.name,
hosting: p.hosting,
encryptionKey: (p.encryptionKey || '').slice(0, 12)
}))
})
break
}
case 'shutdown': {
close()
break
}
}
}
async function close () {
if (closed) return
closed = true
await swarm.destroy()
server.close()
try { fs.unlinkSync(socketPath) } catch {}
try { fs.unlinkSync(peerInfoFile) } catch {}
process.exit(0)
}
process.on('SIGTERM', close)
process.on('SIGINT', close)