OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / lib / services / deploy.js

97 lines · javascript ref:
/**
 * Deploy service — managed wrapper around hyper-deploy's DeployServer.
 *
 * hyper-deploy owns its own lifecycle (ready/close) and can accept an
 * injected Hyperswarm. The orchestrator passes its existing swarm so we
 * don't spin up a second DHT node. The release directory at
 * `<storage>/releases/` is the source of truth — files placed there are
 * indexed and served to peers.
 *
 * Only started when the orchestrator has swarm enabled (opts.swarm is set).
 * Without swarm, hyper-deploy has nothing to serve to.
 */

import { DeployServer } from 'hyper-deploy'
import { mkdir } from '#node:fs/promises'
import { readdirSync, statSync } from '#node:fs'
import { join } from '#node:path'

export async function startDeployService (opts) {
  const { releaseDir, swarm, corestore, identity } = opts
  if (!releaseDir) throw new Error('deploy service requires releaseDir')
  if (!swarm) throw new Error('deploy service requires a Hyperswarm instance')

  await mkdir(releaseDir, { recursive: true })

  const server = new DeployServer(releaseDir, {
    swarm,
    corestore: corestore || undefined,
    topic: identity ? identity.publicKey : undefined
  })

  // DeployServer re-emits connection errors (ECONNRESET etc.) as 'error'
  // events. These are routine in P2P and must not crash the daemon.
  // Register BEFORE ready() because the constructor auto-starts.
  server.on('error', (err) => {
    // eslint-disable-next-line no-console
    if (err.code !== 'ECONNRESET') console.error('[deploy]', err.message)
  })

  await server.ready()

  return server
}

/**
 * RPC handlers for the deploy service.
 * Read-only — release management happens via the filesystem + CI, not RPC.
 */
export function deployHandlers (svc) {
  return [
    {
      name: 'list-releases',
      fn: async () => {
        const deploy = svc._deploy
        if (!deploy) return { files: [], active: false }
        return {
          active: true,
          topic: deploy.topicHex,
          connections: deploy.connections,
          files: deploy.manifest ? deploy.manifest.files : [],
          totalSize: deploy.manifest ? deploy.manifest.totalSize : 0
        }
      }
    },

    {
      name: 'release-status',
      fn: async () => {
        const deploy = svc._deploy
        if (!deploy) return { active: false }
        return {
          active: true,
          topic: deploy.topicHex,
          connections: deploy.connections,
          stats: deploy.stats,
          fileCount: deploy.manifest ? deploy.manifest.files.length : 0,
          totalSize: deploy.manifest ? deploy.manifest.totalSize : 0
        }
      }
    },

    {
      name: 'rescan-releases',
      fn: async (ctx) => {
        // requireWriter equivalent — UDS or trusted HTTP
        if (ctx.transport !== 'uds' && (!ctx.identity || !ctx.identity.publicKey)) {
          throw Object.assign(new Error('authentication required'), { code: 'EAUTH', status: 401 })
        }
        const deploy = svc._deploy
        if (!deploy) throw new Error('deploy service not active (no swarm)')
        const result = await deploy.rescan()
        return result
      }
    }
  ]
}