OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-rooms / test / room-pairing.test.js

146 lines · javascript ref:
// Proof for RoomPairing (blind-pairing join + write-enrollment over Hyperswarm).
//
// Canonical Keet pattern: an invite code → blind-pairing over the DHT → the
// host addWriters the candidate DURING pairing. No HTTP, no --external-url.
// This is the P2P replacement for the bearer-invite (invitations.js) + the
// out-of-band enroll that previously rode HTTP.

import test from 'brittle'
import Corestore from 'corestore'
import Hyperswarm from 'hyperswarm'
import createTestnet from '@hyperswarm/testnet'
import { mkdtempSync } from 'fs'
import { join } from 'path'
import { tmpdir } from 'os'

import { Room, jsonSchema } from '../index.js'
import { createRoomPairing, joinRoomPairing, resumeRoomPairing } from '../lib/room-pairing.js'

let n = 0
const tmp = () => mkdtempSync(join(tmpdir(), 'rp-' + (n++) + '-'))

const schema = jsonSchema({
  async apply (batch, op) {
    if (op.type === 'put') await batch.put('k:' + op.id, { id: op.id, by: op.by })
  },
  validate (op) {
    if (op.type === 'put' && typeof op.id === 'undefined') throw new Error('put requires id')
  }
})

async function waitFor (pred, ms = 25000) {
  const t0 = Date.now()
  while (Date.now() - t0 < ms) {
    try { if (await pred()) return true } catch {}
    await new Promise((r) => setTimeout(r, 100))
  }
  return false
}

async function makeNode (bootstrap) {
  const store = new Corestore(tmp())
  const swarm = new Hyperswarm({ bootstrap })
  swarm.on('connection', (conn) => store.replicate(conn))
  return { store, swarm }
}

test('contributor pairs over the swarm and becomes a writer (no HTTP)', async (t) => {
  const testnet = await createTestnet(3, t.teardown)

  // Host owns the room (creator → writable).
  const h = await makeNode(testnet.bootstrap)
  const host = new Room(h.store, { schema, namespace: 'room/test' })
  await host.ready()
  t.teardown(async () => { try { await host.close() } catch {}; try { await h.swarm.destroy() } catch {} })
  t.ok(host.writable, 'host (creator) is writable')
  await host.append({ type: 'put', id: 1, by: 'host' })

  const pairing = createRoomPairing(host, { role: 'contributor', swarm: h.swarm })
  t.teardown(async () => { try { await pairing.close() } catch {} })
  t.ok(typeof pairing.invite === 'string' && pairing.invite.startsWith('roompair1:'), 'invite code minted')

  // Candidate joins via the invite over the DHT.
  const c = await makeNode(testnet.bootstrap)
  t.teardown(async () => { try { await c.swarm.destroy() } catch {} })
  const { room: cand } = await joinRoomPairing(pairing.invite, { corestore: c.store, swarm: c.swarm, schema })
  t.teardown(async () => { try { await cand.close() } catch {} })

  t.is(cand.keyHex, host.keyHex, 'candidate mounted the SAME room autobase (not a sibling)')

  const becameWritable = await waitFor(() => cand.writable)
  t.ok(becameWritable, 'candidate became a writer via pairing — over the swarm, no HTTP')

  // Candidate writes; host sees it (bidirectional).
  const id = Date.now()
  await cand.append({ type: 'put', id, by: 'cand' })
  const seen = await waitFor(async () => {
    await host.base.update()
    const v = await host.view.get('k:' + id)
    return !!(v && v.value && v.value.by === 'cand')
  })
  t.ok(seen, 'host sees the candidate-authored op (bidirectional writes)')
})

test('reader pairs over the swarm and stays a read-only mirror', async (t) => {
  const testnet = await createTestnet(3, t.teardown)

  const h = await makeNode(testnet.bootstrap)
  const host = new Room(h.store, { schema, namespace: 'room/test' })
  await host.ready()
  t.teardown(async () => { try { await host.close() } catch {}; try { await h.swarm.destroy() } catch {} })
  await host.append({ type: 'put', id: 7, by: 'host' })

  const pairing = createRoomPairing(host, { role: 'reader', swarm: h.swarm })
  t.teardown(async () => { try { await pairing.close() } catch {} })

  const c = await makeNode(testnet.bootstrap)
  t.teardown(async () => { try { await c.swarm.destroy() } catch {} })
  const { room: cand, role } = await joinRoomPairing(pairing.invite, { corestore: c.store, swarm: c.swarm, schema })
  t.teardown(async () => { try { await cand.close() } catch {} })

  t.is(role, 'reader', 'role echoed as reader')
  t.is(cand.keyHex, host.keyHex, 'reader mounted the same room autobase')

  // Reader replicates (read) the host's op...
  const sawRead = await waitFor(async () => {
    await cand.base.update()
    const v = await cand.view.get('k:7')
    return !!(v && v.value && v.value.by === 'host')
  })
  t.ok(sawRead, 'reader replicates the room read-only')

  // ...but is NOT a writer.
  await new Promise((r) => setTimeout(r, 1500))
  t.absent(cand.writable, 'reader is NOT enrolled as a writer (read-only mirror)')
})

test('resumeRoomPairing re-registers a persisted invite (survives owner restart)', async (t) => {
  const testnet = await createTestnet(3, t.teardown)
  const h = await makeNode(testnet.bootstrap)
  const host = new Room(h.store, { schema, namespace: 'room/test' })
  await host.ready()
  t.teardown(async () => { try { await host.close() } catch {}; try { await h.swarm.destroy() } catch {} })
  await host.append({ type: 'put', id: 1, by: 'host' })

  // Mint an invite, capture the persistable descriptor, then CLOSE the pairing
  // (simulating an owner restart — the in-memory member is gone).
  const pairing = createRoomPairing(host, { role: 'contributor', swarm: h.swarm })
  const invite = pairing.invite
  const descriptor = pairing.descriptor
  t.ok(descriptor && descriptor.discoveryKey && descriptor.seed, 'descriptor is persistable')
  await pairing.close()

  // Re-register from the persisted descriptor (as boot would after restart).
  const resumed = resumeRoomPairing(host, descriptor, h.swarm)
  t.teardown(async () => { try { await resumed.close() } catch {} })

  // A candidate using the ORIGINAL (already-shared) invite still pairs + writes.
  const c = await makeNode(testnet.bootstrap)
  t.teardown(async () => { try { await c.swarm.destroy() } catch {} })
  const { room: cand } = await joinRoomPairing(invite, { corestore: c.store, swarm: c.swarm, schema })
  t.teardown(async () => { try { await cand.close() } catch {} })
  t.is(cand.keyHex, host.keyHex, 'candidate mounted the same room')
  const became = await waitFor(() => cand.writable)
  t.ok(became, 'candidate became a writer via the RESUMED pairing — persisted invite still works')
})