// The favela-web FEED REACTOR (community-ux.md §120): reconcile the feeds favela-web SERVES to the durable
// P2P FeedRegistry — with no per-community env and no restart. A FeedRegistry watch fires with the registry's
// FULL current entry list on every advance; reconcile(regKey, entries) makes the served set match:
// - add-feed → feedgen.addFeed (serve getFeedSkeleton) + wireRoster (index the roster, scope viewers)
// - remove-feed (an entry that vanished) → feedgen.removeFeed, but ONLY once NO registry still lists it
// (a feed can be listed in more than one allow-set registry; it stays served while any of them lists it).
// Roster-first: entries with a roomKey (community feeds — curation lives in the room) are served; light
// feeds (no room — curation in the entry) are reported to onLight and skipped for now.
//
// Pure + injected (feedgen, wireRoster, log stubs) so the reconcile is unit-testable without a live server.
// The registry entry shape is feed-schema.js: { did, rkey, name, curator, params, roomKey, handle, ... }.
export function makeFeedReactor ({ feedgen, wireRoster = async () => {}, log = () => {}, onLight = null } = {}) {
if (!feedgen) throw new Error('makeFeedReactor requires a feedgen')
const key = (did, rkey) => `${did}\x00${rkey}`
const servedBy = new Map() // `${did}\x00${rkey}` -> Set<regKey> (served while ANY registry lists it)
const wiredRooms = new Set() // roomKeys we've already wired a roster watch for (wire once; a live watch is
// permanent + harmless even after a feed is de-listed, so we never un-wire — re-adding won't double-watch)
async function reconcile (regKey, entries) {
const desired = new Set()
for (const e of entries || []) {
if (!e || !e.did || !e.rkey) continue
if (!e.roomKey) { if (onLight) onLight(e); else log(`[favela] light feed (no room) not served yet — roster-first: ${e.did} ${e.rkey}`); continue }
const k = key(e.did, e.rkey)
desired.add(k)
// idempotent (Map.set): a re-fire with an unchanged entry is a no-op; a changed name/room updates it.
feedgen.addFeed({ communityDid: e.did, rkey: e.rkey, roomKey: e.roomKey, displayName: e.name || e.rkey, description: `favela community: ${e.name || e.rkey}` })
if (!servedBy.has(k)) servedBy.set(k, new Set())
const regs = servedBy.get(k)
if (!regs.has(regKey)) { regs.add(regKey); log(`[favela] registry feed: at://${e.did}/app.bsky.feed.generator/${e.rkey} (${e.name || e.rkey})`) }
if (!wiredRooms.has(e.roomKey)) { wiredRooms.add(e.roomKey); await wireRoster(e.name || e.rkey, e.roomKey) }
}
// drop THIS registry's claim on feeds it no longer lists; stop SERVING only when no registry lists them.
for (const [k, regs] of servedBy) {
if (regs.has(regKey) && !desired.has(k)) {
regs.delete(regKey)
if (regs.size === 0) {
const i = k.indexOf('\x00')
const did = k.slice(0, i); const rkey = k.slice(i + 1)
feedgen.removeFeed(did, rkey)
servedBy.delete(k)
log(`[favela] registry feed removed: at://${did}/app.bsky.feed.generator/${rkey}`)
}
}
}
}
// introspection (tests / status): which (did,rkey) are currently served, and by which registries.
reconcile.served = () => [...servedBy.keys()].map((k) => { const i = k.indexOf('\x00'); return { did: k.slice(0, i), rkey: k.slice(i + 1), registries: [...servedBy.get(k)] } })
return reconcile
}
export default makeFeedReactor