OverviewFilesCommitsIssuesDocsReleasesPeers

favela-peer commits / 8394507

0.0.44: remove the ceiling timer — the events do the work
The 5-minute sweep is gone, along with its `stale` counter and the every() call. stats() now reports append / connection / boot / hooked / bases. It was there because I did not trust my own trigger, which is not a reason. It was also the wrong shape: an unconditional sweep makes a dead event hook look like a working feature, and it nearly did — ~50s to mirror in the 0.0.42 run was the timer's signature, not a 2s debounce, and I was about to write that up as a pass. The measured case for removing it, from a running node: append 1 · connection 3 · stale 1 · hooked 4 · bases 2 A record reached a following machine in 9s carried by the append hook, with stale at 0 at that moment. The one time the ceiling did fire was during the ~17 minutes the author was switched off — when there was nothing to fetch and nothing to catch. Its return was picked up by `connection`, on its own. ★ AND THE ABSENCE IT WAS SUPPOSED TO EXPRESS IS AN EVENT. A peer being gone is 'close'; their return is 'connection'; a connection open-but-dead becomes 'close' via the keepalive secret-stream already runs. The transport converts silence into an event. What remains — a fetch that hangs — already has a clock at the right layer, ensureLocal's 5s timeout. So this is not the standing rule's "timers express what events cannot". Nothing here was absence: "records whose bytes I do not hold" is positive state, computable at any instant, and re-checking it on a clock was polling for data. ⚠ Residual, untested on hardware: a brand-new writer core that appends before any pass has rehooked it. The next connection or append from any other writer sweeps and picks it up, so it is bounded, not lost. favela-peer 334/334 tests, 797/797 asserts Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8394507c774d3763f3ad965ca78f9389efabaaa0
by plankatron <fleeky@gmail.com> · 2026-09-08T19:23:52.000Z

Changed files (5) +30 -101

