// Probe: multi-writer ROUND-TRIP. Validates issue hyper-fleeky-dev#32's
// "members=writers" claim — a non-founder peer's write must linearize back
// into the shared view that the founder reads (not just founder → member).
//
// A = founder (genesis indexer). B = joiner, added to the writer set as an
// indexer via the `hyper-tps/state-writer-exchange` Protomux channel.
//
// step 1: A.set(world:time) → B must see it (founder → member; baseline)
// step 2: B.set(world:player_b) → A must see it (member → founder; THE proof)
// step 3: both peers converge on BOTH keys.
//
// Run: bare test/probe-state-roundtrip.js
// PASS = A sees B's write AND both peers hold both keys within the deadline.
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import crypto from 'hypercore-crypto'
import createTestnet from 'hyperdht/testnet.js'
import { StateBase } from '../lib/state-base.js'
const ROOM_ID = 'probe-rt-' + Math.random().toString(36).slice(2, 10)
const roomTag = crypto.hash(Buffer.from(ROOM_ID, 'utf-8'))
const DEADLINE_MS = 60000
const dirA = path.join(os.tmpdir(), 'probe-rt-A-' + Date.now())
const dirB = path.join(os.tmpdir(), 'probe-rt-B-' + Date.now())
fs.mkdirSync(dirA, { recursive: true })
fs.mkdirSync(dirB, { recursive: true })
console.log('roomId:', ROOM_ID)
console.log('spinning up local DHT testnet...')
const testnet = await createTestnet(3)
const swarmOpts = { bootstrap: testnet.bootstrap }
const sbA = new StateBase(dirA, roomTag, { swarmOpts })
const sbB = new StateBase(dirB, roomTag, { swarmOpts })
let exitCode = 1
async function waitFor (sb, key, label) {
const t0 = Date.now()
while (Date.now() - t0 < DEADLINE_MS) {
const v = await sb.get(key)
if (v !== null && v !== undefined) {
console.log('[%s] saw %s at +%ss: %s', label, key,
((Date.now() - t0) / 1000).toFixed(1), JSON.stringify(v))
return v
}
await new Promise(r => setTimeout(r, 500))
}
return null
}
try {
console.log('\n[A] ready...'); await sbA.ready()
console.log('[B] ready...'); await sbB.ready()
// ── step 1: founder → member (baseline) ──
console.log('\n[A] set world:time = 12.0 (founder)')
await sbA.set('world:time', 12.0)
const bSawTime = await waitFor(sbB, 'world:time', 'B')
if (bSawTime === null) { console.log('FAIL step1: B never saw A write'); throw new Error('step1') }
// B joins the writer set as an indexer via writer-exchange once A acks.
// Poll writable so we write step 2 with B as a confirmed writer.
const tw = Date.now()
while (!sbB.writable && Date.now() - tw < 20000) {
await new Promise(r => setTimeout(r, 250))
}
console.log('[B] writable before member write:', sbB.writable)
// ── step 2: member → founder (the real proof) ──
console.log('\n[B] set world:player_b = "ready" (member write)')
await sbB.set('world:player_b', 'ready')
const aSawB = await waitFor(sbA, 'world:player_b', 'A')
if (aSawB === null) {
console.log('FAIL step2: A never saw B write — member write did NOT linearize back')
console.log('[B] writable:', sbB.writable, 'state.key:', sbB.stateBaseKey?.toString('hex')?.slice(0, 12))
throw new Error('step2')
}
// ── step 3: full convergence ──
const aTime = await sbA.get('world:time')
const aPB = await sbA.get('world:player_b')
const bTime = await sbB.get('world:time')
const bPB = await sbB.get('world:player_b')
console.log('\nconvergence:')
console.log(' A:', JSON.stringify({ time: aTime, player_b: aPB }))
console.log(' B:', JSON.stringify({ time: bTime, player_b: bPB }))
if (aTime != null && aPB != null && bTime != null && bPB != null) {
console.log('\nPASS — bidirectional multi-writer convergence: member write linearized back to founder.')
exitCode = 0
} else {
console.log('\nFAIL — peers did not converge on both keys.')
}
} catch (err) {
console.error('error:', err.message)
console.error(err.stack)
} finally {
try { await sbA.close() } catch {}
try { await sbB.close() } catch {}
try { await testnet.destroy() } catch {}
try { fs.rmSync(dirA, { recursive: true, force: true }) } catch {}
try { fs.rmSync(dirB, { recursive: true, force: true }) } catch {}
Bare.exit(exitCode)
}