// Bind the adapter to hyper-overlay's FileIndex.
//
// The adapter needs contentHash -> (file, offset, length). FileIndex stores the
// other direction: path -> chunks[{hash, offset, length}] (`chunkmap:<path>`) and
// contentHash -> the same list (`chunkmap-oid:<hash>`). There is no reverse index,
// so we build one by reading the chunk maps of the paths we intend to publish.
//
// Two byte sources, matching how the overlay actually stores things:
// * working tree — bytes at <root>/<path>, chunk map from getChunkMap(path)
// * content spool — bytes at <syncBaseDir>/<oid> (a mirror has no working tree),
// chunk map from getChunkMapByHash(oid)
//
// `index` is duck-typed: anything with getChunkMap / getChunkMapByHash works, so
// this file does not import hyper-overlay (ESM) into a CJS module.
// Dual-runtime: pgv3 and pgh-sidecar run under Bare, the tests and CLI under Node.
// Static requires on both branches so bundlers can still see the dependency.
const { isBare } = require('which-runtime')
const fs = isBare ? require('bare-fs') : require('fs')
const path = isBare ? require('bare-path') : require('path')
const { ContentStore } = require('./storage.cjs')
class OverlayContentStore extends ContentStore {
constructor (index, { roots = [], syncBaseDirs = [] } = {}) {
super()
this.index = index
this.roots = Array.isArray(roots) ? roots : [roots]
this.syncBaseDirs = Array.isArray(syncBaseDirs) ? syncBaseDirs : [syncBaseDirs]
this.indexed = new Set()
}
_locate (dirs, rel) {
for (const dir of dirs) {
const full = path.join(dir, rel)
try {
if (fs.statSync(full).isFile()) return full
} catch {}
}
return null
}
// Register one working-tree path. Returns how many chunks it contributed.
async indexPath (rel) {
if (this.indexed.has('p:' + rel)) return 0
const chunks = await this.index.getChunkMap(rel)
if (!chunks || chunks.length === 0) return 0
const file = this._locate(this.roots, rel)
if (file === null) return 0
for (const c of chunks) this.add(c.hash, file, c.offset, c.length)
this.indexed.add('p:' + rel)
return chunks.length
}
// Register one content-addressed blob from the spool (mirror nodes).
async indexContent (oid) {
if (this.indexed.has('o:' + oid)) return 0
const chunks = await this.index.getChunkMapByHash(oid)
if (!chunks || chunks.length === 0) return 0
const file = this._locate(this.syncBaseDirs, oid)
if (file === null) return 0
for (const c of chunks) this.add(c.hash, file, c.offset, c.length)
this.indexed.add('o:' + oid)
return chunks.length
}
// Register everything the index knows about, working tree first then spool.
// O(total chunks) — fine for a publish, not something to do on every boot.
async indexAll (dir = '') {
const files = await this.index.listFiles(dir)
let chunks = 0
let paths = 0
let blobs = 0
for (const f of files) {
const n = await this.indexPath(f.path)
if (n > 0) {
chunks += n
paths++
continue
}
if (f.contentHash) {
const m = await this.indexContent(f.contentHash)
if (m > 0) {
chunks += m
blobs++
}
}
}
return { files: files.length, paths, blobs, chunks }
}
}
module.exports = { OverlayContentStore }