OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-overlay commits / ecf6c01

overlay-v2: re-seed fetched blobs (seeding multiplication)
After a verified cross-peer fetch, fetchFile registers the blob locally so the node now serves those chunks to other peers. A file held by N peers gets faster as more peers fetch it (torrent-style) — and enables cross-machine multi-source. Best-effort, idempotent-safe (skips if already registered). Test proves a node connected only to a former fetcher can pull the blob from it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ecf6c01ec591743f597e72d0046cfdf2c5371fba
by plankatron <fleeky@gmail.com> · 2026-05-28T22:27:29.000Z

Changed files (2) +46 -0

lib/overlay-v2.js +10 -0
……@@ -197,6 +197,16 @@ export class HyperOverlayV2 extends ReadyResource {
197197 err.code = 'EHASHMISMATCH'
198198 throw err
199199 }
200 // Seeding multiplication: register the verified blob locally so this node
201 // now SERVES these chunks to other peers too. A file held by N peers then
202 // gets faster as more peers fetch it (torrent-style). Best-effort — the
203 // fetch still succeeds even if re-registration fails. Skipped if a peer
204 // raced us to register the same hash (registerFile is idempotent-safe).
205 if (!this._contentHashPaths.has(contentHash)) {
206 try {
207 await this.registerFile('/attachments/' + contentHash, destPath, { contentHash, size: data.length })
208 } catch {}
209 }
200210 return { data, destPath, local: false }
201211 }
202212
test/overlay-v2-fetch.test.js +36 -0
……@@ -96,6 +96,42 @@ test('V2 multi-source: chunks fetched from MULTIPLE peers in parallel', async (t
9696 'chunks were pulled from BOTH seeders (multi-source) — peerCount=' + stats.peerCount)
9797})
9898
99test('V2 re-seed: a fetcher becomes a seeder (multiplication)', async (t) => {
100 const S1 = await makePeer('seed')
101 const F = await makePeer('fetcher')
102 const G = await makePeer('late')
103 t.teardown(async () => {
104 for (const ov of [S1, F, G]) { try { await ov.close() } catch {} }
105 })
106
107 // S1 holds the file.
108 const content = crypto.randomBytes(150 * 1024)
109 const oid = crypto.data(content).toString('hex')
110 const dir = tmpDir('ov2-seed-files')
111 const f = path.join(dir, 'x.bin')
112 fs.writeFileSync(f, content)
113 await S1.registerFile('/attachments/' + oid, f, { contentHash: oid, size: content.length })
114
115 // F connects ONLY to S1 and fetches → should re-seed.
116 const [fa, sa] = makeDuplex()
117 F.attachProtocol(Protomux.from(fa)); S1.attachProtocol(Protomux.from(sa))
118 await new Promise(r => setTimeout(r, 200))
119 const got1 = await F.fetchFile(oid)
120 t.ok(got1 && Buffer.compare(got1.data, content) === 0, 'F fetched the blob from S1')
121
122 // After fetching, F serves it from its own local copy.
123 const localOnF = await F.fetchFile(oid)
124 t.ok(localOnF && localOnF.local, 'F re-seeded the blob (now serves it locally)')
125
126 // G connects ONLY to F (never to S1) — it can only succeed if F is now a seeder.
127 const [gf, fg] = makeDuplex()
128 G.attachProtocol(Protomux.from(gf)); F.attachProtocol(Protomux.from(fg))
129 await new Promise(r => setTimeout(r, 200))
130 const got2 = await G.fetchFile(oid)
131 t.ok(got2 && !got2.local, 'G fetched from a peer (F), not local')
132 t.is(got2 && Buffer.compare(got2.data, content), 0, 'G got the blob — F successfully re-seeded it')
133})
134
99135test('V2 facade: fetch by unknown hash with no peers returns null', async (t) => {
100136 const solo = await makePeer('solo')
101137 t.teardown(async () => { try { await solo.close() } catch {} })