// Cross-machine autobase replication test, driven by tabertron + stortron.
//
// Goal: prove the canonical autobase v7.27.3 multi-writer pattern works
// over real Hyperswarm DHT replication. One peer is primary (creates the
// autobase, owns the only indexer), the other is mirror (joins via the
// bootstrap key, added by primary as a non-indexer writer). Both append
// ops, both see each other's writes through the linearized view.
//
// Usage (3-step flow because we exchange keys via shell, not RPC):
//
// # Step 1 — on tabertron (PRIMARY):
// node test/cross-machine-autobase.js init-primary --storage /tmp/abe2e-tb
// # Outputs: BOOTSTRAP_KEY=<hex>, PRIMARY_LOCAL=<hex>
//
// # Step 2 — on stortron (MIRROR), with the BOOTSTRAP_KEY from step 1:
// node test/cross-machine-autobase.js init-mirror --storage /tmp/abe2e-st --bootstrap <hex>
// # Outputs: MIRROR_LOCAL=<hex>
//
// # Step 3 — back on tabertron, add the mirror as writer:
// node test/cross-machine-autobase.js add-writer --storage /tmp/abe2e-tb --writer <MIRROR_LOCAL hex>
//
// # Step 4 — run both peers in parallel (different terminals):
// # tabertron: node test/cross-machine-autobase.js run --storage /tmp/abe2e-tb --role primary
// # stortron: node test/cross-machine-autobase.js run --storage /tmp/abe2e-st --role mirror
//
// # Each "run" appends 5 ops timestamped with the role, then waits up to 60s
// # for the OTHER role's 5 ops to appear in the view, then exits with a
// # PASS/FAIL summary.
import Autobase from 'autobase'
import Corestore from 'corestore'
import Hyperswarm from 'hyperswarm'
import b4a from 'b4a'
import crypto from 'hypercore-crypto'
import fs from 'fs'
const args = parseArgs(process.argv.slice(2))
const cmd = args._[0]
async function main () {
if (cmd === 'init-primary') return initPrimary()
if (cmd === 'init-mirror') return initMirror()
if (cmd === 'add-writer') return addWriter()
if (cmd === 'run') return runPeer()
console.error('usage: cross-machine-autobase.js {init-primary|init-mirror|add-writer|run} [opts]')
process.exit(2)
}
function parseArgs (argv) {
const out = { _: [] }
for (let i = 0; i < argv.length; i++) {
const a = argv[i]
if (a.startsWith('--')) {
const key = a.slice(2)
const val = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true
out[key] = val
} else out._.push(a)
}
return out
}
// Apply: handles two op types — register-writer (host.addWriter) + msg (view.append)
async function apply (nodes, view, host) {
for (const node of nodes) {
const v = node.value
if (v && v.type === 'register-writer') {
try {
await host.addWriter(b4a.from(v.writerKey, 'hex'), { indexer: !!v.indexer })
} catch (err) {
console.error('[apply] addWriter failed:', err.message)
}
} else {
await view.append(v)
}
}
}
async function openBase (storage, bootstrapKey = null) {
const store = new Corestore(storage)
await store.ready()
const base = new Autobase(store, bootstrapKey, {
apply,
open: (s) => s.get('view', { valueEncoding: 'json' }),
valueEncoding: 'json',
ackInterval: 1000 // production-canonical auto-ack so signedLength advances
})
await base.ready()
return { store, base }
}
async function initPrimary () {
if (!args.storage) throw new Error('--storage required')
if (fs.existsSync(args.storage)) {
console.error(`refusing to clobber existing storage at ${args.storage}`)
process.exit(1)
}
const { store, base } = await openBase(args.storage)
console.log(`BOOTSTRAP_KEY=${b4a.toString(base.key, 'hex')}`)
console.log(`PRIMARY_LOCAL=${b4a.toString(base.local.key, 'hex')}`)
await base.close()
await store.close()
}
async function initMirror () {
if (!args.storage) throw new Error('--storage required')
if (!args.bootstrap) throw new Error('--bootstrap <hex> required')
if (fs.existsSync(args.storage)) {
console.error(`refusing to clobber existing storage at ${args.storage}`)
process.exit(1)
}
const { store, base } = await openBase(args.storage, b4a.from(args.bootstrap, 'hex'))
console.log(`MIRROR_LOCAL=${b4a.toString(base.local.key, 'hex')}`)
await base.close()
await store.close()
}
async function addWriter () {
if (!args.storage) throw new Error('--storage required')
if (!args.writer) throw new Error('--writer <hex> required')
const { store, base } = await openBase(args.storage)
await base.append({ type: 'register-writer', writerKey: args.writer, indexer: false })
await base.update()
console.log(`appended addWriter op for ${args.writer}`)
await base.close()
await store.close()
}
async function runPeer () {
if (!args.storage) throw new Error('--storage required')
if (!args.role) throw new Error('--role primary|mirror required')
const role = args.role
const { store, base } = await openBase(args.storage)
console.log(`[${role}] booted`)
console.log(`[${role}] autobase key = ${b4a.toString(base.key, 'hex').slice(0, 16)}…`)
console.log(`[${role}] local key = ${b4a.toString(base.local.key, 'hex').slice(0, 16)}…`)
console.log(`[${role}] writable = ${base.writable}`)
// Hyperswarm replication on the autobase's discoveryKey.
const swarm = new Hyperswarm()
swarm.on('connection', (conn, info) => {
const remoteHex = b4a.toString(info.publicKey || Buffer.alloc(32), 'hex').slice(0, 12)
console.log(`[${role}] swarm conn from ${remoteHex}`)
store.replicate(conn)
})
const topic = base.discoveryKey
console.log(`[${role}] joining topic ${b4a.toString(topic, 'hex').slice(0, 16)}…`)
const discovery = swarm.join(topic, { client: true, server: true })
await discovery.flushed()
console.log(`[${role}] swarm flushed (announced + queried)`)
// Append 5 ops tagged with role.
// Mirror may need to wait until it's been added as writer.
if (!base.writable) {
console.log(`[${role}] waiting up to 60s for writability…`)
await waitFor(() => base.writable, 60000, 500)
if (!base.writable) {
console.error(`[${role}] never became writable — primary didn't add us`)
process.exit(1)
}
}
console.log(`[${role}] writable; appending 5 ops`)
for (let i = 0; i < 5; i++) {
await base.append({ from: role, seq: i, ts: Date.now() })
}
await base.update()
// Wait for the OTHER role's 5 ops to appear in the view.
const otherRole = role === 'primary' ? 'mirror' : 'primary'
console.log(`[${role}] waiting up to 60s to see 5 ops from ${otherRole}…`)
let seenOther = 0
await waitFor(async () => {
seenOther = 0
const len = base.view ? base.view.length : 0
for (let i = 0; i < len; i++) {
const v = await base.view.get(i)
if (v && v.from === otherRole) seenOther++
}
return seenOther >= 5
}, 60000, 1000)
console.log(`[${role}] saw ${seenOther} ops from ${otherRole}`)
console.log(`[${role}] view length = ${base.view.length}`)
console.log(`[${role}] signed length = ${base.signedLength}`)
console.log(`[${role}] indexerCount = ${base.linearizer.indexers.length}`)
console.log(`[${role}] activeWriters = ${base.activeWriters.size}`)
if (seenOther >= 5) console.log(`[${role}] PASS`)
else console.log(`[${role}] FAIL — only saw ${seenOther}/5`)
await swarm.destroy()
await base.close()
await store.close()
process.exit(seenOther >= 5 ? 0 : 1)
}
async function waitFor (pred, timeoutMs, pollMs) {
const t0 = Date.now()
while (Date.now() - t0 < timeoutMs) {
if (await pred()) return true
await new Promise(r => setTimeout(r, pollMs))
}
return false
}
main().catch(err => { console.error(err); process.exit(1) })