OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / lib / backends / hyperdrive.js

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