// REPRO: two StateBaseV3 instances replicating over an in-process DHT testnet
// (the production path — same as probe-state-base.js, just a local DHT), exercising
// the optimistic→writable transition that the hyper-tps mesh hits once two peers
// connect. A member (B) writes several edits before + during admission; we assert
// every edit COMMITS and REPLICATES to the creator (A), B becomes writable, and
// no autobase encode crash occurs.
//
// Reproduces the v575/v577 failures locally (no deploy, no 2nd machine):
// - v576 (ackWriter-once dedup): later optimistic edits roll back → LOST on A.
// - v577 (ackWriter per-block): edits commit BUT autobase _appendLocalNodes
// crashes on an undefined-value local node.
// Target (keet-faithful, addWriter-only): all edits commit on A, no crash.
import { StateBaseV3 } from '../lib/state-base-v3.js'
import createTestnet from 'hyperdht/testnet'
import crypto from 'hypercore-crypto'
import b4a from 'b4a'
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
const ROOT = path.join(os.tmpdir(), 'sbv3-optcommit-' + Bare.pid)
try { fs.rmSync(ROOT, { recursive: true, force: true }) } catch {}
const roomKey = crypto.randomBytes(32)
const roomSeed = crypto.randomBytes(32)
const kpA = crypto.keyPair() // admin → elected creator (writable)
const kpB = crypto.keyPair() // peer member (joins, admitted optimistically)
const hex = (b) => b4a.toString(b, 'hex')
const members = [
{ key: hex(kpA.publicKey), role: 1 },
{ key: hex(kpB.publicKey), role: 3 }
]
let pass = 0; let fail = 0
const ok = (c, m) => { if (c) { pass++; console.log(' ok ' + m) } else { fail++; console.log(' FAIL ' + m) } }
const sleep = (ms) => new Promise(r => setTimeout(r, ms))
let crashed = null
Bare.on('unhandledRejection', (err) => { crashed = err && err.message ? err.message : String(err) })
let stage = 'init'
const watchdog = setTimeout(() => { console.log(' [WATCHDOG] stuck at stage=' + stage + ' — exiting to flush'); Bare.exit(3) }, 45000)
async function main () {
stage = 'createTestnet'
console.log(' [stage] creating testnet...')
const testnet = await createTestnet(3)
console.log(' [stage] testnet up, bootstrap=', testnet.bootstrap)
const bootstrap = testnet.bootstrap
const baseOpts = (kp) => ({ roomSeed, encryptionKey: roomSeed, network: true, swarmOpts: { bootstrap }, selfKeyPair: kp, selfKey: kp.publicKey, getMembers: async () => members })
stage = 'A.ready'; const A = new StateBaseV3(path.join(ROOT, 'A'), roomKey, baseOpts(kpA))
await A.ready()
ok(A.writable, 'A (admin) elected creator → writable')
await A.projectMembers(members)
stage = 'B.ready'; const B = new StateBaseV3(path.join(ROOT, 'B'), roomKey, baseOpts(kpB))
await B.ready()
ok(!B.writable, 'B (peer) opens as member, not writable')
const sync = async (rounds = 10) => {
for (let i = 0; i < rounds; i++) {
await A.base.update().catch(() => {})
await B.base.update().catch(() => {})
await sleep(80)
}
}
// Confirm the swarm connects (A.base grows as B/replication appears, or just
// log peer counts).
stage = 'sync-connect'; await sync(25)
console.log(' [diag] after connect: A.swarm peers=%d B.swarm peers=%d A.base.len=%d',
A.swarm ? A.swarm.connections.size : -1, B.swarm ? B.swarm.connections.size : -1, A.base.length)
// B writes MULTIPLE edits while not-yet-writable (the pile-up that triggers the
// rollback / crash). set() self-selects optimistic+admit since B isn't writable.
await B.set('world:terrain:edit:1', { op: 'dig', x: 1 })
await B.set('world:terrain:edit:2', { op: 'dig', x: 2 })
await B.set('world:terrain:edit:3', { op: 'dig', x: 3 })
await sync(30)
// After admission propagates, B should be writable; one more on the normal path.
for (let i = 0; i < 12 && !B.writable; i++) { await sync(8); console.log(' [diag] wait writable round %d: B.writable=%s A.writers=%d', i, B.writable, A.base.activeWriters ? A.base.activeWriters.size : -1) }
ok(B.writable, '1. B became WRITABLE after admission (addWriter landed on B.local.key)')
if (B.writable) { await B.set('world:terrain:edit:4', { op: 'dig', x: 4 }); await sync(20) }
// DIAGNOSTICS.
const bState = await B.list('world:terrain:edit:')
console.log(' [diag] B sees own edits:', Object.keys(bState).sort().join(',') || '(none)')
console.log(' [diag] B.local.len=%d B.base.len=%d A.base.len=%d A.writers=%d',
B.base.local.length, B.base.length, A.base.length, A.base.activeWriters ? A.base.activeWriters.size : -1)
const aState = await A.list('world:terrain:edit:')
console.log(' [diag] A sees edits:', Object.keys(aState).sort().join(',') || '(none)')
for (let i = 1; i <= 4; i++) {
ok(aState['world:terrain:edit:' + i] != null,
`2.${i} A sees B's edit ${i} (committed + replicated)`)
}
ok(crashed === null, '3. NO autobase encode crash — got: ' + (crashed || 'none'))
// Skip close()/destroy() — they can block under bare; process exit cleans up.
clearTimeout(watchdog)
console.log('\n[probe] pass=' + pass + ' fail=' + fail)
Bare.exit(fail ? 1 : 0)
}
main().catch((e) => { console.error('[probe] THREW:', e && e.stack ? e.stack : e); Bare.exit(2) })