OverviewFilesCommitsIssuesDocsReleasesPeers

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

89 lines · javascript ref:
// Probe: two independent StateBase instances bootstrapping themselves
// via the announce-swarm and converging on shared state.
//
// Peer A is "founder" — calls set() first, creates the bootstrap.
// Peer B starts with no bootstrap; should receive it via the announce
// channel, joinKey, replicate, and see A's write.
//
// Run: bare test/probe-state-base.js
//
// PASS = peer B's view contains the field A wrote, within 60s.

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-room-' + 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-state-A-' + Date.now())
const dirB = path.join(os.tmpdir(), 'probe-state-B-' + Date.now())
fs.mkdirSync(dirA, { recursive: true })
fs.mkdirSync(dirB, { recursive: true })

console.log('roomId:', ROOM_ID)
console.log('roomTag:', roomTag.toString('hex').slice(0, 16) + '…')
console.log('announceTopic:', StateBase.announceTopic(roomTag).toString('hex').slice(0, 16) + '…')

// In-process DHT testnet — bypasses public-DHT discovery latency for the
// single-process probe. On real two-machine deploys both daemons use the
// default public DHT and find each other in seconds.
console.log('\nspinning up local DHT testnet...')
const testnet = await createTestnet(3)
const bootstrap = testnet.bootstrap
console.log('testnet bootstrap:', bootstrap)

const swarmOpts = { bootstrap }
const sbA = new StateBase(dirA, roomTag, { swarmOpts })
const sbB = new StateBase(dirB, roomTag, { swarmOpts })

let exitCode = 1

try {
  console.log('\n[A] opening (will be founder once we set)...')
  await sbA.ready()

  console.log('[B] opening (no bootstrap yet)...')
  await sbB.ready()

  console.log('\n[A] writing world:time = 12.0 (founder bootstrap)')
  await sbA.set('world:time', 12.0)
  console.log('[A] state.key:', sbA.stateBaseKey.toString('hex').slice(0, 12) + '…')
  console.log('[A] writable:', sbA.writable)

  console.log('\n[B] polling for world:time (deadline ' + (DEADLINE_MS / 1000) + 's)...')
  const t0 = Date.now()
  while (Date.now() - t0 < DEADLINE_MS) {
    const v = await sbB.get('world:time')
    if (v !== null && v !== undefined) {
      console.log('[B] saw value at +' + ((Date.now() - t0) / 1000).toFixed(1) + 's: ' + JSON.stringify(v))
      console.log('[B] state.key:', sbB.stateBaseKey?.toString('hex')?.slice(0, 12) + '…')
      console.log('[B] writable:', sbB.writable)
      console.log('[B] hasBootstrap:', sbB.hasBootstrap)
      console.log('\nPASS — peer B received bootstrap via announce + replicated state.')
      exitCode = 0
      break
    }
    await new Promise(r => setTimeout(r, 500))
  }
  if (exitCode !== 0) {
    console.log('FAIL — B never saw A\'s write within ' + (DEADLINE_MS / 1000) + 's.')
    console.log('[B] hasBootstrap:', sbB.hasBootstrap, 'writable:', sbB.writable)
    console.log('[B] state.key:', sbB.stateBaseKey?.toString('hex')?.slice(0, 12) || 'null')
  }
} 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)
}