OverviewFilesCommitsIssuesDocsReleasesPeers

favela-surface-android commits / fbb4f74

0.0.29: the atproto boot stops blocking the update state write
THE STUCK BANNER, and it was mine. I added `await bootAtproto(...)` between `send({t:'ready'})` and the OTA block. It opens a corestore, a Hyperswarm and a topic per identity, so on a device that actually holds a DID it does not settle quickly — and everything below it never ran, including `writeState()`. So `app-update.json` kept whatever the previous boot had written. Settings showed a stale version, and `staged !== running` stayed true for ever: a banner offering an update that had ALREADY been applied, unclearable from the device. The phone was running 0.0.28 the whole time and saying 0.0.27. ⚠ Signing is a background concern, exactly like updating. A phone that cannot boot its atproto half must still show its records and must still be able to say which build it is running. Awaiting it made the update mechanism depend on the swarm. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fbb4f747d3a0d0f55ae9ae1ea5c544037365f6fa
by plankatron <fleeky@gmail.com> · 2026-09-11T17:36:36.000Z

Changed files (3) +34 -6

lib/node.js +13 -5
……@@ -140,11 +140,19 @@ export async function boot (storage) {
140140 // ⛔ AFTER `ready`, and non-fatal. Updating and signing are background concerns: a phone that cannot
141141 // boot its atproto half must still show its own records. favela-android's rule — "a ready the UI
142142 // cannot use is worse than a failure it can read".
143 try {
144 state.atproto = await bootAtproto(node, { log: (m, d) => stage(m, d) })
145 } catch (e) {
146 stage('atproto boot failed', { error: (e && e.message) || String(e) })
147 }
143 // ⛔ NOT AWAITED, and this is the whole point. `bootAtproto` opens a corestore, a Hyperswarm and
144 // joins a topic per identity — on a device that actually holds a DID it can take a long time or
145 // never settle at all. Awaiting it here stopped everything BELOW from running, including the OTA
146 // block that writes `app-update.json`. So the settings page kept rendering whatever the previous
147 // boot had written: a stale version, and `staged !== running` for ever — a banner offering an
148 // update that had in fact already been applied, with no way to clear it from the device.
149 //
150 // ⚠ The same class as the rule two lines up. Signing is a BACKGROUND concern, exactly like
151 // updating: a phone that cannot boot its atproto half must still show its own records and must
152 // still be able to say which build it is running.
153 bootAtproto(node, { log: (m, d) => stage(m, d) })
154 .then((h) => { state.atproto = h })
155 .catch((e) => stage('atproto boot failed', { error: (e && e.message) || String(e) }))
148156
149157 // ⛔ START OTA, and note this is what makes `lib/host/ota.js` exist at all. It was copied here with
150158 // the rest of the host layer and nothing imported it — so bare-pack, which walks the MODULE graph
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "favela-surface-android",
3 "version": "0.0.28",
3 "version": "0.0.29",
44 "description": "favela-surface-android \u2014 a React Native shell over a bare-kit worklet running a full favela-peer node (markdown records, no atproto) with favela-surface rendering its pages in a WebView.",
55 "type": "module",
66 "private": true,
test/announce.test.js +20 -0
……@@ -73,3 +73,23 @@ test('announceAll covers the namespace and the rooms the hand-rolled list missed
7373 'and so is the namespace — without this a shared registry is three private ones')
7474 t.ok(rec.joined.includes(room), 'and the rooms, which the hand-rolled list also walked past')
7575})
76
77// ⛔ NOTHING IN boot() MAY BLOCK THE OTA STATE WRITE. `bootAtproto` opens a corestore, a swarm and a
78// topic per identity; awaiting it meant that on a device which actually held a DID, everything below
79// it never ran — including the write of app-update.json. The settings page then rendered the previous
80// boot's snapshot for ever: a stale version, and `staged !== running` permanently true, so the update
81// banner offered an update that had already been applied and could not be cleared from the device.
82//
83// Source-level for the same reason as the test above: the failure is an ordering one inside a
84// function that builds its own swarm, and a behavioural test cannot observe it without a DHT.
85test('boot does not await the atproto boot', (t) => {
86 const src = fs.readFileSync(join(ROOT, 'lib/node.js'), 'utf8')
87 t.absent(/await bootAtproto\(/.test(src),
88 'awaiting it stops the OTA state write, which is what tells settings which build is running')
89 // ⚠ `[\s\S]*?` and not `[^)]*`: the call's own argument contains parentheses, so a
90 // no-close-paren class stops inside it and the assertion fails on correct code.
91 t.ok(/bootAtproto\([\s\S]{0,200}?\.then\(/.test(src),
92 'it still runs, and its result is still recorded')
93 // And the write it must not block still happens.
94 t.ok(/writeState\(\)/.test(src), 'boot still writes the update state')
95})