import Hyperdrive from 'hyperdrive'
import { RepositoryBackend } from './base.js'
/**
* Hyperdrive Backend
*
* Wraps DirectGitBackend for read operations but also mirrors repo content
* into a Hyperdrive, enabling pear:// URLs for P2P content access.
*
* The Hyperdrive stores a snapshot of the repo's files for the default branch,
* making the repo accessible via standard pear:// URLs.
*
* Uses a dedicated Hyperswarm for corestore replication, injected from the server.
* This swarm is separate from the git P2P swarm to avoid protocol conflicts.
*/
export class HyperdriveBackend extends RepositoryBackend {
constructor (options = {}) {
super()
this.corestore = options.corestore
this.gitBrowser = options.gitBrowser
this.swarm = options.swarm || null // Shared Hyperswarm
this.drives = new Map() // repoName -> Hyperdrive
this.seeding = new Map() // repoName -> discovery
}
getStrategy () {
return 'hyperdrive'
}
getDescription () {
return 'Git repos mirrored to Hyperdrive for pear:// content access'
}
/**
* Ensure swarm is available for drive replication
*/
async initSwarm () {
if (!this.swarm) {
throw new Error('HyperdriveBackend requires a shared Hyperswarm instance')
}
}
/**
* Seed a drive on Hyperswarm
*/
async seedDrive (repoName, drive) {
await this.initSwarm()
if (this.seeding.has(repoName)) return
const discovery = this.swarm.join(drive.discoveryKey, { server: true, client: false })
await discovery.flushed()
this.seeding.set(repoName, discovery)
console.log(`[Hyperdrive] Seeding ${repoName}: ${drive.key.toString('hex').slice(0, 16)}...`)
}
/**
* Seed all repos that use hyperdrive strategy
* Called on server startup
*/
async seedExistingDrives (repoNames) {
let seeded = 0
for (const repoName of repoNames) {
const drive = await this.getDrive(repoName)
// Only seed drives that have content (key exists = drive was initialized)
if (drive.key) {
await this.seedDrive(repoName, drive)
seeded++
}
}
if (seeded > 0) {
console.log(`[Hyperdrive] Seeding ${seeded} drives on Hyperswarm`)
}
}
/**
* Get or create Hyperdrive for a repo
*/
async getDrive (repoName) {
if (this.drives.has(repoName)) return this.drives.get(repoName)
const drive = new Hyperdrive(this.corestore, { name: `repo-${repoName}` })
await drive.ready()
this.drives.set(repoName, drive)
return drive
}
/**
* Import/mirror a git repo into its Hyperdrive
* Returns the driveKey for pear-link URLs
*/
async importRepo (repoName) {
const drive = await this.getDrive(repoName)
// Get files from git (default branch)
const files = await this.gitBrowser.listFiles(repoName, 'HEAD', '')
let imported = 0
for (const file of files) {
if (file.type === 'directory' || file.type === 'submodule') continue
try {
const content = await this.gitBrowser.readFile(repoName, file.path, 'HEAD')
if (content !== null && content !== undefined) {
const buf = typeof content === 'string' ? Buffer.from(content) : content
await drive.put('/' + file.path, buf)
imported++
}
} catch (err) {
// Skip files that can't be read (binary, too large, etc.)
}
}
// Also store README at root for easy discovery
try {
const readme = await this.gitBrowser.getReadme(repoName, 'HEAD')
if (readme && readme.content) {
const name = readme.isMarkdown ? 'README.md' : 'README'
await drive.put('/' + name, Buffer.from(readme.content))
}
} catch (err) {
// README is optional
}
const driveKey = drive.key.toString('hex')
console.log(`[Hyperdrive] Imported ${repoName}: ${imported} files, key=${driveKey.slice(0, 16)}...`)
// Seed the drive so peers can resolve pear:// URLs
await this.seedDrive(repoName, drive)
return {
driveKey,
discoveryKey: drive.discoveryKey.toString('hex'),
filesImported: imported
}
}
// Read operations delegate to gitBrowser (authoritative source)
async getRepoInfo (repoName) {
const stats = await this.getStats(repoName)
const drive = await this.getDrive(repoName)
return {
name: repoName,
driveKey: drive.key.toString('hex'),
...stats
}
}
async listFiles (repoName, ref, path) {
return this.gitBrowser.listFiles(repoName, ref, path)
}
async getCommits (repoName, ref, limit) {
return this.gitBrowser.getCommits(repoName, ref, limit)
}
async readFile (repoName, filePath, ref) {
return this.gitBrowser.readFile(repoName, filePath, ref)
}
async getBranches (repoName) {
return this.gitBrowser.getBranches(repoName)
}
async getTags (repoName) {
return this.gitBrowser.getTags(repoName)
}
async getStats (repoName) {
return this.gitBrowser.getStats(repoName)
}
async getReadme (repoName, ref) {
return this.gitBrowser.getReadme(repoName, ref)
}
async getSubmodules (repoName, ref) {
return this.gitBrowser.getSubmodules(repoName, ref)
}
async getSubmoduleInfo (repoName, submodulePath, ref) {
return this.gitBrowser.getSubmoduleInfo(repoName, submodulePath, ref)
}
async getCommitDiff (repoName, commitHash) {
return this.gitBrowser.getCommitDiff(repoName, commitHash)
}
async getFileAtCommit (repoName, filePath, commitHash) {
return this.gitBrowser.getFileAtCommit(repoName, filePath, commitHash)
}
parseRepoNameFromURL (url) {
return this.gitBrowser.parseRepoNameFromURL(url)
}
// P2P Support - Hyperdrive supports replication
canReplicate () {
return true
}
async getReplicationKey (repoName) {
const drive = await this.getDrive(repoName)
return drive.key.toString('hex')
}
async getDriveKey (repoName) {
const drive = await this.getDrive(repoName)
return drive.key.toString('hex')
}
async getStorageSize (repoName) {
// TODO: calculate actual Hyperdrive storage
return 0
}
}