/**
* durableConnect — event-driven, no-aggressive-deadline connect over an
* injected Hyperswarm.
*
* Replaces the fragile "swarm.once('connection') + setTimeout(15000, reject)"
* model that this transport used to use. That model treats a P2P connection as
* a synchronous timed dial and gives up when the stopwatch wins — which is what
* starved concurrent org-repo pulls: N `git fetch punch://` each spin their own
* swarm, compete for DHT/hole-punch budget, and the losers of that race hit the
* 15s deadline and fail with ZERO objects (observed on ashatron 2026-07-18:
* repos that started in the same burst all failed empty; less-contended ones
* completed). See pear-git org issue #3.
*
* Instead we treat a connection as an event-driven resource — matching how keet
* and hyper-overlay stay durable:
*
* - join(topic, {client:true}) once, then swarm.on('connection') — react to
* EVERY connection as it arrives, and STAY joined so hyperswarm keeps
* re-forming connections across peers while the topic is joined.
* - NO wall-clock connect deadline racing the DHT+hole-punch. A connection
* that takes 2s or 40s under contention is fine — it just waits.
* - Reconcile ACROSS connections: resolve on the FIRST connection that passes
* `validate(conn, info)`; a connection that fails validation is discarded
* and the next is taken (never first-wins-fail).
* - Cancellable via an AbortSignal AND, for callers that have a fallback path
* (git-remote-punch has an HTTPS fallback), an OPTIONAL generous
* `fallbackAfter` no-progress bound. This is a last-resort escape so an
* offline server doesn't hang forever — NOT a connect deadline. It must be
* long enough to never race a live hole-punch under load; the aggressive
* 15s was the bug, a 120s escape is not.
*
* The swarm is INJECTED and never created/destroyed here — the caller owns its
* lifetime (for the git-remote-punch helper that is one process; for a daemon
* it is a long-lived shared swarm).
*
* @param {object} swarm - a joined-capable Hyperswarm (has join + on/off)
* @param {Buffer} topic - 32-byte swarm topic
* @param {object} [opts]
* @param {(conn, info) => (boolean|Promise<boolean>)} [opts.validate] - liveness
* predicate; default accepts the first connection.
* @param {AbortSignal} [opts.signal] - caller cancellation.
* @param {number} [opts.fallbackAfter] - ms; if > 0, reject after this long
* with NO validated connection so the caller can fall back. 0/undefined =
* wait indefinitely (the bound belongs to the caller).
* @returns {Promise<object>} the first connection that passes validate().
*/
export function durableConnect (swarm, topic, opts = {}) {
const validate = opts.validate || (() => true)
const signal = opts.signal
const fallbackAfter = Number(opts.fallbackAfter) || 0
return new Promise((resolve, reject) => {
let settled = false
let fallbackTimer = null
const cleanup = () => {
swarm.off('connection', onConnection)
if (fallbackTimer) clearTimeout(fallbackTimer)
if (signal) { try { signal.removeEventListener('abort', onAbort) } catch {} }
}
const onConnection = (conn, info) => {
if (settled) return
Promise.resolve(validate(conn, info))
.then((ok) => {
if (settled) return
if (ok) {
settled = true
cleanup()
resolve(conn)
} else {
// Stale/dead/wrong peer — discard and keep waiting. hyperswarm
// keeps forming connections to other (live) peers while joined.
try { conn.destroy() } catch {}
}
})
.catch(() => { try { conn.destroy() } catch {} })
}
const onAbort = () => {
if (settled) return
settled = true
cleanup()
reject(new Error('durable connect aborted'))
}
if (signal) {
if (signal.aborted) return onAbort()
signal.addEventListener('abort', onAbort)
}
// Generous no-progress escape (NOT a connect race) — only for callers that
// opt in because they have a fallback (e.g. HTTPS). Waits by default.
if (fallbackAfter > 0) {
fallbackTimer = setTimeout(() => {
if (settled) return
settled = true
cleanup()
reject(new Error('durable connect: no peer reached after ' + fallbackAfter + 'ms'))
}, fallbackAfter)
// NOT unref'd: this bound MUST fire even when nothing else keeps the loop
// alive, so an offline server falls back instead of hanging. cleanup()
// clears it the moment a connection validates or the caller aborts.
}
// on(), not once() — react to EVERY connection until one validates.
swarm.on('connection', onConnection)
swarm.join(topic, { server: false, client: true })
})
}
export default durableConnect