// Transport — how a feed meets its peers.
//
// Injected, exactly as the KV was. Nothing above this file knows what a socket is: `Feed.replicate`
// takes a boolean or a duplex, and a transport's whole job is to produce one and hand it over. That
// is what makes the alternatives — mDNS on a LAN (`hyperdht-mdns`), BLE (`ble-swarm`,
// `nearby-discovery`) — additions rather than rewrites, which matters because bootstrap is the
// weakest part of this design and the DHT is not the only answer to it.
import Protomux from 'protomux'
/**
* SwarmTransport — Hyperswarm over the DHT.
*
* ⚠ NOT COVERED BY THE TEST SUITE. It needs a live DHT, and a test that needs one is a test that
* fails for reasons unrelated to the code. What IS covered is everything it feeds: replication is
* proven over an in-memory duplex pair in replicate.test.js, which is the same seam this hands a
* socket to. This file is glue, and it is marked as such rather than counted as tested.
*/
export class SwarmTransport {
// `swarm` is a Hyperswarm instance, injected so this file does not own its lifecycle either.
constructor (swarm, opts = {}) {
this.swarm = swarm
// Off by default; the daemon turns it on with FAVELA_DEBUG. A transport that fails silently is
// exactly what shipped here — a follow that records and replicates nothing, with no signal.
this._log = opts.log || (() => {})
// ⛔ The overlay must attach to EVERY connection or the log replicates and the bytes never do.
// That failure is silent and looks like "nothing arrived": the follower converges the pointer
// set, reports every record as UNRESOLVED, and shows an empty timeline. Measured 2026-09-06 —
// two daemons, swarm connected, feeds replicating, zero bytes, because this line did not exist.
this.overlay = opts.overlay || null
this.feeds = new Map() // discovery key hex -> Feed
this._onconnection = (socket) => {
// Pass the SOCKET, never a boolean, and never pipe by hand.
//
// Two bugs lived here and neither showed up in a test, because the suite replicates over a
// dedicated stream pair where a boolean is right:
// 1. `replicate(false)` hardcoded BOTH ends as responder, so no handshake ever completed.
// 2. Piping each feed separately onto one socket gives each its own stream over a shared
// transport. Hypercore multiplexes — every base attaches to the socket's existing muxer,
// which is what lets one connection carry all of them.
// Symptom of either: a follow that records fine and replicates nothing, silently, forever.
let n = 0
for (const [, feed] of this.feeds) {
try { feed.replicate(socket); n++ } catch (err) { this._log('replicate-failed', err.message) }
}
let overlay = false
if (this.overlay) {
try { this.overlay.attachProtocol(Protomux.from(socket)); overlay = true } catch (err) { this._log('overlay-attach-failed', { err: err.message }) }
}
this._log('connection', { feeds: n, overlay })
}
this.swarm.on('connection', this._onconnection)
}
// Announce and look for peers of this feed. The topic is the feed's discovery key — derived from
// its public key, so it reveals which feed you want without revealing that you can write to it.
async join (feed) {
const dk = feed.discoveryKey
const id = Buffer.from(dk).toString('hex')
if (this.feeds.has(id)) return
this.feeds.set(id, feed)
const discovery = this.swarm.join(dk, { server: true, client: true })
// Attach to connections we ALREADY have — a feed held after boot (a fresh follow) would
// otherwise wait for a new connection that may never come.
let existing = 0
for (const socket of this.swarm.connections) { try { feed.replicate(socket); existing++ } catch {} }
// ⛔ NOT awaited. `flushed()` waits for the DHT to acknowledge the announce, and its result is
// never used — replication to peers we already have is set up on the line above, and new peers
// arrive on the 'connection' handler whenever the announce lands. Awaiting it put a DHT round
// trip inside every feed open: a socket request that hung, and a boot that paid it once per
// followed feed, in series.
discovery.flushed().then(
() => this._log('join', { topic: id.slice(0, 12), existingConns: existing }),
(err) => this._log('join-flush-failed', { topic: id.slice(0, 12), err: err.message })
)
}
async leave (feed) {
const id = Buffer.from(feed.discoveryKey).toString('hex')
if (!this.feeds.delete(id)) return
await this.swarm.leave(feed.discoveryKey)
// ⚠ Leaving stops ANNOUNCING. It does not un-replicate what a peer already has, and it does not
// close live connections. Same shape as delisting: propagation stops, nothing is retracted.
}
async destroy () {
this.swarm.off('connection', this._onconnection)
this.feeds.clear()
await this.swarm.destroy()
}
}