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