OverviewFilesCommitsIssuesDocsReleasesPeers

favela-peer / index.js

97 lines · javascript ref:
// favela-peer — the daemon. Runs under Bare: `bare index.js`.
//
// pgh installs this and the launcher runs it. There is no `pear run` path and never was.

import fs from '#node:fs'
import os from '#node:os'
import { join } from '#node:path'
import net from '#node:net'
import process from '#node:process'
import Hyperswarm from 'hyperswarm'
import { FavelaNode } from './lib/node.js'
import { SwarmTransport } from './lib/transport.js'
import { createServer } from './lib/rpc.js'
import { startControl } from './lib/control.js'

const storage = process.env.FAVELA_STORAGE || join(os.homedir(), '.favela')
const sock = process.env.FAVELA_SOCK || join(storage, 'favela.sock')
// The pgh control socket (lib/control.js), separate from favela.sock: the sidecar speaks hyper-svc.
const svcSock = process.env.FAVELA_SVC_SOCK || join(storage, 'svc.sock')

function pkgVersion () {
  try { return JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url).pathname, 'utf8')).version } catch { return null }
}

// ⛔ Refuse to start if a daemon already holds the socket. Removing a live socket would leave the
// running daemon unreachable while this one looked healthy — one process, two owners, no error.
async function socketIsLive (path) {
  return new Promise((resolve) => {
    const probe = net.connect(path)
    const done = (v) => { try { probe.destroy() } catch {}; resolve(v) }
    probe.on('connect', () => done(true))
    probe.on('error', () => done(false))
    setTimeout(() => done(false), 500)
  })
}

async function main () {
  fs.mkdirSync(storage, { recursive: true })
  if (fs.existsSync(sock) && await socketIsLive(sock)) {
    console.error('favela-peer: a daemon is already listening on ' + sock)
    process.exit(1)
  }

  const swarm = new Hyperswarm()
  const node = new FavelaNode(storage, { transport: null, swarm })
  // ready() FIRST: it builds the overlay, and the transport needs it — the bytes ride the same
  // connections as the log, so a transport without the overlay replicates half the system.
  await node.ready()
  const log = process.env.FAVELA_DEBUG ? (msg, data) => console.log(JSON.stringify({ msg, ...data })) : null
  node.transport = new SwarmTransport(swarm, { log, overlay: node.overlay })
  // The transport is set after construction so ready() can join every feed it opens, including the
  // followed ones it re-opens on boot.
  // ⛔ ONE call. `ready()` opens the feeds, the namespace and the rooms, and cannot announce any of
  // them — the transport is built after it so the overlay exists first, so every join inside ready()
  // is a no-op. Three things have needed re-joining by hand here and each was forgotten in turn,
  // producing a log that replicates for nobody. `announceAll` makes the next one automatic.
  await node.announceAll()

  createServer(node, sock, { onError: () => {} })

  console.log(JSON.stringify({
    msg: 'favela-peer up', feed: node.key.toString('hex'), storage, sock
  }))

  // Idempotent, and BOUNDED: a close that never settles must not keep an OTA from respawning (the
  // supervisor only respawns once we exit). The first caller's exit code wins: 75 for release-ready,
  // which the supervisor respawns immediately, 0 for a signal.
  let control = null
  let stopping = false
  const shutdown = async (code = 0) => {
    if (stopping) return
    stopping = true
    const deadline = setTimeout(() => {
      console.log(JSON.stringify({ msg: 'shutdown: deadline reached, exiting anyway', code }))
      process.exit(code)
    }, 15000)
    try { if (control) await control.close() } catch {}
    try { await node.close() } catch {}
    try { fs.unlinkSync(sock) } catch {}
    clearTimeout(deadline)
    process.exit(code)
  }
  process.on('SIGINT', () => shutdown(0))
  process.on('SIGTERM', () => shutdown(0))

  // OTA: registers svc.sock with pgh-sidecar so a published release reaches THIS process. Fails soft.
  control = await startControl({
    udsPath: svcSock,
    name: process.env.PGH_APP_NAME || 'favela-peer',
    version: pkgVersion(),
    status: () => ({ ok: true, name: 'favela-peer', version: pkgVersion(), feed: node.key.toString('hex') }),
    onRelease: () => shutdown(75)
  })
}

main().catch((err) => { console.error(err); process.exit(1) })