OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-overlay commits / 6cac8ee

0.2.9: fetchFile mirrorSpool option — store fetched blob content-addressed
Phase 2 overlay-mirror primitive. opts.mirrorSpool makes fetchFile store the verified blob at <mirrorSpool>/<contentHash> instead of a throwaway destDir name. A forge backend (no working tree) can thus fetch a repo's large files into a content-addressed spool and durably re-serve them via the 0.2.8 <syncBaseDir>/<contentHash> resolution — becoming a second source (v2 multi-source). test/overlay-v2-mirror-spool.test.js: mirror fetch lands the blob at <mirrorSpool>/<oid>; a cold re-server (mirror-spool only) serves it onward. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6cac8ee5c184f7bb51f0daef3632301f6624c3a8
by plankatron <fleeky@gmail.com> · 2026-06-08T00:51:33.000Z

Changed files (3) +84 -5

lib/overlay-v2.js +17 -4
……@@ -174,10 +174,23 @@ export class HyperOverlayV2 extends ReadyResource {
174174 // Multi-source: fetch chunks in parallel from ALL connected peers that
175175 // have the file (torrent-style). The scheduler dedups + fails over.
176176 const peers = [...this._protocol._peers.values()]
177 const destPath = path.join(
178 this._destDir,
179 'blob-' + contentHash + '-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8)
180 )
177 // opts.mirrorSpool: store the verified blob CONTENT-ADDRESSED at
178 // <mirrorSpool>/<contentHash> rather than a throwaway destDir name. This is
179 // the overlay-mirror path — a forge backend with no working tree fetches a
180 // repo's large files into a content-addressed spool so it durably re-serves
181 // them (via the <syncBaseDir>/<contentHash> resolution in _onContentRequest).
182 let destPath
183 if (opts.destPath) {
184 destPath = opts.destPath
185 } else if (opts.mirrorSpool) {
186 try { fs.mkdirSync(opts.mirrorSpool, { recursive: true }) } catch {}
187 destPath = path.join(opts.mirrorSpool, contentHash)
188 } else {
189 destPath = path.join(
190 this._destDir,
191 'blob-' + contentHash + '-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8)
192 )
193 }
181194 try {
182195 await this._protocol.fetchContent(contentHash, peers, {
183196 destPath,
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "hyper-overlay",
3 "version": "0.2.8",
3 "version": "0.2.9",
44 "description": "Content-addressed union filesystem — P2P file distribution with FastCDC chunking and zero duplication",
55 "type": "module",
66 "main": "index.js",
test/overlay-v2-mirror-spool.test.js +66 -0
……@@ -0,0 +1,66 @@
1import test from 'brittle'
2import Protomux from 'protomux'
3import crypto from 'hypercore-crypto'
4import { Duplex } from 'streamx'
5import { tmpStore, tmpDir, fs, path } from './helpers.js'
6import { 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
14function 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}
22const settle = (ms = 400) => new Promise(r => setTimeout(r, ms))
23
24test('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})