// favela-community — the FEED REGISTRY: the durable, co-ownable, keyless-readable list of feeds that every
// reactor (favela-web index / membrane front+requestCrawl / hpp serve / blind-relay seed) watches, so "a
// feed exists" is ONE P2P fact with no per-feed config (favela-web/spec/community-ux.md). Same three-role
// shape as CommunityRoom: OWNER (create) / MOD (open + admitted) / READER (keyless openReadonly).
// Bare-native (Autobase/Hyperbee need a disk path). Run: bare test/feed-registry.test.mjs
import Corestore from 'corestore'
import { FeedRegistry, FeedRegistries } from '../index.js'
const assert = (c, m) => { if (!c) { console.error('FAIL:', m); process.exit(1) } console.log(' ok -', m) }
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const stamp = Date.now()
// a light feed (creator's OWN hpp repo, no roomKey) + a community feed (co-owned DID + roster roomKey)
const OWN = 'did:plc:owner222222222222222222222'
const COMM = 'did:plc:community222222222222222'
const ROSTER = 'a'.repeat(64) // a 32-byte hex CommunityRoom key
// pipe two bases' replication streams together (in-process stand-in for the swarm)
function wire (a, b) {
const s1 = a.base.replicate(true); const s2 = b.base.replicate(false)
s1.on('error', () => {}); s2.on('error', () => {})
s1.pipe(s2).pipe(s1)
return () => { try { s1.destroy() } catch {} try { s2.destroy() } catch {} }
}
async function until (fn, ms = 15000) { const end = Date.now() + ms; while (Date.now() < end) { if (await fn()) return true; await sleep(100) } return false }
const ownerStore = new Corestore('data/fr-owner-' + stamp)
const readerStore = new Corestore('data/fr-reader-' + stamp)
const modStore = new Corestore('data/fr-mod-' + stamp)
// ── OWNER: create + publish feeds ──
const owner = await FeedRegistry.create(ownerStore, { name: 'Favela Feeds' })
const REG = owner.key
assert(/^[0-9a-f]{64}$/.test(REG), 'fresh registry has a 64-hex public bootstrap key')
assert(owner.writable, 'creator is a writer (genesis indexer)')
// a light feed (arbitrary curator, HOME = creator's own hpp, no roster) + a community feed (roster curator)
await owner.addFeed({ did: OWN, rkey: 'my-p2p', name: 'p2p tag', curator: 'subject', params: { tag: 'p2p' } })
await owner.addFeed({ did: COMM, rkey: 'loser-club', name: 'loser club', curator: 'roster', params: {}, roomKey: ROSTER })
await owner.update()
let of = await owner.feeds()
assert(of.length === 2, 'owner lists both published feeds')
const community = of.find((f) => f.rkey === 'loser-club')
assert(community && community.did === COMM && community.curator === 'roster' && community.roomKey === ROSTER, 'community feed entry carries did + roster curator + roomKey')
const light = of.find((f) => f.rkey === 'my-p2p')
assert(light && light.curator === 'subject' && light.params.tag === 'p2p' && light.roomKey === null, 'light feed entry carries subject curator + params, roomKey null')
const one = await owner.feed(COMM, 'loser-club')
assert(one && one.name === 'loser club', 'feed(did,rkey) reads a single entry')
// ── validation ──
const rejects = async (fn, m) => { let t = false; try { await fn() } catch { t = true } assert(t, m) }
await rejects(() => owner.addFeed({ did: 'not-a-did', rkey: 'x', curator: 'roster' }), 'addFeed rejects a bad did')
await rejects(() => owner.addFeed({ did: OWN, rkey: '..', curator: 'roster' }), 'addFeed rejects a bad rkey')
await rejects(() => owner.addFeed({ did: OWN, rkey: 'x', curator: '' }), 'addFeed rejects an empty curator')
await rejects(() => owner.addFeed({ did: OWN, rkey: 'x', curator: 'roster', params: [1, 2] }), 'addFeed rejects non-object params')
// ── remove-feed ──
await owner.removeFeed(OWN, 'my-p2p')
assert(await until(async () => (await owner.feeds()).length === 1), 'removeFeed drops the entry (last-writer-wins del)')
// ── KEYLESS READER (the reactor path): openReadonly, replicates, never writes ──
const reader = await FeedRegistry.openReadonly(readerStore, REG)
assert(!reader.writable, 'keyless reader is NOT writable (holds no key)')
const unwireR = wire(owner, reader)
assert(await until(async () => (await reader.feeds()).some((f) => f.rkey === 'loser-club')), 'KEYLESS READ: reader replicates the feed list (no key)')
await rejects(() => reader.addFeed({ did: OWN, rkey: 'evil', curator: 'roster' }), 'keyless reader REFUSES to write (addFeed throws — reactors are keyless)')
// ── MOD: admitted by the owner, co-publishes (a registry can be co-owned) ──
const mod = await FeedRegistry.open(modStore, REG)
assert(!mod.writable, 'mod is not writable before admission')
const unwireM = wire(owner, mod)
await owner.addMod(mod.localKey, { userId: 'mod-b' })
assert(await mod.waitWritable({ timeout: 15000 }), 'mod becomes writable after owner.addMod (admission)')
await mod.addFeed({ did: COMM, rkey: 'mod-feed', name: 'mod feed', curator: 'roster', params: {}, roomKey: ROSTER })
assert(await until(async () => (await owner.feeds()).some((f) => f.rkey === 'mod-feed')), 'WRITE LAYER: owner sees the mod\'s addFeed — co-writer ops linearize through the owner')
assert(await until(async () => (await reader.feeds()).some((f) => f.rkey === 'mod-feed')), 'the keyless reader also converges on the mod\'s addFeed')
// ── REACTOR WATCHER: a feed published AFTER watch starts reaches the reactor with NO restart (the design goal) ──
const watchStore = new Corestore('data/fr-watch-' + stamp)
const regs = new FeedRegistries(watchStore, null) // no swarm — wire replication manually below
const seen = []
const unwatch = await regs.watch(REG, (feeds) => { seen.push(feeds.map((f) => f.rkey).sort()) }, { debounceMs: 50 })
const watchReader = await regs.registry(REG) // same replica watch() opened
const unwireW = wire(owner, watchReader)
assert(await until(() => seen.length > 0 && seen[seen.length - 1].includes('loser-club')), 'watch BASELINE: reactor sees the current feed set after replication')
await owner.addFeed({ did: COMM, rkey: 'fresh', name: 'fresh feed', curator: 'roster', params: {}, roomKey: ROSTER })
assert(await until(() => seen[seen.length - 1].includes('fresh')), 'watch DELTA: a feed published AFTER watch started reaches the reactor with NO re-open')
await unwatch(); unwireW()
console.log('\nFAVELA-COMMUNITY FEED-REGISTRY OK (Bare-native): owner publish + validation + remove + keyless read + mod admission (co-owned) + reactor watcher (baseline + delta)')
unwireR(); unwireM()
await owner.close(); await reader.close(); await mod.close()
try { await regs.close() } catch {}
try { await ownerStore.close(); await readerStore.close(); await modStore.close(); await watchStore.close() } catch {}