OverviewFilesCommitsIssuesDocsReleasesPeers

pgh / lib / subscriptions.js

112 lines · javascript ref:
/**
 * Release-registry subscriptions — persistent list of registry keys the
 * local pgh-sidecar should replicate as live warm replicas.
 *
 * Without this, cross-machine OTA is broken: a subscriber has an empty
 * own-registry, the `releases-latest` handler returns null, the daemon's
 * OTA poller never sees new releases from the publisher.
 *
 * Config format (JSON at ~/.config/pgh/subscriptions.json):
 *   {
 *     "registries":     ["<64-char hex>", ...],   // release registries (OTA)
 *     "nameRegistries": ["<64-char hex>", ...]     // NameRegistry autobases
 *   }
 *
 * sidecar reads this on boot, pgh install auto-adds, user can edit.
 *
 * `nameRegistries` is the NameRegistry analogue: a fresh standalone peer's
 * sidecar boots its own empty NameRegistry, so it can't resolve bare app
 * names claimed in someone else's registry. Subscribing to that registry's
 * autobase key opens a read-only warm replica, so `pgh app install <name>`
 * resolves. `pgh names subscribe <key>` adds one here.
 */

import process from '#node:process'
import fs from '#node:fs'
import path from '#node:path'
import { homedir } from '#node:os'

export const SUBSCRIPTIONS_PATH = path.join(homedir(), '.config', 'pgh', 'subscriptions.json')

// Resolved per call (not once at import) so tests and an isolated sidecar can
// point at their own file. Mirrors the PGH_SIDECAR_* overrides in
// pgh-sidecar/bin — subscriptions.json was the one piece of that isolation
// story with no override, so an experimental sidecar shared prod's file.
function subscriptionsPath () {
  return process.env.PGH_SUBSCRIPTIONS_PATH || SUBSCRIPTIONS_PATH
}

export function readSubscriptions () {
  try {
    const raw = fs.readFileSync(subscriptionsPath(), 'utf8')
    const j = JSON.parse(raw)
    return Array.isArray(j.registries) ? j.registries.filter(k => typeof k === 'string' && k.length === 64) : []
  } catch { return [] }
}

function writeSubscriptions (keys) {
  // Preserve sibling keys (e.g. nameRegistries) — don't clobber the file.
  const j = (() => { try { return JSON.parse(fs.readFileSync(subscriptionsPath(), 'utf8')) || {} } catch { return {} } })()
  j.registries = [...new Set(keys)]
  fs.mkdirSync(path.dirname(subscriptionsPath()), { recursive: true })
  fs.writeFileSync(subscriptionsPath(), JSON.stringify(j, null, 2))
}

export function addSubscription (keyHex) {
  if (!keyHex || keyHex.length !== 64) return false
  const cur = readSubscriptions()
  if (cur.includes(keyHex)) return false
  cur.push(keyHex)
  writeSubscriptions(cur)
  return true
}

export function removeSubscription (keyHex) {
  const cur = readSubscriptions()
  const next = cur.filter(k => k !== keyHex)
  if (next.length === cur.length) return false
  writeSubscriptions(next)
  return true
}

// ── Name-registry subscriptions ──────────────────────────────
// Same file, separate `nameRegistries` array. Read by the sidecar on boot
// to open read-only NameRegistry replicas (so bare app-name resolution
// works across publishers).

function readRaw () {
  try { return JSON.parse(fs.readFileSync(subscriptionsPath(), 'utf8')) || {} }
  catch { return {} }
}

function writeNameSubscriptions (keys) {
  const j = readRaw()
  j.nameRegistries = [...new Set(keys)]
  fs.mkdirSync(path.dirname(subscriptionsPath()), { recursive: true })
  fs.writeFileSync(subscriptionsPath(), JSON.stringify(j, null, 2))
}

export function readNameSubscriptions () {
  const j = readRaw()
  return Array.isArray(j.nameRegistries)
    ? j.nameRegistries.filter(k => typeof k === 'string' && k.length === 64)
    : []
}

export function addNameSubscription (keyHex) {
  if (!keyHex || keyHex.length !== 64) return false
  const cur = readNameSubscriptions()
  if (cur.includes(keyHex)) return false
  cur.push(keyHex)
  writeNameSubscriptions(cur)
  return true
}

export function removeNameSubscription (keyHex) {
  const cur = readNameSubscriptions()
  const next = cur.filter(k => k !== keyHex)
  if (next.length === cur.length) return false
  writeNameSubscriptions(next)
  return true
}