OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-overlay / test / overlay-v2-mirror-spool.test.js

67 lines · javascript ref:
import test from 'brittle'
import Protomux from 'protomux'
import crypto from 'hypercore-crypto'
import { Duplex } from 'streamx'
import { tmpStore, tmpDir, fs, path } from './helpers.js'
import { HyperOverlay } from '../lib/overlay.js'

// Phase 2 mirror primitive: a forge backend (no working tree) replicates a
// repo's large files by FETCHING each by oid and storing it CONTENT-ADDRESSED at
// <mirrorSpool>/<oid>. Combined with the 0.2.8 content-addressed serve fallback
// (<syncBaseDir>/<oid>), the backend then durably re-serves them — becoming a
// second source. This is the building block the pgh-sidecar overlay mirror uses.

function makeDuplex () {
  let aWrite, bWrite
  const a = new Duplex({ write (d, cb) { bWrite(d); cb() }, read () {} })
  const b = new Duplex({ write (d, cb) { aWrite(d); cb() }, read () {} })
  aWrite = (d) => a.push(d)
  bWrite = (d) => b.push(d)
  return [a, b]
}
const settle = (ms = 400) => new Promise(r => setTimeout(r, ms))

test('mirror fetch stores the blob content-addressed at <mirrorSpool>/<oid> and re-serves it', async (t) => {
  // Publisher serves from its content-addressed spool.
  const pubSpool = tmpDir('ms-pub-spool')
  const content = crypto.randomBytes(256 * 1024)
  const oid = crypto.data(content).toString('hex')
  fs.writeFileSync(path.join(pubSpool, oid), content)
  const pub = new HyperOverlay(tmpStore('ms-pub'), { namespace: 'overlay-v2', syncBaseDirs: [pubSpool], destDir: tmpDir('ms-pub-dest') })

  // Mirror has its own content-addressed mirror-spool (no working tree).
  const mirrorSpool = tmpDir('ms-mirror-spool')
  const mir = new HyperOverlay(tmpStore('ms-mir'), { namespace: 'overlay-v2', syncBaseDirs: [mirrorSpool], destDir: tmpDir('ms-mir-dest') })

  await pub.ready(); await mir.ready()
  t.teardown(async () => { try { await pub.close() } catch {}; try { await mir.close() } catch {} })

  const [sa, sb] = makeDuplex()
  pub.attachProtocol(Protomux.from(sa))
  mir.attachProtocol(Protomux.from(sb))
  await settle()

  const got = await mir.fetchFile(oid, { mirrorSpool, timeout: 6000 })
  t.ok(got, 'mirror fetched the file from the publisher')
  if (got) t.is(Buffer.compare(fs.readFileSync(got.destPath), content), 0, 'fetched bytes match')

  // KEY: the blob is stored content-addressed in the mirror-spool (durable,
  // restart-survivable, re-servable via the 0.2.8 <syncBaseDir>/<oid> fallback).
  const spoolPath = path.join(mirrorSpool, oid)
  t.ok(fs.existsSync(spoolPath), 'blob stored at <mirrorSpool>/<oid>')
  if (fs.existsSync(spoolPath)) t.is(Buffer.compare(fs.readFileSync(spoolPath), content), 0, 'spool bytes match')

  // The mirror is now a SECOND source: a cold third node (only the mirror's
  // spool, empty maps) re-serves it to a fresh consumer.
  const reserver = new HyperOverlay(tmpStore('ms-re'), { namespace: 'overlay-v2', syncBaseDirs: [mirrorSpool], destDir: tmpDir('ms-re-dest') })
  const con2 = new HyperOverlay(tmpStore('ms-con2'), { namespace: 'overlay-v2', destDir: tmpDir('ms-con2-dest') })
  await reserver.ready(); await con2.ready()
  t.teardown(async () => { try { await reserver.close() } catch {}; try { await con2.close() } catch {} })
  const [sc, sd] = makeDuplex()
  reserver.attachProtocol(Protomux.from(sc))
  con2.attachProtocol(Protomux.from(sd))
  await settle()
  const got2 = await con2.fetchFile(oid, { timeout: 6000 })
  t.ok(got2, 'a cold re-server (mirror-spool only) serves the mirrored blob onward')
})