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