OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-proto-peer commits / 672b0b3

0.3.112: a DID's boot must not wait on the DHT
`ready()` on both the control and membrane surfaces is `Promise.all([topic.flushed(), …])` — the DHT ANNOUNCE completing, not the surface working. swarm.join() has already registered the topics and connections are served meanwhile; the phone logged five `device control connection`s while boot sat inside that await. On a workstation the flush returns in milliseconds, so this was invisible. Behind carrier-grade NAT it may never resolve — and awaiting it wedged bootSwarmDevice, so bootMultiDevice never returned, favela-android never sent `ready`, and the app sat on its splash screen with a node that was actually up. Only reachable once the DID got far enough to reach these lines, which is why it appeared the moment the two boot bugs before it were fixed. Both announces and the initial membrane projection are now time-boxed (15s/15s/30s); boot continues and they finish in the background. A timeout is REPORTED, naming the surface — never silently skipped. The projection is safe to bound because the event-driven tick re-syncs on the next StateBase advance, so a slow first pass costs freshness, not correctness; sync() reads through a StateBase that on an unadmitted device may never advance at all (#40 §6). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
672b0b34dc94c7762584f9c3252614b058908fbe
by plankatron <fleeky@gmail.com> · 2026-08-10T16:25:40.000Z

Changed files (2) +27 -4

lib/device-node.js +26 -3
……@@ -533,7 +533,28 @@ export async function bootSwarmDevice ({ storage, deviceMnemonic, did, signingKe
533533 // no I/O, so the ordering is free.
534534 const sessions = makeSessions({ did, handle, appPasswordHash, jwtSecret })
535535 serving = serveDevice(device, controlSwarm, { canWrite, blobs, sessions })
536 await serving.ready()
536 // ⛔ TIME-BOXED, because `ready()` on BOTH surfaces is `Promise.all([topic.flushed(), …])` — the DHT
537 // ANNOUNCE completing, which is not the same thing as the surface working. `swarm.join()` has already
538 // registered the topics and connections are served meanwhile: on the phone five `device control
539 // connection`s were logged while boot was still sitting inside this await. On a workstation the flush
540 // returns in milliseconds; behind carrier-grade NAT it can take minutes or never resolve, and awaiting
541 // it wedged bootSwarmDevice → bootMultiDevice never returned → favela-android never sent `ready` and
542 // the app sat on its splash screen with a node that was, in fact, up (hyper-social #40).
543 //
544 // So boot proceeds and the announce finishes in the background. Nothing is skipped and nothing is
545 // swallowed silently: a timeout says so, naming the surface. The membrane projection is time-boxed on
546 // the same reasoning — the event-driven tick re-syncs on the next StateBase advance anyway, so a slow
547 // first pass costs a beat of freshness, never correctness.
548 const awaitOrContinue = async (what, promise, ms = 15000) => {
549 let timer = null
550 const timeout = new Promise((resolve) => { timer = setTimeout(() => resolve('__timeout__'), ms); if (timer && timer.unref) timer.unref() })
551 const outcome = await Promise.race([promise.then(() => '__ok__', (e) => e), timeout])
552 clearTimeout(timer)
553 if (outcome === '__timeout__') console.error(JSON.stringify({ msg: 'boot continued WITHOUT waiting for ' + what + ' — it is still in progress in the background; the surface already accepts connections', did, waitedMs: ms }))
554 else if (outcome !== '__ok__') console.error(JSON.stringify({ msg: what + ' failed (boot continues)', did, error: (outcome && outcome.message) || String(outcome) }))
555 return outcome === '__ok__'
556 }
557 await awaitOrContinue('the control-surface DHT announce', serving.ready())
537558 // projectRepoCore comes straight off the DID's config entry (bootMultiDevice spreads `...entry`),
538559 // exactly like storeBackend. FALSE = the stateless virtual device: no membrane-repo Hyperbee is
539560 // opened, record bytes exist only as the content-addressed files, and a consumer derives the repo
……@@ -541,8 +562,10 @@ export async function bootSwarmDevice ({ storage, deviceMnemonic, did, signingKe
541562 // nothing changes until a DID is explicitly flipped — rolled out one DID and one node at a time.
542563 // Flipping back is instant: the existing core on disk is untouched, just not opened.
543564 membraneServing = await serveMembrane(device, { corestore: membraneStore, swarm: controlSwarm, did, handle, sessions, blobs, projectRepoCore })
544 await membraneServing.ready()
545 await membraneServing.sync().catch(() => {})
565 await awaitOrContinue('the membrane DHT announce', membraneServing.ready())
566 // sync() reads the device's __commit__ log through the StateBase. On a device that is not yet admitted
567 // (roster projected by another machine — #40 §6) that base may never advance, so this too is bounded.
568 await awaitOrContinue('the initial membrane projection', membraneServing.sync(), 30000)
546569 }
547570
548571 const close = async () => {
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "hyper-proto-peer",
3 "version": "0.3.111",
3 "version": "0.3.112",
44 "description": "Self-custody, P2P-native atproto peer — holds the user's key + content as Hypercores and authors+signs its own repo. The source of truth in the peer/bridge split (Model B); atproto-bridge projects it to bsky.",
55 "type": "module",
66 "main": "lib/peer.js",