OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-did-peer commits / d1ece65

did-peer: DidPeerRouter — one announce channel per connection, dispatch by DID
Multi-DID reading over a shared corestore+swarm was broken: each DidPeer.attach opens its OWN atproto/did-announce/v1 channel per connection, but Protomux allows only ONE channel per (protocol,id) on a mux — so when several DidPeers share a swarm, only the FIRST-added peer binds the channel, receives every announce, and drops all but its own DID. Every other member silently never learns its cores. (Masked whenever the data-bearing peer happened to be added first.) DidPeerRouter owns corestore replication + ONE shared announce channel per connection and DISPATCHES each announce to the matching DidPeer by did (the keet / atproto-bridge PeerRouter model). DidPeer gains a public onAnnounce, a `holds` getter, and announceVia(message) for the shared channel; attach() is kept for the single-peer path. Verified live: a reader with the list-owner DID added FIRST + wj62 second now reads BOTH (list resolves + wj62's posts load in ~8s); before the fix wj62 stayed empty for 90s+. Offline: commitops + favela suites green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
d1ece65abc25c1d0f947981f9f477fa94c9c139a
by plankatron <fleeky@gmail.com> · 2026-07-14T17:21:00.000Z

Changed files (2) +87 -3

index.js +1 -1
……@@ -1 +1 @@
1export { DidPeer, ANNOUNCE_PROTOCOL } from './lib/did-peer.js'
1export { DidPeer, DidPeerRouter, ANNOUNCE_PROTOCOL } from './lib/did-peer.js'
lib/did-peer.js +86 -2
……@@ -89,13 +89,31 @@ export class DidPeer {
8989 if (!chan) return // already open on this mux
9090 chan._announce = chan.addMessage({
9191 encoding: c.json,
92 onmessage: (a) => this._onannounce(a).catch((e) => this._fail(e))
92 onmessage: (a) => this.onAnnounce(a).catch((e) => this._fail(e))
9393 })
9494 chan.open()
9595 this._channels.add(chan)
9696 if (this.repo) this._announceTo(chan)
9797 }
9898
99 // Does this peer currently hold (and can serve/seed) the DID's cores?
100 get holds () { return !!this.repo }
101
102 // Send this peer's announce over a protomux message (used by DidPeerRouter's shared per-connection
103 // channel, where a single channel is multiplexed across many DIDs).
104 announceVia (message) {
105 if (!this.repo || !message) return
106 try {
107 message.send({
108 did: this.did,
109 repoKey: b4a.toString(this._repoKey, 'hex'),
110 firehoseKey: b4a.toString(this._firehoseKey, 'hex'),
111 repoLength: this.repo.core.length,
112 firehoseLength: this.seq.core.length
113 })
114 } catch (e) { this._fail(e) }
115 }
116
99117 _announceTo (chan) {
100118 if (!this.repo || !chan._announce) return
101119 try {
……@@ -111,7 +129,7 @@ export class DidPeer {
111129
112130 _announceAll () { for (const chan of this._channels) this._announceTo(chan) }
113131
114 async _onannounce (a) {
132 async onAnnounce (a) {
115133 if (!a || a.did !== this.did || !a.repoKey || !a.firehoseKey) return
116134 if (!this.repo) {
117135 await this._openCores(a.repoKey, a.firehoseKey)
……@@ -240,4 +258,70 @@ export class DidPeer {
240258 }
241259}
242260
261// Multi-DID reader over ONE shared corestore + swarm. Fixes the per-DidPeer attach() collision: each
262// DidPeer.attach opens its OWN ANNOUNCE_PROTOCOL channel per connection, but Protomux allows only ONE
263// channel per (protocol, id) on a mux — so when several DidPeers share a swarm, only the FIRST-added
264// peer binds the channel and every other DID silently never learns its cores (its announce is received
265// by the first peer, which drops it as a did mismatch). The router opens ONE announce channel per
266// connection and DISPATCHES each announce to the matching DidPeer by did. Mirrors atproto-bridge's
267// PeerRouter / keet's one-channel-per-connection model.
268//
269// The caller owns the swarm (and joins discoveryTopic(did) per DID); the router owns the corestore
270// replication + the shared announce channel. Wire it: swarm.on('connection', c => router.onconnection(c)).
271export class DidPeerRouter {
272 constructor (corestore) {
273 this.corestore = corestore
274 this._peers = new Map() // did -> DidPeer
275 this._channels = new Set()
276 }
277
278 get (did) { return this._peers.get(did) || null }
279 peers () { return [...this._peers.values()] }
280
281 // Register (or return) a DidPeer for a did. Does NOT join the swarm — the caller joins the topic.
282 add (did, opts = {}) {
283 let p = this._peers.get(did)
284 if (p) return p
285 p = new DidPeer(this.corestore, { did, ...opts })
286 this._peers.set(did, p)
287 for (const chan of this._channels) p.announceVia(chan._announce) // (re-add) announce if already held
288 return p
289 }
290
291 // Wire ONE swarm connection: replicate the corestore ONCE, open ONE announce channel, and route each
292 // received announce to the matching DidPeer by did.
293 onconnection (conn) {
294 try { this.corestore.replicate(conn) } catch {}
295 const mux = Protomux.from(conn)
296 const chan = mux.createChannel({
297 protocol: ANNOUNCE_PROTOCOL,
298 onopen: () => this._announceHeld(chan),
299 onclose: () => this._channels.delete(chan)
300 })
301 if (!chan) return // already open on this mux
302 chan._announce = chan.addMessage({
303 encoding: c.json,
304 onmessage: (a) => {
305 const p = a && a.did ? this._peers.get(a.did) : null
306 if (p) p.onAnnounce(a).catch(() => {})
307 }
308 })
309 chan.open()
310 this._channels.add(chan)
311 this._announceHeld(chan)
312 }
313
314 // Announce every held DID on a channel (symmetric seeding: tell the peer which cores we can serve).
315 _announceHeld (chan) {
316 for (const p of this._peers.values()) p.announceVia(chan._announce)
317 }
318
319 async close () {
320 for (const chan of this._channels) { try { chan.close() } catch {} }
321 this._channels.clear()
322 for (const p of this._peers.values()) { try { await p.close() } catch {} }
323 this._peers.clear()
324 }
325}
326
243327export { ANNOUNCE_PROTOCOL }