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')
})