| … | … | @@ -0,0 +1,66 @@ |
| 1 | import test from 'brittle' |
| 2 | import Protomux from 'protomux' |
| 3 | import crypto from 'hypercore-crypto' |
| 4 | import { Duplex } from 'streamx' |
| 5 | import { tmpStore, tmpDir, fs, path } from './helpers.js' |
| 6 | import { HyperOverlayV2 } from '../lib/overlay-v2.js' |
| 7 | |
| 8 | // Phase 2 mirror primitive: a forge backend (no working tree) replicates a |
| 9 | // repo's large files by FETCHING each by oid and storing it CONTENT-ADDRESSED at |
| 10 | // <mirrorSpool>/<oid>. Combined with the 0.2.8 content-addressed serve fallback |
| 11 | // (<syncBaseDir>/<oid>), the backend then durably re-serves them — becoming a |
| 12 | // second source. This is the building block the pgh-sidecar overlay mirror uses. |
| 13 | |
| 14 | function makeDuplex () { |
| 15 | let aWrite, bWrite |
| 16 | const a = new Duplex({ write (d, cb) { bWrite(d); cb() }, read () {} }) |
| 17 | const b = new Duplex({ write (d, cb) { aWrite(d); cb() }, read () {} }) |
| 18 | aWrite = (d) => a.push(d) |
| 19 | bWrite = (d) => b.push(d) |
| 20 | return [a, b] |
| 21 | } |
| 22 | const settle = (ms = 400) => new Promise(r => setTimeout(r, ms)) |
| 23 | |
| 24 | test('mirror fetch stores the blob content-addressed at <mirrorSpool>/<oid> and re-serves it', async (t) => { |
| 25 | // Publisher serves from its content-addressed spool. |
| 26 | const pubSpool = tmpDir('ms-pub-spool') |
| 27 | const content = crypto.randomBytes(256 * 1024) |
| 28 | const oid = crypto.data(content).toString('hex') |
| 29 | fs.writeFileSync(path.join(pubSpool, oid), content) |
| 30 | const pub = new HyperOverlayV2(tmpStore('ms-pub'), { namespace: 'overlay-v2', syncBaseDirs: [pubSpool], destDir: tmpDir('ms-pub-dest') }) |
| 31 | |
| 32 | // Mirror has its own content-addressed mirror-spool (no working tree). |
| 33 | const mirrorSpool = tmpDir('ms-mirror-spool') |
| 34 | const mir = new HyperOverlayV2(tmpStore('ms-mir'), { namespace: 'overlay-v2', syncBaseDirs: [mirrorSpool], destDir: tmpDir('ms-mir-dest') }) |
| 35 | |
| 36 | await pub.ready(); await mir.ready() |
| 37 | t.teardown(async () => { try { await pub.close() } catch {}; try { await mir.close() } catch {} }) |
| 38 | |
| 39 | const [sa, sb] = makeDuplex() |
| 40 | pub.attachProtocol(Protomux.from(sa)) |
| 41 | mir.attachProtocol(Protomux.from(sb)) |
| 42 | await settle() |
| 43 | |
| 44 | const got = await mir.fetchFile(oid, { mirrorSpool, timeout: 6000 }) |
| 45 | t.ok(got, 'mirror fetched the file from the publisher') |
| 46 | if (got) t.is(Buffer.compare(got.data, content), 0, 'fetched bytes match') |
| 47 | |
| 48 | // KEY: the blob is stored content-addressed in the mirror-spool (durable, |
| 49 | // restart-survivable, re-servable via the 0.2.8 <syncBaseDir>/<oid> fallback). |
| 50 | const spoolPath = path.join(mirrorSpool, oid) |
| 51 | t.ok(fs.existsSync(spoolPath), 'blob stored at <mirrorSpool>/<oid>') |
| 52 | if (fs.existsSync(spoolPath)) t.is(Buffer.compare(fs.readFileSync(spoolPath), content), 0, 'spool bytes match') |
| 53 | |
| 54 | // The mirror is now a SECOND source: a cold third node (only the mirror's |
| 55 | // spool, empty maps) re-serves it to a fresh consumer. |
| 56 | const reserver = new HyperOverlayV2(tmpStore('ms-re'), { namespace: 'overlay-v2', syncBaseDirs: [mirrorSpool], destDir: tmpDir('ms-re-dest') }) |
| 57 | const con2 = new HyperOverlayV2(tmpStore('ms-con2'), { namespace: 'overlay-v2', destDir: tmpDir('ms-con2-dest') }) |
| 58 | await reserver.ready(); await con2.ready() |
| 59 | t.teardown(async () => { try { await reserver.close() } catch {}; try { await con2.close() } catch {} }) |
| 60 | const [sc, sd] = makeDuplex() |
| 61 | reserver.attachProtocol(Protomux.from(sc)) |
| 62 | con2.attachProtocol(Protomux.from(sd)) |
| 63 | await settle() |
| 64 | const got2 = await con2.fetchFile(oid, { timeout: 6000 }) |
| 65 | t.ok(got2, 'a cold re-server (mirror-spool only) serves the mirrored blob onward') |
| 66 | }) |