OverviewFilesCommitsIssuesDocsReleasesPeers

favela-web / lib / fronts-diag.js

91 lines · javascript ref:
// fronts-diag — which co-owner's firehose is this node actually following, and why?
//
// WHY THIS EXISTS. A co-owned DID is served by several co-owners, each with its own firehose core.
// DidPeerRouter picks one ("the freshest front") by ranking verified fronts on head.rev, preferring live
// ones, behind a continuity gate. When that choice is wrong the index FREEZES at the failover point while
// every other surface looks healthy: the peer is connected, the daemon is up, backfill reports success,
// and the profile page renders — it is simply missing everything appended since.
//
// That is not hypothetical. favela-web's own ingest loop carries the comment "wj62/ashatron both froze at
// their July co-owner failovers", and on 2026-07-30 ashatron was found ~42h stale on wj62 while both
// co-owners held 151 records. Diagnosing it from outside was impossible: nothing exposed which front was
// chosen, what rev each offered, or how long their firehoses were. Four restarts and a from-zero backfill
// all reported success and changed nothing.
//
// So this reports the INPUTS to the decision, not just its outcome. `chosen` alone would say what
// happened; rev/live/firehoseLength per front say WHY, and make "we are pinned to a short line" visible
// at a glance instead of inferable after an afternoon.
//
// READS ROUTER INTERNALS (_fronts, _byDid, _freshestFront, front._firehoseKey/head/seq). atproto-did-peer
// exposes no public accessor for them — its announce payloads carry firehoseLength but there is no
// status(). Same bargain hpp's conflict-diag made: hooking internals is worse than being handed an API,
// and better than a failure mode nobody can see. If this earns its keep, push it upstream.
//
// Defensive throughout: a half-open front, a peer mid-handshake or a shape change upstream must degrade
// to nulls, never throw. A diagnostic that can 500 the page it is meant to explain is worthless.

const hex = (b) => {
  try { return typeof b === 'string' ? b : (b ? Buffer.from(b).toString('hex') : null) } catch { return null }
}

export function frontsDiag (router, dids = null) {
  const at = new Date().toISOString()
  if (!router || !router._fronts) return { at, error: 'no router', dids: [] }

  const targets = Array.isArray(dids) && dids.length ? dids : [...router._fronts.keys()]
  const out = []

  for (const did of targets) {
    const set = router._fronts.get(did) || new Map()
    const live = (router._byDid && router._byDid.get(did)) || new Map()

    // The router's own answer, asked the same way the ingest loop asks it.
    let chosenKey = null
    try {
      const f = router._freshestFront(did)
      chosenKey = f && f._firehoseKey ? hex(f._firehoseKey) : null
    } catch { chosenKey = null }

    const fronts = []
    for (const [key, f] of set) {
      let firehoseLength = null
      try { firehoseLength = (f && f.seq && f.seq.core) ? f.seq.core.length : null } catch {}
      const rev = (f && f.head && f.head.rev) ? f.head.rev : null
      fronts.push({
        firehoseKey: key,
        short: typeof key === 'string' ? key.slice(0, 12) : null,
        // `live` is the router's own liveness map — a front can be known (announced) without a live
        // connection, and the ranking prefers live ones, so the distinction is load-bearing.
        live: live.has(key),
        // Only fronts with a verified head are ranked at all; an unverified one is invisible to the
        // choice, which is itself a reason a node can end up on an older front.
        verified: !!rev,
        rev,
        firehoseLength,
        chosen: chosenKey != null && key === chosenKey
      })
    }

    // Highest rev first — the order the ranking sees, so a chosen front that is NOT at the top is
    // immediately visible as the anomaly worth explaining (continuity gate, or not live).
    fronts.sort((a, b) => String(b.rev || '').localeCompare(String(a.rev || '')))

    const chosen = fronts.find((f) => f.chosen) || null
    const best = fronts.find((f) => f.verified) || null
    out.push({
      did,
      frontCount: fronts.length,
      chosen: chosen ? chosen.short : null,
      // The one-line answer an operator wants: are we on the highest-rev line, and is the line we are on
      // shorter than one we can see? Either being true is the freeze signature.
      onHighestRev: !!(chosen && best && chosen.firehoseKey === best.firehoseKey),
      behindBy: (chosen && best && Number.isFinite(chosen.firehoseLength) && Number.isFinite(best.firehoseLength))
        ? best.firehoseLength - chosen.firehoseLength
        : null,
      fronts
    })
  }

  return { at, dids: out }
}