OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / lib / services / vdrive-seeder.js

151 lines · javascript ref:
/**
 * vdrive-seeder — announce each repo's virtual Hyperdrive on the daemon's swarm.
 *
 * A drive that is written but never announced is unreachable: the pear:// link
 * the CI hook prints resolves to nothing. This closes that.
 *
 * We seed ourselves rather than shelling out to `pear seed`, and that is
 * structural rather than preference. `pear seed` opens the drive by key in
 * PEAR's own corestore and downloads it (`ops/seed.js`), which would
 * materialise a full second copy of every byte — precisely the copy a virtual
 * drive exists to avoid. It is a re-seeder, not a source.
 *
 * Doing it in the daemon also gets things `pear seed` does not have: one swarm
 * for every repo rather than a process per link, supervision under `pgh app`,
 * re-announcement on boot with no operator, and serving through the redirect so
 * a whole release history costs its pointer store rather than its bytes.
 *
 * Failures are per-repo and non-fatal — a drive that will not open must never
 * take down the daemon or stop the other drives being served.
 */

import fsp from '#node:fs/promises'
import path from '#node:path'

import { vdriveStoreDir } from '../vdrive-publish.js'

export class VdriveSeeder {
  constructor (opts = {}) {
    this._releaseDir = opts.releaseDir || null
    // wire/unwire come from the daemon: a vdrive lives in its OWN corestore,
    // outside the main corestore.replicate, so it needs the same three steps
    // as a resync side-store — mount (future connections), replicate on the
    // existing ones, and join the topic. svc.wireVdrive does that.
    this._wire = typeof opts.wire === 'function' ? opts.wire : null
    this._unwire = typeof opts.unwire === 'function' ? opts.unwire : null
    this._log = opts.log || { info () {}, warn () {} }
    this._drives = new Map() // repo -> { opened, discovery }
    this._openDrive = null
    this._closeDrive = null
    this._announce = null
  }

  get enabled () {
    return !!(this._releaseDir && this._wire)
  }

  get count () {
    return this._drives.size
  }

  async _load () {
    if (this._openDrive) return true
    try {
      const mod = await import('hyper-vdrive/serve')
      const m = mod.openDrive ? mod : mod.default
      this._openDrive = m.openDrive
      this._closeDrive = m.closeDrive
      return true
    } catch (err) {
      this._log.warn('[vdrive] unavailable, not seeding: ' + (err && err.message))
      return false
    }
  }

  /** Announce one repo's drive. Idempotent — re-announcing reopens it. */
  async announceRepo (repo) {
    if (!this.enabled) return null
    if (!(await this._load())) return null

    const storeDir = vdriveStoreDir(this._releaseDir, repo)
    try {
      await fsp.stat(storeDir)
    } catch {
      return null // repo has no drive — not an error, most repos do not
    }

    await this.dropRepo(repo)

    try {
      const opened = await this._openDrive({
        storeDir,
        root: path.join(this._releaseDir, repo)
      })
      this._wire(repo, opened)
      this._drives.set(repo, { opened })
      this._log.info(
        `[vdrive] seeding ${repo}: ${opened.blocks} blocks, ${opened.locations} locations -> ${opened.link}`
      )
      return { repo, link: opened.link, key: opened.key, blocks: opened.blocks }
    } catch (err) {
      this._log.warn('[vdrive] seed ' + repo + ' failed: ' + (err && err.message))
      return null
    }
  }

  async dropRepo (repo) {
    const entry = this._drives.get(repo)
    if (!entry) return
    this._drives.delete(repo)
    try {
      if (this._unwire) await this._unwire(repo, entry.opened)
    } catch {}
    if (this._closeDrive) await this._closeDrive(entry.opened)
  }

  /** Announce every repo that already has a drive. Safe to call on boot. */
  async start () {
    if (!this.enabled) return { seeded: 0 }
    let names = []
    try {
      names = (await fsp.readdir(this._releaseDir, { withFileTypes: true }))
        .filter((e) => e.isDirectory())
        .map((e) => e.name)
    } catch {
      return { seeded: 0 }
    }

    let seeded = 0
    for (const repo of names) {
      const res = await this.announceRepo(repo)
      if (res) seeded++
    }
    if (seeded > 0) this._log.info(`[vdrive] seeding ${seeded} drive${seeded === 1 ? '' : 's'}`)
    return { seeded }
  }

  /** What `pear seed` shows as stats, per drive. */
  stats () {
    const out = []
    for (const [repo, { opened }] of this._drives) {
      let peers = 0
      try {
        peers = opened.drive.core.peers.length
      } catch {}
      out.push({
        repo,
        link: opened.link,
        key: opened.key,
        blocks: opened.blocks,
        locations: opened.locations,
        peers
      })
    }
    return out
  }

  async close () {
    for (const repo of [...this._drives.keys()]) await this.dropRepo(repo)
  }
}