OverviewFilesCommitsIssuesDocsReleasesPeers

favela-community commits / 7a3b06d

favela-community: prove live-swarm e2e + FIX CommunityRooms never replicated
The live e2e (test/live-swarm-e2e.mjs, `npm run e2e`) surfaced a real bug: a keyless reader would DISCOVER a served room on the DHT and connect, but read 0 members — CommunityRooms joined the room's discoveryKey yet never replicated on connection, so no cores flowed and the roster never synced. favela-web's community read would have silently never worked. - lib/community-room.js — CommunityRooms now registers swarm.on('connection', conn => this._store.replicate(conn)) (muxes over the connection; composes with a shared swarm's other consumers e.g. favela-web's DidPeer router). conn error swallowed (peer resets on close). - test/live-swarm-e2e.mjs — two INDEPENDENT Hyperswarm nodes over the REAL DHT (not testnet, not pipes): a seeder (the CLI `serve` mechanism: base.replicate + join(discoveryKey)) and a keyless reader (favela-web's exact CommunityRooms). PROVEN: reader discovers the room on the DHT, connects, replicates, reads the full 3-member roster; reader is keyless (not writable). `npm run e2e` (needs network; NOT in the offline `npm test`). Offline suite still green (community-room + cli). favela-web boots unchanged (the fix only adds replication when a community-topic connection forms). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7a3b06de00ca22379fd730a3b2c8fd8cc111edd7
by plankatron <fleeky@gmail.com> · 2026-07-15T04:21:19.000Z

Changed files (3) +81 -1

lib/community-room.js +5 -0
……@@ -138,6 +138,11 @@ export class CommunityRooms {
138138 this._store = corestore
139139 this._swarm = swarm
140140 this._rooms = new Map() // roomKeyHex -> Promise<CommunityRoom>
141 // Replicate our corestore (all opened room replicas) on every swarm connection. Joining a room's
142 // discoveryKey only DISCOVERS peers — without this the connection forms but no cores flow and the
143 // roster never syncs. corestore.replicate muxes over the connection (composes with other consumers of
144 // a shared swarm, e.g. favela-web's DidPeer router). Errors are swallowed (peer resets on close).
145 if (swarm) swarm.on('connection', (conn) => { conn.on('error', () => {}); this._store.replicate(conn) })
141146 }
142147
143148 room (roomKeyHex) {
package.json +2 -1
……@@ -8,7 +8,8 @@
88 "favela-community": "bin/favela-community.js"
99 },
1010 "scripts": {
11 "test": "bare test/community-room.test.mjs && bare test/cli.test.mjs"
11 "test": "bare test/community-room.test.mjs && bare test/cli.test.mjs",
12 "e2e": "bare test/live-swarm-e2e.mjs"
1213 },
1314 "dependencies": {
1415 "hyper-rooms": "file:../hyper-rooms",
test/live-swarm-e2e.mjs +74 -0
……@@ -0,0 +1,74 @@
1// LIVE swarm e2e — over the REAL DHT (real Hyperswarm bootstrap, NOT @hyperswarm/testnet, NOT in-process
2// pipes). Two INDEPENDENT swarm nodes discover each other on the DHT and replicate a community room:
3// - SEEDER : an owner room + a real Hyperswarm serving it — the exact mechanism the CLI `serve` uses
4// (base.replicate on every connection, join(discoveryKey)).
5// - READER : favela-web's exact keyless read path — CommunityRooms(store, realSwarm).members(roomKey).
6// Proves: owner `serve` ↔ favela-web keyless read, over a live swarm. Needs NETWORK (DHT bootstrap).
7// Run: npm run e2e (NOT part of the offline `npm test`).
8import process from 'bare-process'
9import Corestore from 'corestore'
10import Hyperswarm from 'hyperswarm'
11import { CommunityRoom, CommunityRooms } from '../index.js'
12
13const stamp = Date.now()
14const SEED_STORE = 'data/e2e-seed-' + stamp
15const READ_STORE = 'data/e2e-read-' + stamp
16const MEMBERS = ['did:plc:alice2222222222222222222', 'did:plc:bob222222222222222222222', 'did:plc:carol22222222222222222222']
17const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
18const log = (...a) => console.log('[e2e]', ...a)
19
20let seedStore, readStore, seedSwarm, readSwarm, rooms, ownerRoom
21async function cleanup () {
22 try { if (seedSwarm) await seedSwarm.destroy() } catch {}
23 try { if (readSwarm) await readSwarm.destroy() } catch {}
24 try { if (rooms) await rooms.close() } catch {}
25 try { if (ownerRoom) await ownerRoom.close() } catch {}
26 try { if (seedStore) await seedStore.close() } catch {}
27 try { if (readStore) await readStore.close() } catch {}
28}
29
30async function main () {
31 // ── SEEDER: owner creates + populates a room, then serves it on a real swarm ──
32 seedStore = new Corestore(SEED_STORE)
33 ownerRoom = await CommunityRoom.create(seedStore, { name: 'Live E2E' })
34 const KEY = ownerRoom.key
35 for (const did of MEMBERS) await ownerRoom.addMember(did)
36 await ownerRoom.update()
37 log('owner created + populated room', KEY, '(' + (await ownerRoom.members()).length + ' members)')
38
39 seedSwarm = new Hyperswarm()
40 seedSwarm.on('connection', (conn) => { conn.on('error', () => {}); ownerRoom.base.replicate(conn) }) // the CLI `serve` mechanism
41 seedSwarm.join(ownerRoom.discoveryKey, { server: true, client: true })
42 await seedSwarm.flush()
43 log('seeder announced on the DHT (discoveryKey', Buffer.from(ownerRoom.discoveryKey).toString('hex').slice(0, 16) + '…)')
44
45 // ── READER: favela-web's exact keyless read over an INDEPENDENT real swarm ──
46 readStore = new Corestore(READ_STORE)
47 readSwarm = new Hyperswarm()
48 rooms = new CommunityRooms(readStore, readSwarm) // joins the room's discoveryKey on first members()
49 log('reader joining the DHT + waiting for a live connection + replication…')
50
51 let got = []
52 const end = Date.now() + 90000
53 let firstSeen = 0
54 while (Date.now() < end) {
55 try { got = await rooms.members(KEY) } catch {}
56 const conns = readSwarm.connections.size
57 if (conns && !firstSeen) { firstSeen = Date.now(); log('reader connected to a peer (' + conns + ') — replicating…') }
58 if (got.length >= MEMBERS.length) break
59 await sleep(1500)
60 }
61
62 const room = await rooms.room(KEY)
63 const keyless = !room.writable
64 const ok = keyless && got.length === MEMBERS.length && MEMBERS.every((m) => got.includes(m))
65 log('reader keyless (not writable):', keyless)
66 log('reader read', got.length, '/', MEMBERS.length, 'members over the live swarm:', JSON.stringify(got))
67 log(ok
68 ? '\nPASS — an independent keyless reader discovered the served room on the real DHT and read the full roster.'
69 : '\nFAIL — reader did not converge on the roster over the live swarm within 90s.')
70 return ok
71}
72
73main().then(async (ok) => { await cleanup(); process.exit(ok ? 0 : 1) })
74 .catch(async (e) => { console.error('[e2e] ERROR:', e && (e.stack || e.message)); await cleanup(); process.exit(1) })