.pgh-lock.json +23 -23
……@@ -1,9 +1,9 @@
11{
22 "name": "favela-peer",
3 "version": "0.0.42",
3 "version": "0.0.43",
44 "runtime": "bare",
5 "rootTreeHash": "ef10bb20c36ea5e78eb287b295cc60fb037d05879b97033bd36aec1901b2f9a7",
6 "publishedAt": "2026-09-08T18:44:13.359Z",
5 "rootTreeHash": "91f004fdec4b38dd4c751e1e99702893b60b2ba5010e4b92015a945fb1803f17",
6 "publishedAt": "2026-09-08T19:00:06.696Z",
77 "deps": [
88 {
99 "name": "@hyperswarm/secret-stream",
……@@ -3017,7 +3017,7 @@
30173017 }
30183018 ],
30193019 "reason": "version-splits only: 0 code-differs",
3020 "acceptedAt": "2026-09-08T18:44:13.372Z"
3020 "acceptedAt": "2026-09-08T19:00:06.709Z"
30213021 },
30223022 {
30233023 "type": "VERSION_CONFLICT",
……@@ -3067,7 +3067,7 @@
30673067 }
30683068 ],
30693069 "reason": "version-splits only: 0 code-differs",
3070 "acceptedAt": "2026-09-08T18:44:13.372Z"
3070 "acceptedAt": "2026-09-08T19:00:06.709Z"
30713071 },
30723072 {
30733073 "type": "VERSION_CONFLICT",
……@@ -3082,7 +3082,7 @@
30823082 }
30833083 ],
30843084 "reason": "version-splits only: 0 code-differs",
3085 "acceptedAt": "2026-09-08T18:44:13.372Z"
3085 "acceptedAt": "2026-09-08T19:00:06.709Z"
30863086 },
30873087 {
30883088 "type": "VERSION_CONFLICT",
……@@ -3107,7 +3107,7 @@
31073107 }
31083108 ],
31093109 "reason": "version-splits only: 0 code-differs",
3110 "acceptedAt": "2026-09-08T18:44:13.372Z"
3110 "acceptedAt": "2026-09-08T19:00:06.709Z"
31113111 },
31123112 {
31133113 "type": "VERSION_CONFLICT",
……@@ -3142,7 +3142,7 @@
31423142 }
31433143 ],
31443144 "reason": "version-splits only: 0 code-differs",
3145 "acceptedAt": "2026-09-08T18:44:13.372Z"
3145 "acceptedAt": "2026-09-08T19:00:06.709Z"
31463146 },
31473147 {
31483148 "type": "VERSION_CONFLICT",
……@@ -3157,7 +3157,7 @@
31573157 }
31583158 ],
31593159 "reason": "version-splits only: 0 code-differs",
3160 "acceptedAt": "2026-09-08T18:44:13.372Z"
3160 "acceptedAt": "2026-09-08T19:00:06.709Z"
31613161 },
31623162 {
31633163 "type": "VERSION_CONFLICT",
……@@ -3172,7 +3172,7 @@
31723172 }
31733173 ],
31743174 "reason": "version-splits only: 0 code-differs",
3175 "acceptedAt": "2026-09-08T18:44:13.372Z"
3175 "acceptedAt": "2026-09-08T19:00:06.709Z"
31763176 },
31773177 {
31783178 "type": "VERSION_CONFLICT",
……@@ -3187,7 +3187,7 @@
31873187 }
31883188 ],
31893189 "reason": "version-splits only: 0 code-differs",
3190 "acceptedAt": "2026-09-08T18:44:13.372Z"
3190 "acceptedAt": "2026-09-08T19:00:06.709Z"
31913191 },
31923192 {
31933193 "type": "VERSION_CONFLICT",
……@@ -3202,7 +3202,7 @@
32023202 }
32033203 ],
32043204 "reason": "version-splits only: 0 code-differs",
3205 "acceptedAt": "2026-09-08T18:44:13.372Z"
3205 "acceptedAt": "2026-09-08T19:00:06.709Z"
32063206 },
32073207 {
32083208 "type": "VERSION_CONFLICT",
……@@ -3232,7 +3232,7 @@
32323232 }
32333233 ],
32343234 "reason": "version-splits only: 0 code-differs",
3235 "acceptedAt": "2026-09-08T18:44:13.372Z"
3235 "acceptedAt": "2026-09-08T19:00:06.709Z"
32363236 },
32373237 {
32383238 "type": "VERSION_CONFLICT",
……@@ -3247,7 +3247,7 @@
32473247 }
32483248 ],
32493249 "reason": "version-splits only: 0 code-differs",
3250 "acceptedAt": "2026-09-08T18:44:13.372Z"
3250 "acceptedAt": "2026-09-08T19:00:06.709Z"
32513251 },
32523252 {
32533253 "type": "VERSION_CONFLICT",
……@@ -3277,7 +3277,7 @@
32773277 }
32783278 ],
32793279 "reason": "version-splits only: 0 code-differs",
3280 "acceptedAt": "2026-09-08T18:44:13.372Z"
3280 "acceptedAt": "2026-09-08T19:00:06.709Z"
32813281 },
32823282 {
32833283 "type": "VERSION_CONFLICT",
……@@ -3307,7 +3307,7 @@
33073307 }
33083308 ],
33093309 "reason": "version-splits only: 0 code-differs",
3310 "acceptedAt": "2026-09-08T18:44:13.372Z"
3310 "acceptedAt": "2026-09-08T19:00:06.709Z"
33113311 },
33123312 {
33133313 "type": "VERSION_CONFLICT",
……@@ -3322,7 +3322,7 @@
33223322 }
33233323 ],
33243324 "reason": "version-splits only: 0 code-differs",
3325 "acceptedAt": "2026-09-08T18:44:13.372Z"
3325 "acceptedAt": "2026-09-08T19:00:06.709Z"
33263326 },
33273327 {
33283328 "type": "VERSION_CONFLICT",
……@@ -3337,7 +3337,7 @@
33373337 }
33383338 ],
33393339 "reason": "version-splits only: 0 code-differs",
3340 "acceptedAt": "2026-09-08T18:44:13.372Z"
3340 "acceptedAt": "2026-09-08T19:00:06.709Z"
33413341 },
33423342 {
33433343 "type": "VERSION_CONFLICT",
……@@ -3367,7 +3367,7 @@
33673367 }
33683368 ],
33693369 "reason": "version-splits only: 0 code-differs",
3370 "acceptedAt": "2026-09-08T18:44:13.372Z"
3370 "acceptedAt": "2026-09-08T19:00:06.709Z"
33713371 },
33723372 {
33733373 "type": "VERSION_CONFLICT",
……@@ -3387,7 +3387,7 @@
33873387 }
33883388 ],
33893389 "reason": "version-splits only: 0 code-differs",
3390 "acceptedAt": "2026-09-08T18:44:13.372Z"
3390 "acceptedAt": "2026-09-08T19:00:06.709Z"
33913391 },
33923392 {
33933393 "type": "VERSION_CONFLICT",
……@@ -3412,7 +3412,7 @@
34123412 }
34133413 ],
34143414 "reason": "version-splits only: 0 code-differs",
3415 "acceptedAt": "2026-09-08T18:44:13.372Z"
3415 "acceptedAt": "2026-09-08T19:00:06.709Z"
34163416 },
34173417 {
34183418 "type": "VERSION_CONFLICT",
……@@ -3427,7 +3427,7 @@
34273427 }
34283428 ],
34293429 "reason": "version-splits only: 0 code-differs",
3430 "acceptedAt": "2026-09-08T18:44:13.372Z"
3430 "acceptedAt": "2026-09-08T19:00:06.709Z"
34313431 },
34323432 {
34333433 "type": "VERSION_CONFLICT",
……@@ -3442,7 +3442,7 @@
34423442 }
34433443 ],
34443444 "reason": "version-splits only: 0 code-differs",
3445 "acceptedAt": "2026-09-08T18:44:13.372Z"
3445 "acceptedAt": "2026-09-08T19:00:06.709Z"
34463446 }
34473447 ]
34483448}
lib/mirror.js +6 -28
……@@ -39,13 +39,11 @@ export function createMirror ({ feeds, concurrency = DEFAULT_CONCURRENCY, deboun
3939 let stopped = false
4040 let running = false
4141 let timer = null
42 let interval = null
4342 let lastPassAt = 0
44 // ⛔ Counted per TRIGGER so a dead hook is OBSERVABLE. 0.0.40's watcher never fired and an
45 // unconditional timer would have hidden that — the feature would have looked like it worked.
46 // If `append` and `connection` stay at zero while `stale` climbs, the events are dead, and that
47 // is a fact anyone can read rather than infer from how long something took.
48 const fired = { append: 0, connection: 0, stale: 0, boot: 0 }
43 // ⛔ Counted per TRIGGER so a dead hook is OBSERVABLE. 0.0.40's watcher never fired and nothing
44 // on a running node could say so — the only evidence was how LONG a record took to appear, which
45 // is inference. These counters are what turned that into a measurement.
46 const fired = { append: 0, connection: 0, boot: 0 }
4947 const bases = []
5048 const hooked = new Map()
5149
……@@ -198,34 +196,14 @@ export function createMirror ({ feeds, concurrency = DEFAULT_CONCURRENCY, deboun
198196 onPeer () { this.schedule('connection') },
199197
200198 /**
201 * A slow sweep, so a missed signal cannot strand a feed for ever.
202 *
203 * ⚠ Yes, a timer — and deliberately. The rule this fleet keeps re-learning is not to POLL THE
204 * NETWORK for something an event should tell you; this polls a local bee and stats local files,
205 * and touches the network only when something is genuinely missing. Given that 0.0.40 shipped a
206 * trigger that silently never fired, a floor that cannot be wrong is worth its cost.
199 * Which trigger is doing the work. `hooked` is the one that matters: 0.0.42 hooked ZERO cores
200 * and looked identical to a working feature from the outside.
207201 */
208 every (ms = 300_000) {
209 if (stopped || interval) return
210 interval = setInterval(() => {
211 // ⛔ EVENT-INFORMED: skipped entirely if an event already swept inside the window. Events
212 // are the latency floor; this is only the staleness ceiling, and it must not run in
213 // parallel with them or it masks a dead hook — which is exactly how 0.0.40 would have
214 // passed. Matches the bridge's watchdog: an append inside the window SKIPS the poll.
215 if (Date.now() - lastPassAt < ms) return
216 fired.stale++
217 this.pass().catch(() => {})
218 }, ms)
219 if (interval && interval.unref) interval.unref()
220 },
221
222 /** Which trigger has actually been firing. `stale` climbing alone means the events are dead. */
223202 stats () { return { ...fired, hooked: hooked.size, bases: bases.length, lastPassAt } },
224203
225204 stop () {
226205 stopped = true
227206 if (timer) { clearTimeout(timer); timer = null }
228 if (interval) { clearInterval(interval); interval = null }
229207 for (const [c, fn] of hooked) { try { c.off('append', fn) } catch {} }
230208 hooked.clear()
231209 bases.length = 0
lib/node.js +0 -5
……@@ -258,11 +258,6 @@ export class FavelaNode {
258258 this._onMirrorPeer = () => this._mirror.onPeer()
259259 this.swarm.on('connection', this._onMirrorPeer)
260260 }
261 // ⚠ And a staleness CEILING, event-informed: it skips itself entirely when events are alive, so
262 // it cannot mask a dead hook the way 0.0.40's unconditional sweep would have. Events are the
263 // latency floor; this only catches what emits nothing at all.
264 this._mirror.every()
265
266261 return this
267262 }
268263
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "favela-peer",
3 "version": "0.0.43",
3 "version": "0.0.44",
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 +0 -44
……@@ -239,27 +239,6 @@ test('★★ a sweep refreshes the view before reading pointers', async (t) => {
239239 t.alike(order, ['update', 'pointers', 'fetch:fresh'], 'refresh, then read, then fetch')
240240})
241241
242// ⚠ A timer on purpose. 0.0.40 shipped a trigger that silently never fired; a floor that cannot be
243// wrong is worth its cost. It polls a local bee and stats local files — not the network.
244test('★ the periodic sweep is a floor under a missed signal', async (t) => {
245 let passes = 0
246 const feed = {
247 async update () {},
248 async pointers () { passes++; return [] },
249 store: { async get () { return null }, async ensureLocal () { return null } }
250 }
251 const m = createMirror({ feeds: () => [{ key: 'f1', feed }], log: () => {} })
252
253 m.every(20)
254 await new Promise((r) => setTimeout(r, 90))
255 m.stop()
256 const seen = passes
257 await new Promise((r) => setTimeout(r, 50))
258
259 t.ok(seen >= 2, 'it kept sweeping with no event at all (' + seen + ')')
260 t.is(passes, seen, 'and stop() ended it')
261})
262
263242// ⛔ A PEER COMING BACK IS AN EVENT, NOT AN ABSENCE. A record stays unresolved because nobody
264243// holding its bytes is online; their return is a swarm 'connection'. Re-checking that on a clock
265244// was polling for data — the part of the rule that IS absolute.
……@@ -287,26 +266,3 @@ test('★★ a peer connecting retries what could not be fetched', async (t) =>
287266 t.is(m.stats().connection, 1, 'and the connection trigger is what did it')
288267})
289268
290// ⛔ THE PROPERTY THAT WOULD HAVE CAUGHT 0.0.40. An unconditional timer makes a dead event hook
291// look like a working feature. Event-informed means the ceiling skips itself when events are alive
292// — so `stale` climbing while `append` and `connection` stay at zero is a READABLE fact.
293test('★★ the staleness ceiling skips itself when events are alive', async (t) => {
294 const feed = {
295 async update () {},
296 async pointers () { return [] },
297 store: { async get () { return null }, async ensureLocal () { return null } }
298 }
299 const m = createMirror({ feeds: () => [{ key: 'f1', feed }], debounceMs: 5, log: () => {} })
300
301 m.every(40)
302 const base = fakeBase()
303 m.watch(base)
304 await m.pass() // attaches the hooks
305
306 // Keep events flowing faster than the ceiling.
307 for (let i = 0; i < 5; i++) { base.emit(); await new Promise((r) => setTimeout(r, 20)) }
308 m.stop()
309
310 t.is(m.stats().stale, 0, 'the timer never needed to fire')
311 t.ok(m.stats().append > 0, 'because the event was doing the work (' + m.stats().append + ')')
312})