OverviewFilesCommitsIssuesDocsReleasesPeers

favela-peer commits / 063a5aa

0.0.43: a mirror-stats RPC, and hook writers per pass instead of once
Two changes, and the first exists because of the second. ★ `mirror-stats` RPC. Three fixes in a row shipped a trigger that never fired, and nothing on a running node could say so — each time the only evidence was how LONG a record took to appear, which is inference, not measurement. stats() now reports append / connection / stale / boot counts plus how many cores are actually hooked. If append and connection stay at zero while stale climbs, the events are dead, and that is a fact you can read. ⛔ AND THE HOOKS WERE ATTACHING TO NOTHING. watch() snapshotted base.writers inside hold(). ActiveWriters IS iterable, so the loop ran — but its `active` map is EMPTY when a freshly-opened remote feed returns from hold(), because writers are only added once the system core has replicated. So it hooked zero cores, silently, and the "no writer cores to watch" log went to a no-op logger. That is the THIRD time in this one feature that doing a thing once at open time ran before the data it needed existed: the boot sweep, then the watcher, now the hooks. So watch() now only REGISTERS the base, and hooking happens in every pass — idempotent, a Map lookup after the first, and incapable of being early. It also picks up writers admitted later, which the snapshot never could. ⚠ Hooks are keyed on the CORE OBJECT, not core.id or core.key. The first draft keyed on an identifier and skipped any core that had not exposed one yet — silently, which is the exact failure mode this trigger has now had three times. The unit test caught it only because a fake core has no id. favela-peer 336/336 tests, 801/801 asserts Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
063a5aab31f12522b9118a700d5454b75b5f6ecd
by plankatron <fleeky@gmail.com> · 2026-09-08T19:00:04.000Z

Changed files (5) +97 -78

