OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-vdrive / lib / overlay-content-store.cjs

93 lines · javascript ref:
// 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 }