OverviewFilesCommitsIssuesDocsReleasesPeers

keet-daemon / test / probe-state-roundtrip.js

108 lines · javascript ref:
// 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)
}