import test from 'brittle'
import { durableConnect } from '../lib/durable-connect.js'
// ── minimal fakes (no runtime-specific imports, runs under node or bare) ──
class Emitter {
constructor () { this._l = new Map() }
on (e, fn) { if (!this._l.has(e)) this._l.set(e, new Set()); this._l.get(e).add(fn); return this }
off (e, fn) { if (this._l.has(e)) this._l.get(e).delete(fn); return this }
emit (e, ...a) { if (this._l.has(e)) for (const fn of [...this._l.get(e)]) fn(...a) }
}
class FakeSwarm extends Emitter {
constructor () { super(); this.joined = [] }
join (topic, opts) { this.joined.push({ topic, opts }) }
}
class FakeConn extends Emitter {
constructor (id) { super(); this.id = id; this.destroyed = false }
destroy () { this.destroyed = true }
}
const TOPIC = Buffer.alloc(32, 7)
const later = (ms, fn) => setTimeout(fn, ms)
// The OLD connect model this transport used — the control. Reproduces the bug:
// swarm.once('connection') + a wall-clock reject timer that races the DHT.
function legacyConnect (swarm, topic, { timeout }) {
return new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error('Connection timeout')), timeout)
let done = false
const onc = (conn) => { if (done) return; done = true; clearTimeout(t); swarm.off('connection', onc); resolve(conn) }
swarm.on('connection', onc)
swarm.join(topic, { server: false, client: true })
})
}
// ── CONTROL: the old model FAILS a connection that arrives after the deadline ──
test('control — legacy once+timer rejects a late connection (reproduces the bug)', async (t) => {
const swarm = new FakeSwarm()
const p = legacyConnect(swarm, TOPIC, { timeout: 50 })
later(150, () => swarm.emit('connection', new FakeConn('late'))) // arrives after 50ms deadline
await t.exception(p, /Connection timeout/, 'a live-but-late peer is lost to the wall-clock timer')
})
// ── MECHANISM: durableConnect resolves the SAME late connection ──
test('mechanism — durableConnect resolves a late connection (no aggressive deadline)', async (t) => {
const swarm = new FakeSwarm()
const conn = new FakeConn('late')
const p = durableConnect(swarm, TOPIC, {}) // no fallbackAfter → waits
later(150, () => swarm.emit('connection', conn))
const got = await p
t.is(got, conn, 'the late connection wins — it just waited instead of dying')
t.is(swarm.joined.length, 1, 'joined the topic as client once')
t.is(swarm.joined[0].opts.client, true, 'client:true')
})
// ── THE ASSERTION: concurrency starves losers under the timer; durable fixes it ──
// Models the observed ashatron failure: N concurrent `git fetch punch://`, each
// its own swarm, sharing a contended connect budget so connections arrive
// STAGGERED. Under the 15s-style per-connector deadline the ones behind the
// contention exceed it and fail EMPTY; durableConnect lets every one land.
test('repro — 5 concurrent pulls, staggered connects: legacy starves losers, durable lands all', async (t) => {
const N = 5
const DEADLINE = 100
// Staggered arrival times (a shared, contended connect budget serializes them).
const arrivals = [40, 90, 140, 190, 240] // 2 within the deadline, 3 past it
// Legacy: each connector has its own swarm + its own deadline timer.
const legacyResults = await Promise.allSettled(arrivals.map((ms) => {
const swarm = new FakeSwarm()
const p = legacyConnect(swarm, TOPIC, { timeout: DEADLINE })
later(ms, () => swarm.emit('connection', new FakeConn('l' + ms)))
return p
}))
const legacyOk = legacyResults.filter((r) => r.status === 'fulfilled').length
t.is(legacyOk, 2, 'legacy: only the 2 that beat the deadline complete — 3 starve EMPTY (the bug)')
// Durable: same staggered arrivals, no aggressive deadline.
const durableResults = await Promise.allSettled(arrivals.map((ms) => {
const swarm = new FakeSwarm()
const p = durableConnect(swarm, TOPIC, {})
later(ms, () => swarm.emit('connection', new FakeConn('d' + ms)))
return p
}))
const durableOk = durableResults.filter((r) => r.status === 'fulfilled').length
t.is(durableOk, N, 'durable: ALL 5 land — the losers waited out the contention instead of failing')
})
// ── reconcile across connections: discard a bad peer, take the next ──
test('reconcile — a connection that fails validate is discarded, the next is taken', async (t) => {
const swarm = new FakeSwarm()
const bad = new FakeConn('bad')
const good = new FakeConn('good')
const p = durableConnect(swarm, TOPIC, {
validate: (conn) => conn.id === 'good'
})
later(30, () => swarm.emit('connection', bad))
later(80, () => swarm.emit('connection', good))
const got = await p
t.is(got, good, 'resolved with the peer that passed validate')
t.ok(bad.destroyed, 'the failing peer was destroyed, not handed to the caller')
})
// ── caller cancellation ──
test('abort — rejects on AbortSignal, never on a built-in deadline', async (t) => {
const swarm = new FakeSwarm()
const ac = new AbortController()
const p = durableConnect(swarm, TOPIC, { signal: ac.signal })
later(20, () => ac.abort())
await t.exception(p, /aborted/, 'aborts only when the caller asks')
})
// ── generous no-progress escape (for the HTTPS fallback), NOT a connect race ──
test('fallbackAfter — rejects after the generous bound when NO peer is reached', async (t) => {
const swarm = new FakeSwarm()
const p = durableConnect(swarm, TOPIC, { fallbackAfter: 60 }) // nothing ever connects
await t.exception(p, /no peer reached/, 'offline server falls back instead of hanging forever')
})