OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-overlay commits / 596af1e

chunk-registry: cross-mode dedup (drop redundant inline cache when a local pointer exists)
ChunkRegistry kept bytes twice for one chunk hash: a working-tree `local` pointer AND a base64 `cache` copy could both sit in sources[] because the dedup key was only (type, path, offset). The peer-fetch caching path (overlay get/read/hydrate/getByHash) always inline-caches as base64 (+33%), so any chunk that also had a working-tree pointer was double-stored. Add a shared mergeSource(): never append a redundant `cache` copy when a byte-yielding source already exists, and let a durable `local` pointer supersede (drop) any inline cache copy of the same bytes. Peer routing hints carry no bytes, so they still coexist. putChunk + putChunks both use it. Tests: 3 new cases (cache-blocked-by-local, local-supersedes-cache, peer-coexists). 10/10 registry tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
596af1e5329fa16bb7deaccc106262572d555767
by plankatron <fleeky@gmail.com> · 2026-06-20T19:22:08.000Z

Changed files (3) +89 -22

lib/chunk-registry.js +41 -21
……@@ -45,14 +45,7 @@ export class ChunkRegistry extends ReadyResource {
4545
4646 if (existing) {
4747 const val = existing.value
48 // Add source if not already present
49 const hasSource = val.sources.some(s =>
50 s.type === source.type && s.path === source.path && s.offset === source.offset
51 )
52 if (!hasSource) {
53 val.sources.push(source)
54 await this._bee.put(key, val)
55 }
48 if (mergeSource(val, source)) await this._bee.put(key, val)
5649 } else {
5750 await this._bee.put(key, {
5851 size: source.size,
……@@ -78,15 +71,9 @@ export class ChunkRegistry extends ReadyResource {
7871 // (same chunk hash from different offsets) — coalesce in a Map.
7972 const merged = new Map()
8073 for (const { hash, source } of records) {
81 const cur = merged.get(hash)
82 if (cur) {
83 const has = cur.sources.some(s =>
84 s.type === source.type && s.path === source.path && s.offset === source.offset
85 )
86 if (!has) cur.sources.push(source)
87 } else {
88 merged.set(hash, { size: source.size, sources: [source] })
89 }
74 let cur = merged.get(hash)
75 if (!cur) { cur = { size: source.size, sources: [] }; merged.set(hash, cur) }
76 mergeSource(cur, source)
9077 }
9178
9279 const batch = this._bee.batch()
……@@ -97,10 +84,7 @@ export class ChunkRegistry extends ReadyResource {
9784 const cur = existing.value
9885 let changed = false
9986 for (const s of val.sources) {
100 const has = cur.sources.some(x =>
101 x.type === s.type && x.path === s.path && x.offset === s.offset
102 )
103 if (!has) { cur.sources.push(s); changed = true }
87 if (mergeSource(cur, s)) changed = true
10488 }
10589 if (changed) await batch.put(`chunk:${hash}`, cur)
10690 } else {
……@@ -221,3 +205,39 @@ export class ChunkRegistry extends ReadyResource {
221205 return { chunkCount, manifestCount, totalSize }
222206 }
223207}
208
209// A source "yields bytes" locally if reading it returns the chunk content
210// without a network round-trip: an inline `cache` copy, or a working-tree
211// `local` pointer. A bare `peer` routing hint carries no bytes.
212function byteYielding (s) {
213 return (s.type === 'cache' && s.data != null) || (s.type === 'local' && s.path != null)
214}
215
216// Merge `source` into a chunk record `val` ({ size, sources }) applying
217// cross-mode dedup. Returns true iff the record changed.
218//
219// The rules close the "same bytes stored two ways" gap (working-tree pointer
220// + base64 inline cache for one chunk hash):
221// 1. Never append a redundant inline `cache` copy when a byte-yielding
222// source already exists — that duplicates the bytes (and base64 inflates
223// them +33%). Peer routing hints don't yield bytes, so they never block.
224// 2. A durable `local` pointer supersedes any inline `cache` copy of the
225// same bytes: keep the pointer (smaller, no base64 tax, working tree is
226// the source of truth) and drop the now-redundant cache copies.
227function mergeSource (val, source) {
228 if (source.type === 'cache' && val.sources.some(byteYielding)) return false
229
230 const has = val.sources.some(s =>
231 s.type === source.type && s.path === source.path && s.offset === source.offset
232 )
233 let changed = false
234 if (!has) { val.sources.push(source); changed = true }
235
236 if (source.type === 'local' && source.path != null) {
237 const before = val.sources.length
238 val.sources = val.sources.filter(s => s.type !== 'cache')
239 if (val.sources.length !== before) changed = true
240 }
241
242 return changed
243}
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "hyper-overlay",
3 "version": "0.2.10",
3 "version": "0.2.11",
44 "description": "Content-addressed union filesystem — P2P file distribution with FastCDC chunking and zero duplication",
55 "type": "module",
66 "main": "index.js",
test/registry.test.js +47 -0
……@@ -46,6 +46,53 @@ test('putChunk adds source without duplicating', async (t) => {
4646 await registry.close()
4747})
4848
49test('cross-mode dedup: redundant cache copy not stored when local exists', async (t) => {
50 const store = tmpStore()
51 const registry = new ChunkRegistry(store)
52 await registry.ready()
53
54 await registry.putChunk('h', { size: 5, type: 'local', path: '/x', offset: 0 })
55 // Peer-fetch path would inline-cache the same bytes as base64 — must be a no-op.
56 await registry.putChunk('h', { size: 5, type: 'cache', data: Buffer.from('hello').toString('base64') })
57
58 const chunk = await registry.getChunk('h')
59 t.is(chunk.sources.length, 1, 'redundant cache copy not stored')
60 t.is(chunk.sources[0].type, 'local', 'kept the working-tree pointer')
61
62 await registry.close()
63})
64
65test('cross-mode dedup: local pointer supersedes an existing cache copy', async (t) => {
66 const store = tmpStore()
67 const registry = new ChunkRegistry(store)
68 await registry.ready()
69
70 await registry.putChunk('h', { size: 5, type: 'cache', data: Buffer.from('hello').toString('base64') })
71 t.is((await registry.getChunk('h')).sources.length, 1, 'cache copy present')
72
73 await registry.putChunk('h', { size: 5, type: 'local', path: '/x', offset: 0 })
74
75 const chunk = await registry.getChunk('h')
76 t.is(chunk.sources.length, 1, 'cache copy dropped in favor of the pointer')
77 t.is(chunk.sources[0].type, 'local', 'pointer is the surviving source')
78
79 await registry.close()
80})
81
82test('peer routing hints coexist with a byte source', async (t) => {
83 const store = tmpStore()
84 const registry = new ChunkRegistry(store)
85 await registry.ready()
86
87 await registry.putChunk('h', { size: 5, type: 'local', path: '/x', offset: 0 })
88 await registry.putChunk('h', { size: 5, type: 'peer' })
89
90 const chunk = await registry.getChunk('h')
91 t.is(chunk.sources.length, 2, 'peer hint carries no bytes, so it is kept')
92
93 await registry.close()
94})
95
4996test('deleteChunk removes chunk', async (t) => {
5097 const store = tmpStore()
5198 const registry = new ChunkRegistry(store)