/**
* 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)
}
}