.pgh-lock.json +43 -43
……@@ -1,9 +1,9 @@
11{
22 "name": "favela-peer",
3 "version": "0.0.41",
3 "version": "0.0.42",
44 "runtime": "bare",
5 "rootTreeHash": "5e5291250d721374a84f3649ebfe79da2535bd06c0edd4a7a4f2c8733e9daa60",
6 "publishedAt": "2026-09-08T18:37:47.228Z",
5 "rootTreeHash": "ef10bb20c36ea5e78eb287b295cc60fb037d05879b97033bd36aec1901b2f9a7",
6 "publishedAt": "2026-09-08T18:44:13.359Z",
77 "deps": [
88 {
99 "name": "@hyperswarm/secret-stream",
……@@ -3016,8 +3016,8 @@
30163016 "tree": "c5f18b3620514f54714aeb351f074adface216a335242e323e33d28d261376fe"
30173017 }
30183018 ],
3019 "reason": "version-splits only: dry-run reports 0 code-differs",
3020 "acceptedAt": "2026-09-08T18:37:47.243Z"
3019 "reason": "version-splits only: 0 code-differs",
3020 "acceptedAt": "2026-09-08T18:44:13.372Z"
30213021 },
30223022 {
30233023 "type": "VERSION_CONFLICT",
……@@ -3066,8 +3066,8 @@
30663066 "tree": "6bb5233025d1c30e4f181e29b6ab5af549d8909c7070c7b4438b30f1a4e76ce2"
30673067 }
30683068 ],
3069 "reason": "version-splits only: dry-run reports 0 code-differs",
3070 "acceptedAt": "2026-09-08T18:37:47.243Z"
3069 "reason": "version-splits only: 0 code-differs",
3070 "acceptedAt": "2026-09-08T18:44:13.372Z"
30713071 },
30723072 {
30733073 "type": "VERSION_CONFLICT",
……@@ -3081,8 +3081,8 @@
30813081 "tree": "58c0bf8698af19464d0ee8354a77ed1ac96d3a5fa18aa4e794686cfbbf080e5d"
30823082 }
30833083 ],
3084 "reason": "version-splits only: dry-run reports 0 code-differs",
3085 "acceptedAt": "2026-09-08T18:37:47.243Z"
3084 "reason": "version-splits only: 0 code-differs",
3085 "acceptedAt": "2026-09-08T18:44:13.372Z"
30863086 },
30873087 {
30883088 "type": "VERSION_CONFLICT",
……@@ -3106,8 +3106,8 @@
31063106 "tree": "bdebba5a532ce4846b49f1e5ffad774becfc44182d0c3737f7e441ab42f4c43d"
31073107 }
31083108 ],
3109 "reason": "version-splits only: dry-run reports 0 code-differs",
3110 "acceptedAt": "2026-09-08T18:37:47.243Z"
3109 "reason": "version-splits only: 0 code-differs",
3110 "acceptedAt": "2026-09-08T18:44:13.372Z"
31113111 },
31123112 {
31133113 "type": "VERSION_CONFLICT",
……@@ -3141,8 +3141,8 @@
31413141 "tree": "b799e6ba8726aa951b45f12cc7ecffe5f4e9ee1e8adf09a34d026d9d5658828a"
31423142 }
31433143 ],
3144 "reason": "version-splits only: dry-run reports 0 code-differs",
3145 "acceptedAt": "2026-09-08T18:37:47.243Z"
3144 "reason": "version-splits only: 0 code-differs",
3145 "acceptedAt": "2026-09-08T18:44:13.372Z"
31463146 },
31473147 {
31483148 "type": "VERSION_CONFLICT",
……@@ -3156,8 +3156,8 @@
31563156 "tree": "e92100a4f821a638ac99c1407e4c7e784f8eb9661e6d9119ecbec4faae77cc7e"
31573157 }
31583158 ],
3159 "reason": "version-splits only: dry-run reports 0 code-differs",
3160 "acceptedAt": "2026-09-08T18:37:47.243Z"
3159 "reason": "version-splits only: 0 code-differs",
3160 "acceptedAt": "2026-09-08T18:44:13.372Z"
31613161 },
31623162 {
31633163 "type": "VERSION_CONFLICT",
……@@ -3171,8 +3171,8 @@
31713171 "tree": "1f5de2daa08643a5cb8bce2153ca4d55d4504fb366f1f1ef80bca1700ec085f7"
31723172 }
31733173 ],
3174 "reason": "version-splits only: dry-run reports 0 code-differs",
3175 "acceptedAt": "2026-09-08T18:37:47.243Z"
3174 "reason": "version-splits only: 0 code-differs",
3175 "acceptedAt": "2026-09-08T18:44:13.372Z"
31763176 },
31773177 {
31783178 "type": "VERSION_CONFLICT",
……@@ -3186,8 +3186,8 @@
31863186 "tree": "579bdc1eaabbca726d3916f1fc74cfd4986653f6748632db586817a2a8caabea"
31873187 }
31883188 ],
3189 "reason": "version-splits only: dry-run reports 0 code-differs",
3190 "acceptedAt": "2026-09-08T18:37:47.243Z"
3189 "reason": "version-splits only: 0 code-differs",
3190 "acceptedAt": "2026-09-08T18:44:13.372Z"
31913191 },
31923192 {
31933193 "type": "VERSION_CONFLICT",
……@@ -3201,8 +3201,8 @@
32013201 "tree": "967cf214778dbacd7cc7cc3850ffdff6a6f145b4bd73de741d0938d7d84d33d5"
32023202 }
32033203 ],
3204 "reason": "version-splits only: dry-run reports 0 code-differs",
3205 "acceptedAt": "2026-09-08T18:37:47.243Z"
3204 "reason": "version-splits only: 0 code-differs",
3205 "acceptedAt": "2026-09-08T18:44:13.372Z"
32063206 },
32073207 {
32083208 "type": "VERSION_CONFLICT",
……@@ -3231,8 +3231,8 @@
32313231 "tree": "35b214bfbf85f8bbcb9f1926470e77f4e240d9b5045b07f81ce83fec24c1380c"
32323232 }
32333233 ],
3234 "reason": "version-splits only: dry-run reports 0 code-differs",
3235 "acceptedAt": "2026-09-08T18:37:47.243Z"
3234 "reason": "version-splits only: 0 code-differs",
3235 "acceptedAt": "2026-09-08T18:44:13.372Z"
32363236 },
32373237 {
32383238 "type": "VERSION_CONFLICT",
……@@ -3246,8 +3246,8 @@
32463246 "tree": "67c4c05f69061f65a2548595c43282be2da8e0934e6ce347afa034b29cc40c81"
32473247 }
32483248 ],
3249 "reason": "version-splits only: dry-run reports 0 code-differs",
3250 "acceptedAt": "2026-09-08T18:37:47.243Z"
3249 "reason": "version-splits only: 0 code-differs",
3250 "acceptedAt": "2026-09-08T18:44:13.372Z"
32513251 },
32523252 {
32533253 "type": "VERSION_CONFLICT",
……@@ -3276,8 +3276,8 @@
32763276 "tree": "4ef8a03ba96b1488cb7ee35dc582b14d096ae4b3fab5481186c94516d434fe18"
32773277 }
32783278 ],
3279 "reason": "version-splits only: dry-run reports 0 code-differs",
3280 "acceptedAt": "2026-09-08T18:37:47.243Z"
3279 "reason": "version-splits only: 0 code-differs",
3280 "acceptedAt": "2026-09-08T18:44:13.372Z"
32813281 },
32823282 {
32833283 "type": "VERSION_CONFLICT",
……@@ -3306,8 +3306,8 @@
33063306 "tree": "5fc49d698f6ddfd653b3cdaa72ec572d6804293e2e9c3e98303f69417115e903"
33073307 }
33083308 ],
3309 "reason": "version-splits only: dry-run reports 0 code-differs",
3310 "acceptedAt": "2026-09-08T18:37:47.243Z"
3309 "reason": "version-splits only: 0 code-differs",
3310 "acceptedAt": "2026-09-08T18:44:13.372Z"
33113311 },
33123312 {
33133313 "type": "VERSION_CONFLICT",
……@@ -3321,8 +3321,8 @@
33213321 "tree": "d0099e1743480f28a08c7374d20ad5048088475180ab606626b63acd59e365a5"
33223322 }
33233323 ],
3324 "reason": "version-splits only: dry-run reports 0 code-differs",
3325 "acceptedAt": "2026-09-08T18:37:47.243Z"
3324 "reason": "version-splits only: 0 code-differs",
3325 "acceptedAt": "2026-09-08T18:44:13.372Z"
33263326 },
33273327 {
33283328 "type": "VERSION_CONFLICT",
……@@ -3336,8 +3336,8 @@
33363336 "tree": "60429c7756f039ec2b3e5240f702dee450c1da19ad6de06900a3877c6c72bb52"
33373337 }
33383338 ],
3339 "reason": "version-splits only: dry-run reports 0 code-differs",
3340 "acceptedAt": "2026-09-08T18:37:47.243Z"
3339 "reason": "version-splits only: 0 code-differs",
3340 "acceptedAt": "2026-09-08T18:44:13.372Z"
33413341 },
33423342 {
33433343 "type": "VERSION_CONFLICT",
……@@ -3366,8 +3366,8 @@
33663366 "tree": "6fc80ad1ccbfb046613f36299032f21a4752e2614ff8d88a7a5bd7ce66fc1293"
33673367 }
33683368 ],
3369 "reason": "version-splits only: dry-run reports 0 code-differs",
3370 "acceptedAt": "2026-09-08T18:37:47.243Z"
3369 "reason": "version-splits only: 0 code-differs",
3370 "acceptedAt": "2026-09-08T18:44:13.372Z"
33713371 },
33723372 {
33733373 "type": "VERSION_CONFLICT",
……@@ -3386,8 +3386,8 @@
33863386 "tree": "92424acff1a50468df203528e41739f44f602904abc4beb34500ce172c6d0ad2"
33873387 }
33883388 ],
3389 "reason": "version-splits only: dry-run reports 0 code-differs",
3390 "acceptedAt": "2026-09-08T18:37:47.243Z"
3389 "reason": "version-splits only: 0 code-differs",
3390 "acceptedAt": "2026-09-08T18:44:13.372Z"
33913391 },
33923392 {
33933393 "type": "VERSION_CONFLICT",
……@@ -3411,8 +3411,8 @@
34113411 "tree": "36d01631ddee74ac2ce62971abd7c0ac30562da8dfd6495480da56c534eb80b7"
34123412 }
34133413 ],
3414 "reason": "version-splits only: dry-run reports 0 code-differs",
3415 "acceptedAt": "2026-09-08T18:37:47.243Z"
3414 "reason": "version-splits only: 0 code-differs",
3415 "acceptedAt": "2026-09-08T18:44:13.372Z"
34163416 },
34173417 {
34183418 "type": "VERSION_CONFLICT",
……@@ -3426,8 +3426,8 @@
34263426 "tree": "bba869f5c85b11bce5671d57a87cd2793b490588867cd07fbe94cad796114970"
34273427 }
34283428 ],
3429 "reason": "version-splits only: dry-run reports 0 code-differs",
3430 "acceptedAt": "2026-09-08T18:37:47.243Z"
3429 "reason": "version-splits only: 0 code-differs",
3430 "acceptedAt": "2026-09-08T18:44:13.372Z"
34313431 },
34323432 {
34333433 "type": "VERSION_CONFLICT",
……@@ -3441,8 +3441,8 @@
34413441 "tree": "2ecc91a8c3595dda5dfc86ace62a70875520adc4e588a2ac14945dbc4313c450"
34423442 }
34433443 ],
3444 "reason": "version-splits only: dry-run reports 0 code-differs",
3445 "acceptedAt": "2026-09-08T18:37:47.243Z"
3444 "reason": "version-splits only: 0 code-differs",
3445 "acceptedAt": "2026-09-08T18:44:13.372Z"
34463446 }
34473447 ]
34483448}
lib/mirror.js +40 -32
……@@ -46,7 +46,31 @@ export function createMirror ({ feeds, concurrency = DEFAULT_CONCURRENCY, deboun
4646 // If `append` and `connection` stay at zero while `stale` climbs, the events are dead, and that
4747 // is a fact anyone can read rather than infer from how long something took.
4848 const fired = { append: 0, connection: 0, stale: 0, boot: 0 }
49 const watched = []
49 const bases = []
50 const hooked = new Map()
51
52 // Attach an 'append' listener to every writer core we have not already hooked. A hypercore emits
53 // append when blocks land, INCLUDING replicated ones — the earliest honest signal someone else
54 // wrote something. Idempotent, so calling it every pass is free after the first.
55 function rehook (onAppend) {
56 for (const base of bases) {
57 let writers
58 try { writers = base.writers } catch { continue }
59 if (!writers) continue
60 try {
61 for (const w of writers) {
62 const core = w && w.core
63 if (!core || typeof core.on !== 'function') continue
64 // ⛔ Keyed on the CORE OBJECT, not on core.id or core.key. Keying on an identifier meant
65 // a core that had not yet exposed one was silently skipped — and "silently skipped" is
66 // precisely the failure this whole trigger has already had three times.
67 if (hooked.has(core)) continue
68 core.on('append', onAppend)
69 hooked.set(core, onAppend)
70 }
71 } catch { /* a writer set mid-change is not a failure of the pass */ }
72 }
73 }
5074
5175 async function mirrorFeed (key, feed) {
5276 // ⛔ `pointers()` and not `list()` or `unresolved()`. Both of those RESOLVE every record, which
……@@ -108,6 +132,9 @@ export function createMirror ({ feeds, concurrency = DEFAULT_CONCURRENCY, deboun
108132 async pass () {
109133 if (stopped || running) return { fetched: 0, failed: 0 }
110134 running = true
135 // Hook whatever writers exist NOW. On the first pass a remote feed usually has none; by the
136 // second it does, and from then on every remote append triggers a sweep of its own.
137 rehook(() => this.schedule('append'))
111138 let fetched = 0
112139 let failed = 0
113140 try {
……@@ -151,34 +178,14 @@ export function createMirror ({ feeds, concurrency = DEFAULT_CONCURRENCY, deboun
151178 */
152179 watch (base) {
153180 if (!base) return () => {}
154 const onAppend = () => this.schedule()
155
156 // ⛔ THE WRITER CORES, not the autobee. autobee emits no "view advanced" event: its whole
157 // surface is interrupt / error / rotate-local-writer / move-to, and the one `emit('update')`
158 // fires inside _onError on an INTERRUPT. 0.0.40 subscribed to it and therefore never fired —
159 // caught only by an end-to-end run, because a fake emitter emits whatever you tell it to.
160 //
161 // A hypercore emits 'append' when blocks land, INCLUDING replicated ones, which is the
162 // earliest honest signal that someone else wrote something.
163 //
164 // ⚠ Writers admitted later are not hooked until the next boot. The periodic sweep below is
165 // what stops that being a hole.
166 let hooked = 0
167 try {
168 for (const w of (base.writers || [])) {
169 if (!w || !w.core || typeof w.core.on !== 'function') continue
170 w.core.on('append', onAppend)
171 watched.push([w.core, onAppend])
172 hooked++
173 }
174 } catch (err) {
175 log({ msg: 'mirror: could not watch writer cores', error: err.message })
176 }
177 if (!hooked) log({ msg: 'mirror: no writer cores to watch — relying on the periodic sweep' })
178 return () => {
179 for (const [c, fn] of watched) { try { c.off('append', fn) } catch {} }
180 watched.length = 0
181 }
181 bases.push(base)
182 // ⛔ REGISTERED, NOT HOOKED — hooking happens in every pass. `base.writers` is an
183 // ActiveWriters whose `active` map is EMPTY when a freshly-opened remote feed returns from
184 // hold(): writers are only added once the system core has replicated. Hooking once here
185 // therefore attached to NOTHING, silently, and that is the third time in this feature that
186 // doing a thing once at open time ran before the data it needed existed. Re-hooking per pass
187 // costs a Map lookup and cannot be early.
188 return () => { bases.length = 0 }
182189 },
183190
184191 /**
……@@ -213,14 +220,15 @@ export function createMirror ({ feeds, concurrency = DEFAULT_CONCURRENCY, deboun
213220 },
214221
215222 /** Which trigger has actually been firing. `stale` climbing alone means the events are dead. */
216 stats () { return { ...fired, lastPassAt } },
223 stats () { return { ...fired, hooked: hooked.size, bases: bases.length, lastPassAt } },
217224
218225 stop () {
219226 stopped = true
220227 if (timer) { clearTimeout(timer); timer = null }
221228 if (interval) { clearInterval(interval); interval = null }
222 for (const [c, fn] of watched) { try { c.off('append', fn) } catch {} }
223 watched.length = 0
229 for (const [c, fn] of hooked) { try { c.off('append', fn) } catch {} }
230 hooked.clear()
231 bases.length = 0
224232 }
225233 }
226234}
lib/rpc.js +7 -0
……@@ -415,6 +415,13 @@ export async function dispatch (node, cmd, a) {
415415 // ⚠ Reported, never folded into a listing: a record we hold a pointer for and cannot read is
416416 // not an absent record, and the CLI must be able to say which.
417417 case 'unresolved': return peer.unresolved()
418
419 // ⛔ WHICH TRIGGER IS ACTUALLY FIRING. Built because three fixes in a row shipped a mirror
420 // trigger that never fired, and nothing on a running node could say so — each time the only
421 // evidence was how LONG a record took to appear, which is inference, not measurement. If
422 // `append` and `connection` stay at zero while `stale` climbs, the events are dead. That is now
423 // a fact you can read.
424 case 'mirror-stats': return node._mirror ? node._mirror.stats() : { mirror: 'not running' }
418425 default:
419426 throw new Error('unknown command: ' + cmd)
420427 }
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "favela-peer",
3 "version": "0.0.42",
3 "version": "0.0.43",
44 "description": "The favela daemon — feeds and documents of markdown records over Hypercore. No atproto, no ActivityPub.",
55 "type": "module",
66 "scripts": {
test/mirror.test.js +6 -2
……@@ -171,8 +171,9 @@ test('★★ a log that advances AFTER the boot pass still gets mirrored', async
171171 const m = createMirror({ feeds: () => [{ key: 'f1', feed }], debounceMs: 10, log: () => {} })
172172 m.watch(base)
173173
174 await m.pass()
174 await m.pass() // the boot pass: finds nothing, and attaches the append hooks
175175 t.is(fetched.length, 0, 'boot pass finds nothing — the log has not arrived')
176 t.is(m.stats().hooked, 1, 'but it DID hook the writer core — the thing 0.0.42 silently did not')
176177
177178 names = ['late'] // the log replicates
178179 base.emit() // autobee says the view advanced
……@@ -191,10 +192,12 @@ test('★ a burst of updates coalesces into one pass', async (t) => {
191192
192193 const m = createMirror({ feeds: () => [{ key: 'f1', feed }], debounceMs: 20, log: () => {} })
193194 m.watch(base)
195 await m.pass() // ⛔ hooks attach during a PASS, not at watch() — writers do not exist yet at open
196 const after = passes
194197 for (let i = 0; i < 10; i++) base.emit()
195198 await new Promise((r) => setTimeout(r, 80))
196199
197 t.is(passes, 1, 'ten view changes, one sweep — autobee emits far more often than records arrive')
200 t.is(passes - after, 1, 'ten appends, one sweep — a core appends far more often than records land')
198201})
199202
200203test('★ stop() detaches the watcher', async (t) => {
……@@ -298,6 +301,7 @@ test('★★ the staleness ceiling skips itself when events are alive', async (t
298301 m.every(40)
299302 const base = fakeBase()
300303 m.watch(base)
304 await m.pass() // attaches the hooks
301305
302306 // Keep events flowing faster than the ceiling.
303307 for (let i = 0; i < 5; i++) { base.emit(); await new Promise((r) => setTimeout(r, 20)) }