| … | … | @@ -1,9 +1,9 @@ |
| 1 | 1 | { |
| 2 | 2 | "name": "favela-peer", |
| 3 | "version": "0.0.42", | |
| 3 | "version": "0.0.43", | |
| 4 | 4 | "runtime": "bare", |
| 5 | "rootTreeHash": "ef10bb20c36ea5e78eb287b295cc60fb037d05879b97033bd36aec1901b2f9a7", | |
| 6 | "publishedAt": "2026-09-08T18:44:13.359Z", | |
| 5 | "rootTreeHash": "91f004fdec4b38dd4c751e1e99702893b60b2ba5010e4b92015a945fb1803f17", | |
| 6 | "publishedAt": "2026-09-08T19:00:06.696Z", | |
| 7 | 7 | "deps": [ |
| 8 | 8 | { |
| 9 | 9 | "name": "@hyperswarm/secret-stream", |
| … | … | @@ -3017,7 +3017,7 @@ |
| 3017 | 3017 | } |
| 3018 | 3018 | ], |
| 3019 | 3019 | "reason": "version-splits only: 0 code-differs", |
| 3020 | "acceptedAt": "2026-09-08T18:44:13.372Z" | |
| 3020 | "acceptedAt": "2026-09-08T19:00:06.709Z" | |
| 3021 | 3021 | }, |
| 3022 | 3022 | { |
| 3023 | 3023 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3067,7 +3067,7 @@ |
| 3067 | 3067 | } |
| 3068 | 3068 | ], |
| 3069 | 3069 | "reason": "version-splits only: 0 code-differs", |
| 3070 | "acceptedAt": "2026-09-08T18:44:13.372Z" | |
| 3070 | "acceptedAt": "2026-09-08T19:00:06.709Z" | |
| 3071 | 3071 | }, |
| 3072 | 3072 | { |
| 3073 | 3073 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3082,7 +3082,7 @@ |
| 3082 | 3082 | } |
| 3083 | 3083 | ], |
| 3084 | 3084 | "reason": "version-splits only: 0 code-differs", |
| 3085 | "acceptedAt": "2026-09-08T18:44:13.372Z" | |
| 3085 | "acceptedAt": "2026-09-08T19:00:06.709Z" | |
| 3086 | 3086 | }, |
| 3087 | 3087 | { |
| 3088 | 3088 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3107,7 +3107,7 @@ |
| 3107 | 3107 | } |
| 3108 | 3108 | ], |
| 3109 | 3109 | "reason": "version-splits only: 0 code-differs", |
| 3110 | "acceptedAt": "2026-09-08T18:44:13.372Z" | |
| 3110 | "acceptedAt": "2026-09-08T19:00:06.709Z" | |
| 3111 | 3111 | }, |
| 3112 | 3112 | { |
| 3113 | 3113 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3142,7 +3142,7 @@ |
| 3142 | 3142 | } |
| 3143 | 3143 | ], |
| 3144 | 3144 | "reason": "version-splits only: 0 code-differs", |
| 3145 | "acceptedAt": "2026-09-08T18:44:13.372Z" | |
| 3145 | "acceptedAt": "2026-09-08T19:00:06.709Z" | |
| 3146 | 3146 | }, |
| 3147 | 3147 | { |
| 3148 | 3148 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3157,7 +3157,7 @@ |
| 3157 | 3157 | } |
| 3158 | 3158 | ], |
| 3159 | 3159 | "reason": "version-splits only: 0 code-differs", |
| 3160 | "acceptedAt": "2026-09-08T18:44:13.372Z" | |
| 3160 | "acceptedAt": "2026-09-08T19:00:06.709Z" | |
| 3161 | 3161 | }, |
| 3162 | 3162 | { |
| 3163 | 3163 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3172,7 +3172,7 @@ |
| 3172 | 3172 | } |
| 3173 | 3173 | ], |
| 3174 | 3174 | "reason": "version-splits only: 0 code-differs", |
| 3175 | "acceptedAt": "2026-09-08T18:44:13.372Z" | |
| 3175 | "acceptedAt": "2026-09-08T19:00:06.709Z" | |
| 3176 | 3176 | }, |
| 3177 | 3177 | { |
| 3178 | 3178 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3187,7 +3187,7 @@ |
| 3187 | 3187 | } |
| 3188 | 3188 | ], |
| 3189 | 3189 | "reason": "version-splits only: 0 code-differs", |
| 3190 | "acceptedAt": "2026-09-08T18:44:13.372Z" | |
| 3190 | "acceptedAt": "2026-09-08T19:00:06.709Z" | |
| 3191 | 3191 | }, |
| 3192 | 3192 | { |
| 3193 | 3193 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3202,7 +3202,7 @@ |
| 3202 | 3202 | } |
| 3203 | 3203 | ], |
| 3204 | 3204 | "reason": "version-splits only: 0 code-differs", |
| 3205 | "acceptedAt": "2026-09-08T18:44:13.372Z" | |
| 3205 | "acceptedAt": "2026-09-08T19:00:06.709Z" | |
| 3206 | 3206 | }, |
| 3207 | 3207 | { |
| 3208 | 3208 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3232,7 +3232,7 @@ |
| 3232 | 3232 | } |
| 3233 | 3233 | ], |
| 3234 | 3234 | "reason": "version-splits only: 0 code-differs", |
| 3235 | "acceptedAt": "2026-09-08T18:44:13.372Z" | |
| 3235 | "acceptedAt": "2026-09-08T19:00:06.709Z" | |
| 3236 | 3236 | }, |
| 3237 | 3237 | { |
| 3238 | 3238 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3247,7 +3247,7 @@ |
| 3247 | 3247 | } |
| 3248 | 3248 | ], |
| 3249 | 3249 | "reason": "version-splits only: 0 code-differs", |
| 3250 | "acceptedAt": "2026-09-08T18:44:13.372Z" | |
| 3250 | "acceptedAt": "2026-09-08T19:00:06.709Z" | |
| 3251 | 3251 | }, |
| 3252 | 3252 | { |
| 3253 | 3253 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3277,7 +3277,7 @@ |
| 3277 | 3277 | } |
| 3278 | 3278 | ], |
| 3279 | 3279 | "reason": "version-splits only: 0 code-differs", |
| 3280 | "acceptedAt": "2026-09-08T18:44:13.372Z" | |
| 3280 | "acceptedAt": "2026-09-08T19:00:06.709Z" | |
| 3281 | 3281 | }, |
| 3282 | 3282 | { |
| 3283 | 3283 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3307,7 +3307,7 @@ |
| 3307 | 3307 | } |
| 3308 | 3308 | ], |
| 3309 | 3309 | "reason": "version-splits only: 0 code-differs", |
| 3310 | "acceptedAt": "2026-09-08T18:44:13.372Z" | |
| 3310 | "acceptedAt": "2026-09-08T19:00:06.709Z" | |
| 3311 | 3311 | }, |
| 3312 | 3312 | { |
| 3313 | 3313 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3322,7 +3322,7 @@ |
| 3322 | 3322 | } |
| 3323 | 3323 | ], |
| 3324 | 3324 | "reason": "version-splits only: 0 code-differs", |
| 3325 | "acceptedAt": "2026-09-08T18:44:13.372Z" | |
| 3325 | "acceptedAt": "2026-09-08T19:00:06.709Z" | |
| 3326 | 3326 | }, |
| 3327 | 3327 | { |
| 3328 | 3328 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3337,7 +3337,7 @@ |
| 3337 | 3337 | } |
| 3338 | 3338 | ], |
| 3339 | 3339 | "reason": "version-splits only: 0 code-differs", |
| 3340 | "acceptedAt": "2026-09-08T18:44:13.372Z" | |
| 3340 | "acceptedAt": "2026-09-08T19:00:06.709Z" | |
| 3341 | 3341 | }, |
| 3342 | 3342 | { |
| 3343 | 3343 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3367,7 +3367,7 @@ |
| 3367 | 3367 | } |
| 3368 | 3368 | ], |
| 3369 | 3369 | "reason": "version-splits only: 0 code-differs", |
| 3370 | "acceptedAt": "2026-09-08T18:44:13.372Z" | |
| 3370 | "acceptedAt": "2026-09-08T19:00:06.709Z" | |
| 3371 | 3371 | }, |
| 3372 | 3372 | { |
| 3373 | 3373 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3387,7 +3387,7 @@ |
| 3387 | 3387 | } |
| 3388 | 3388 | ], |
| 3389 | 3389 | "reason": "version-splits only: 0 code-differs", |
| 3390 | "acceptedAt": "2026-09-08T18:44:13.372Z" | |
| 3390 | "acceptedAt": "2026-09-08T19:00:06.709Z" | |
| 3391 | 3391 | }, |
| 3392 | 3392 | { |
| 3393 | 3393 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3412,7 +3412,7 @@ |
| 3412 | 3412 | } |
| 3413 | 3413 | ], |
| 3414 | 3414 | "reason": "version-splits only: 0 code-differs", |
| 3415 | "acceptedAt": "2026-09-08T18:44:13.372Z" | |
| 3415 | "acceptedAt": "2026-09-08T19:00:06.709Z" | |
| 3416 | 3416 | }, |
| 3417 | 3417 | { |
| 3418 | 3418 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3427,7 +3427,7 @@ |
| 3427 | 3427 | } |
| 3428 | 3428 | ], |
| 3429 | 3429 | "reason": "version-splits only: 0 code-differs", |
| 3430 | "acceptedAt": "2026-09-08T18:44:13.372Z" | |
| 3430 | "acceptedAt": "2026-09-08T19:00:06.709Z" | |
| 3431 | 3431 | }, |
| 3432 | 3432 | { |
| 3433 | 3433 | "type": "VERSION_CONFLICT", |
| … | … | @@ -3442,7 +3442,7 @@ |
| 3442 | 3442 | } |
| 3443 | 3443 | ], |
| 3444 | 3444 | "reason": "version-splits only: 0 code-differs", |
| 3445 | "acceptedAt": "2026-09-08T18:44:13.372Z" | |
| 3445 | "acceptedAt": "2026-09-08T19:00:06.709Z" | |
| 3446 | 3446 | } |
| 3447 | 3447 | ] |
| 3448 | 3448 | } |
| … | … | @@ -39,13 +39,11 @@ export function createMirror ({ feeds, concurrency = DEFAULT_CONCURRENCY, deboun |
| 39 | 39 | let stopped = false |
| 40 | 40 | let running = false |
| 41 | 41 | let timer = null |
| 42 | let interval = null | |
| 43 | 42 | 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 } | |
| 49 | 47 | const bases = [] |
| 50 | 48 | const hooked = new Map() |
| 51 | 49 | |
| … | … | @@ -198,34 +196,14 @@ export function createMirror ({ feeds, concurrency = DEFAULT_CONCURRENCY, deboun |
| 198 | 196 | onPeer () { this.schedule('connection') }, |
| 199 | 197 | |
| 200 | 198 | /** |
| 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. | |
| 207 | 201 | */ |
| 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. */ | |
| 223 | 202 | stats () { return { ...fired, hooked: hooked.size, bases: bases.length, lastPassAt } }, |
| 224 | 203 | |
| 225 | 204 | stop () { |
| 226 | 205 | stopped = true |
| 227 | 206 | if (timer) { clearTimeout(timer); timer = null } |
| 228 | if (interval) { clearInterval(interval); interval = null } | |
| 229 | 207 | for (const [c, fn] of hooked) { try { c.off('append', fn) } catch {} } |
| 230 | 208 | hooked.clear() |
| 231 | 209 | bases.length = 0 |
| … | … | @@ -258,11 +258,6 @@ export class FavelaNode { |
| 258 | 258 | this._onMirrorPeer = () => this._mirror.onPeer() |
| 259 | 259 | this.swarm.on('connection', this._onMirrorPeer) |
| 260 | 260 | } |
| 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 | ||
| 266 | 261 | return this |
| 267 | 262 | } |
| 268 | 263 |
| … | … | @@ -1,6 +1,6 @@ |
| 1 | 1 | { |
| 2 | 2 | "name": "favela-peer", |
| 3 | "version": "0.0.43", | |
| 3 | "version": "0.0.44", | |
| 4 | 4 | "description": "The favela daemon — feeds and documents of markdown records over Hypercore. No atproto, no ActivityPub.", |
| 5 | 5 | "type": "module", |
| 6 | 6 | "scripts": { |
| … | … | @@ -239,27 +239,6 @@ test('★★ a sweep refreshes the view before reading pointers', async (t) => { |
| 239 | 239 | t.alike(order, ['update', 'pointers', 'fetch:fresh'], 'refresh, then read, then fetch') |
| 240 | 240 | }) |
| 241 | 241 | |
| 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. | |
| 244 | test('★ 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 | ||
| 263 | 242 | // ⛔ A PEER COMING BACK IS AN EVENT, NOT AN ABSENCE. A record stays unresolved because nobody |
| 264 | 243 | // holding its bytes is online; their return is a swarm 'connection'. Re-checking that on a clock |
| 265 | 244 | // 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) => |
| 287 | 266 | t.is(m.stats().connection, 1, 'and the connection trigger is what did it') |
| 288 | 267 | }) |
| 289 | 268 | |
| 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. | |
| 293 | test('★★ 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 | }) |