OverviewFilesCommitsIssuesDocsReleasesPeers

ap-bridge commits / dbb92cd

0.10.10: one unreachable peer must not take the whole timeline down
⛔ MEASURED IN PRODUCTION, minutes after deploying 0.10.9 and wrongly suspected of being it: /healthz 200 in 0.003s /ap/users/p2p 200 in 0.008s /ap/users/fleeky 000 in 8.002s ← a handset, held over the swarm, not answering /ap/users/ashatron 200 in 0.083s /timeline 000 in 6.002s The combined page reads EVERY peer, so one silent feed hung all three — while everything else on the server answered in milliseconds. That is a fault-isolation defect introduced in 0.10.7 when /timeline became the relay view, not anything to do with observations. ⛔ A HANG IS NOT AN EXCEPTION. gatherFor already said "a record source that is down must not blank the page" and guarded it with try/catch, which catches a throw and waits for ever on a silence. The guard was written for the wrong failure. Each peer now has its own 4s deadline — per PEER, so a slow source cannot spend the budget of the ones after it — and the page renders without whoever missed it. ⚠ AND IT SAYS WHO. A peer that is quiet and a peer that is unreachable look identical on a page that simply leaves them out, and telling those apart is the difference between "nothing happened" and "we could not see". The footer names them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dbb92cd11b48ec4409ad5f1d1fa0fee6f4620caa
by plankatron <fleeky@gmail.com> · 2026-09-16T00:36:50.000Z

Changed files (5) +173 -118

.pgh-lock.json +94 -114
……@@ -1,9 +1,9 @@
11{
22 "name": "ap-bridge",
3 "version": "0.10.8",
3 "version": "0.10.9",
44 "runtime": "bare",
5 "rootTreeHash": "a5f2dfc3a0fca68ec50c499d801ec68ccc8a7f6b158070d63826c29e44e55f9a",
6 "publishedAt": "2026-09-15T16:28:31.063Z",
5 "rootTreeHash": "7c995430fefe253aa0d1954eb1c0b831190d4cf0e313fc8eb45bd4263e92f19f",
6 "publishedAt": "2026-09-16T00:30:11.529Z",
77 "deps": [
88 {
99 "name": "@hyperswarm/secret-stream",
……@@ -2395,9 +2395,9 @@
23952395 },
23962396 {
23972397 "name": "favela-peer",
2398 "version": "0.0.56",
2399 "tree": "d671f1d149cab9418c05acb06158ebeb24cd1daa984d57e428f31a716732f590",
2400 "normHash": "2c803b1b4fa958b417a5fa554b969cdb0fd3949822902fcae2b8205d41da50b6",
2398 "version": "0.0.57",
2399 "tree": "d9a4175b27e1b3a814b3f7a6a335fdd0bf83b5688f91502cf1be755b7869339f",
2400 "normHash": "a4480383d5050712323e1c0dc72a65d1259aae418114aa91b1425580c0512387",
24012401 "workspace": true
24022402 },
24032403 {
……@@ -4004,8 +4004,8 @@
40044004 "tree": "38f44af48b2d59b46f1b5db31b81d02f3ca8b8bad6bc0fb9fffff698241f5e37"
40054005 }
40064006 ],
4007 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4008 "acceptedAt": "2026-09-15T16:28:31.080Z"
4007 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4008 "acceptedAt": "2026-09-16T00:30:11.555Z"
40094009 },
40104010 {
40114011 "type": "VERSION_CONFLICT",
……@@ -4019,8 +4019,8 @@
40194019 "tree": "ee91b6bd77101e3404ca571b2e5adf0f47029d174398a05f50294fc207e39d99"
40204020 }
40214021 ],
4022 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4023 "acceptedAt": "2026-09-15T16:28:31.080Z"
4022 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4023 "acceptedAt": "2026-09-16T00:30:11.555Z"
40244024 },
40254025 {
40264026 "type": "VERSION_CONFLICT",
……@@ -4034,8 +4034,8 @@
40344034 "tree": "3bb78d8f06263a549bd1077f37a7df257b1d557e44608631435af625f05a9c11"
40354035 }
40364036 ],
4037 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4038 "acceptedAt": "2026-09-15T16:28:31.080Z"
4037 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4038 "acceptedAt": "2026-09-16T00:30:11.555Z"
40394039 },
40404040 {
40414041 "type": "VERSION_CONFLICT",
……@@ -4054,8 +4054,8 @@
40544054 "tree": "58c0bf8698af19464d0ee8354a77ed1ac96d3a5fa18aa4e794686cfbbf080e5d"
40554055 }
40564056 ],
4057 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4058 "acceptedAt": "2026-09-15T16:28:31.080Z"
4057 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4058 "acceptedAt": "2026-09-16T00:30:11.555Z"
40594059 },
40604060 {
40614061 "type": "VERSION_CONFLICT",
……@@ -4069,8 +4069,8 @@
40694069 "tree": "da4c6b2654e9d66f10b0faa989311cf182878c24ab51b1c41458cfbf37ad29cd"
40704070 }
40714071 ],
4072 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4073 "acceptedAt": "2026-09-15T16:28:31.080Z"
4072 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4073 "acceptedAt": "2026-09-16T00:30:11.555Z"
40744074 },
40754075 {
40764076 "type": "VERSION_CONFLICT",
……@@ -4084,8 +4084,8 @@
40844084 "tree": "40be8f559a13b54ebf0689e63e6c70f0d218035359e17c497ed7bc8a85c3cc95"
40854085 }
40864086 ],
4087 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4088 "acceptedAt": "2026-09-15T16:28:31.080Z"
4087 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4088 "acceptedAt": "2026-09-16T00:30:11.555Z"
40894089 },
40904090 {
40914091 "type": "VERSION_CONFLICT",
……@@ -4099,8 +4099,8 @@
40994099 "tree": "3eae85efce82299769898c8151b5ed0a94e5a5ebdbcdee2105b815ca733a61cb"
41004100 }
41014101 ],
4102 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4103 "acceptedAt": "2026-09-15T16:28:31.080Z"
4102 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4103 "acceptedAt": "2026-09-16T00:30:11.555Z"
41044104 },
41054105 {
41064106 "type": "VERSION_CONFLICT",
……@@ -4114,8 +4114,8 @@
41144114 "tree": "8d9e01d443a127781773598d2e77c3f372334fad09e20623a819dabe49b22e1a"
41154115 }
41164116 ],
4117 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4118 "acceptedAt": "2026-09-15T16:28:31.080Z"
4117 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4118 "acceptedAt": "2026-09-16T00:30:11.555Z"
41194119 },
41204120 {
41214121 "type": "VERSION_CONFLICT",
……@@ -4129,8 +4129,8 @@
41294129 "tree": "dc1ad6c999fe90d946ba0ef34698ea9af451b639be2ad1c5b61eabb08337a6ca"
41304130 }
41314131 ],
4132 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4133 "acceptedAt": "2026-09-15T16:28:31.080Z"
4132 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4133 "acceptedAt": "2026-09-16T00:30:11.555Z"
41344134 },
41354135 {
41364136 "type": "VERSION_CONFLICT",
……@@ -4144,8 +4144,8 @@
41444144 "tree": "88b4779570e5113aa21b204989ac018d1628b387681f7e53428a7c99114fb701"
41454145 }
41464146 ],
4147 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4148 "acceptedAt": "2026-09-15T16:28:31.080Z"
4147 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4148 "acceptedAt": "2026-09-16T00:30:11.555Z"
41494149 },
41504150 {
41514151 "type": "VERSION_CONFLICT",
……@@ -4159,8 +4159,8 @@
41594159 "tree": "720b867ec4300a3caa2325eb1449499f9940bfb88341ecceb5992fa0fdf5f37d"
41604160 }
41614161 ],
4162 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4163 "acceptedAt": "2026-09-15T16:28:31.080Z"
4162 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4163 "acceptedAt": "2026-09-16T00:30:11.555Z"
41644164 },
41654165 {
41664166 "type": "VERSION_CONFLICT",
……@@ -4174,8 +4174,8 @@
41744174 "tree": "b697fe174482a708b49d0764cd4786291845f0034a31e0699381ca7e7b269d04"
41754175 }
41764176 ],
4177 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4178 "acceptedAt": "2026-09-15T16:28:31.080Z"
4177 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4178 "acceptedAt": "2026-09-16T00:30:11.555Z"
41794179 },
41804180 {
41814181 "type": "VERSION_CONFLICT",
……@@ -4189,8 +4189,8 @@
41894189 "tree": "767f07242277ae80bd395de5a4969fcdbf956c216158ae517fb2b562558ac1a4"
41904190 }
41914191 ],
4192 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4193 "acceptedAt": "2026-09-15T16:28:31.080Z"
4192 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4193 "acceptedAt": "2026-09-16T00:30:11.555Z"
41944194 },
41954195 {
41964196 "type": "VERSION_CONFLICT",
……@@ -4204,8 +4204,8 @@
42044204 "tree": "de7deed21f587ffc8bde4971abe00fca77bb6c71c4e7effb31ad34ee330815df"
42054205 }
42064206 ],
4207 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4208 "acceptedAt": "2026-09-15T16:28:31.080Z"
4207 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4208 "acceptedAt": "2026-09-16T00:30:11.555Z"
42094209 },
42104210 {
42114211 "type": "VERSION_CONFLICT",
……@@ -4219,8 +4219,8 @@
42194219 "tree": "2df42517ebf73cd9c11f2f652f75e87401fd4ed7b856c0adc15c33baf1d4f1ad"
42204220 }
42214221 ],
4222 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4223 "acceptedAt": "2026-09-15T16:28:31.080Z"
4222 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4223 "acceptedAt": "2026-09-16T00:30:11.555Z"
42244224 },
42254225 {
42264226 "type": "VERSION_CONFLICT",
……@@ -4234,8 +4234,8 @@
42344234 "tree": "325aca171cbff560487177584078c8066133e91176639ca47c98d70e195aa92e"
42354235 }
42364236 ],
4237 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4238 "acceptedAt": "2026-09-15T16:28:31.080Z"
4237 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4238 "acceptedAt": "2026-09-16T00:30:11.555Z"
42394239 },
42404240 {
42414241 "type": "VERSION_CONFLICT",
……@@ -4264,8 +4264,8 @@
42644264 "tree": "89a40400106bf4fef95fb5c44a66ee29aada6f76c7740bfba09d8aeeb841f39c"
42654265 }
42664266 ],
4267 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4268 "acceptedAt": "2026-09-15T16:28:31.080Z"
4267 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4268 "acceptedAt": "2026-09-16T00:30:11.555Z"
42694269 },
42704270 {
42714271 "type": "VERSION_CONFLICT",
……@@ -4279,8 +4279,8 @@
42794279 "tree": "1bd0eb382789c62cd4622d47bffb60984fdd766260c79f73a2fc7a136f3edbe9"
42804280 }
42814281 ],
4282 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4283 "acceptedAt": "2026-09-15T16:28:31.080Z"
4282 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4283 "acceptedAt": "2026-09-16T00:30:11.555Z"
42844284 },
42854285 {
42864286 "type": "VERSION_CONFLICT",
……@@ -4294,8 +4294,8 @@
42944294 "tree": "4526fe9dc5468264c7171ec16e9303726674b7b44ca0b3e09cb8b9a6a1be6169"
42954295 }
42964296 ],
4297 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4298 "acceptedAt": "2026-09-15T16:28:31.080Z"
4297 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4298 "acceptedAt": "2026-09-16T00:30:11.555Z"
42994299 },
43004300 {
43014301 "type": "VERSION_CONFLICT",
……@@ -4309,8 +4309,8 @@
43094309 "tree": "32b291cd756e142da18a6ed01c351e024fcedc3e2b1ecfbfb85f3106ada4419b"
43104310 }
43114311 ],
4312 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4313 "acceptedAt": "2026-09-15T16:28:31.080Z"
4312 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4313 "acceptedAt": "2026-09-16T00:30:11.555Z"
43144314 },
43154315 {
43164316 "type": "VERSION_CONFLICT",
……@@ -4324,8 +4324,8 @@
43244324 "tree": "4a8cd3b0093092b33088617dc0415e8179882d7f2f13b7649962ca20b08beeed"
43254325 }
43264326 ],
4327 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4328 "acceptedAt": "2026-09-15T16:28:31.080Z"
4327 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4328 "acceptedAt": "2026-09-16T00:30:11.555Z"
43294329 },
43304330 {
43314331 "type": "VERSION_CONFLICT",
……@@ -4339,8 +4339,8 @@
43394339 "tree": "4f63f165ee6a581f689b40c4a40df4df01ab6fba17d46bd29e40ef5bbdd9884f"
43404340 }
43414341 ],
4342 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4343 "acceptedAt": "2026-09-15T16:28:31.080Z"
4342 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4343 "acceptedAt": "2026-09-16T00:30:11.555Z"
43444344 },
43454345 {
43464346 "type": "VERSION_CONFLICT",
……@@ -4354,8 +4354,8 @@
43544354 "tree": "735109c736c0bd3413ab47414fd8b6ea9a99c401fe8e6d608459c79293806754"
43554355 }
43564356 ],
4357 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4358 "acceptedAt": "2026-09-15T16:28:31.080Z"
4357 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4358 "acceptedAt": "2026-09-16T00:30:11.555Z"
43594359 },
43604360 {
43614361 "type": "VERSION_CONFLICT",
……@@ -4369,8 +4369,8 @@
43694369 "tree": "b32b75bf1b2acdd18ed0cc370d01d477fba42ba09fd1f2f78af3ebad925e72d5"
43704370 }
43714371 ],
4372 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4373 "acceptedAt": "2026-09-15T16:28:31.080Z"
4372 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4373 "acceptedAt": "2026-09-16T00:30:11.555Z"
43744374 },
43754375 {
43764376 "type": "VERSION_CONFLICT",
……@@ -4389,8 +4389,8 @@
43894389 "tree": "189397178121e9a18430943345dab820691648a324229071ceb980bbb77a7393"
43904390 }
43914391 ],
4392 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4393 "acceptedAt": "2026-09-15T16:28:31.080Z"
4392 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4393 "acceptedAt": "2026-09-16T00:30:11.555Z"
43944394 },
43954395 {
43964396 "type": "VERSION_CONFLICT",
……@@ -4404,8 +4404,8 @@
44044404 "tree": "3bedca5287a9041fc2fd5ec5ace4df1fa86240e6afec0a105ba67398bafc09a5"
44054405 }
44064406 ],
4407 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4408 "acceptedAt": "2026-09-15T16:28:31.080Z"
4407 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4408 "acceptedAt": "2026-09-16T00:30:11.555Z"
44094409 },
44104410 {
44114411 "type": "VERSION_CONFLICT",
……@@ -4419,8 +4419,8 @@
44194419 "tree": "b9ca9b272008bd67672a1b252390e8b527095c46b46cf6b6725a7a187eb038b6"
44204420 }
44214421 ],
4422 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4423 "acceptedAt": "2026-09-15T16:28:31.080Z"
4422 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4423 "acceptedAt": "2026-09-16T00:30:11.555Z"
44244424 },
44254425 {
44264426 "type": "VERSION_CONFLICT",
……@@ -4434,8 +4434,8 @@
44344434 "tree": "7793645d7d4248d6d6533e3cbcd4bf40bed4f38c678b0ca0606c15640ba5e7b9"
44354435 }
44364436 ],
4437 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4438 "acceptedAt": "2026-09-15T16:28:31.080Z"
4437 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4438 "acceptedAt": "2026-09-16T00:30:11.555Z"
44394439 },
44404440 {
44414441 "type": "VERSION_CONFLICT",
……@@ -4449,8 +4449,8 @@
44494449 "tree": "79447513d4ac0357c8d60bdc7b81b3c5143d761b26700340cba266c4d1cb77b0"
44504450 }
44514451 ],
4452 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4453 "acceptedAt": "2026-09-15T16:28:31.080Z"
4452 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4453 "acceptedAt": "2026-09-16T00:30:11.555Z"
44544454 },
44554455 {
44564456 "type": "VERSION_CONFLICT",
……@@ -4464,8 +4464,8 @@
44644464 "tree": "279d4e0447eb79fd75816864ba7f41f2df03080c9da69ea4603fe2900c8d9e53"
44654465 }
44664466 ],
4467 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4468 "acceptedAt": "2026-09-15T16:28:31.080Z"
4467 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4468 "acceptedAt": "2026-09-16T00:30:11.555Z"
44694469 },
44704470 {
44714471 "type": "VERSION_CONFLICT",
……@@ -4479,8 +4479,8 @@
44794479 "tree": "ed4e0b480417610e195ff0b59e5b9a453abbf2db6d6ce72d7cd522269f835e9a"
44804480 }
44814481 ],
4482 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4483 "acceptedAt": "2026-09-15T16:28:31.080Z"
4482 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4483 "acceptedAt": "2026-09-16T00:30:11.555Z"
44844484 },
44854485 {
44864486 "type": "VERSION_CONFLICT",
……@@ -4499,8 +4499,8 @@
44994499 "tree": "a9e07d54e526c838dfe5e27df33936682aefb218f61a45b6c15289243999f648"
45004500 }
45014501 ],
4502 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4503 "acceptedAt": "2026-09-15T16:28:31.080Z"
4502 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4503 "acceptedAt": "2026-09-16T00:30:11.555Z"
45044504 },
45054505 {
45064506 "type": "VERSION_CONFLICT",
……@@ -4514,8 +4514,8 @@
45144514 "tree": "9d84c86441fe138b227c4fd01d1e948aa83bf8d13cfb1aa957838feea05fbb57"
45154515 }
45164516 ],
4517 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4518 "acceptedAt": "2026-09-15T16:28:31.080Z"
4517 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4518 "acceptedAt": "2026-09-16T00:30:11.555Z"
45194519 },
45204520 {
45214521 "type": "VERSION_CONFLICT",
……@@ -4529,8 +4529,8 @@
45294529 "tree": "4a3afbe2fc7a882e24454eb2e7e7cfe6816811ee5d73ae0f92e8b9ea9cfb8c78"
45304530 }
45314531 ],
4532 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4533 "acceptedAt": "2026-09-15T16:28:31.080Z"
4532 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4533 "acceptedAt": "2026-09-16T00:30:11.555Z"
45344534 },
45354535 {
45364536 "type": "VERSION_CONFLICT",
……@@ -4544,8 +4544,8 @@
45444544 "tree": "1e5c5810d8971a3c27fde7fb0e4d633dcabbe2cf98d8df86b473f8d67eead8b9"
45454545 }
45464546 ],
4547 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4548 "acceptedAt": "2026-09-15T16:28:31.080Z"
4547 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4548 "acceptedAt": "2026-09-16T00:30:11.555Z"
45494549 },
45504550 {
45514551 "type": "VERSION_CONFLICT",
……@@ -4559,8 +4559,8 @@
45594559 "tree": "e3478f50e85ba0ac288efed83e84c992483904897dc39ccf243d20a4ea25d972"
45604560 }
45614561 ],
4562 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4563 "acceptedAt": "2026-09-15T16:28:31.080Z"
4562 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4563 "acceptedAt": "2026-09-16T00:30:11.555Z"
45644564 },
45654565 {
45664566 "type": "VERSION_CONFLICT",
……@@ -4574,8 +4574,8 @@
45744574 "tree": "c05d8bf7eab5024ea98140f78c84d12fb494b4b7b10c7eedeb50b2be842fa519"
45754575 }
45764576 ],
4577 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4578 "acceptedAt": "2026-09-15T16:28:31.080Z"
4577 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4578 "acceptedAt": "2026-09-16T00:30:11.555Z"
45794579 },
45804580 {
45814581 "type": "VERSION_CONFLICT",
……@@ -4589,8 +4589,8 @@
45894589 "tree": "0697bb338bbcec517353cffcdb2345a8232b9b90cc627a549a306f0323576390"
45904590 }
45914591 ],
4592 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4593 "acceptedAt": "2026-09-15T16:28:31.080Z"
4592 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4593 "acceptedAt": "2026-09-16T00:30:11.555Z"
45944594 },
45954595 {
45964596 "type": "VERSION_CONFLICT",
……@@ -4604,8 +4604,8 @@
46044604 "tree": "a366a73e74f8d80f106465058083b62bb70b833c721047124e99c89880e5c706"
46054605 }
46064606 ],
4607 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4608 "acceptedAt": "2026-09-15T16:28:31.080Z"
4607 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4608 "acceptedAt": "2026-09-16T00:30:11.555Z"
46094609 },
46104610 {
46114611 "type": "VERSION_CONFLICT",
……@@ -4619,8 +4619,8 @@
46194619 "tree": "7912e790c9e87e9829a6daa037c5f5da0cc4355214f51d334f96e57678dfddeb"
46204620 }
46214621 ],
4622 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4623 "acceptedAt": "2026-09-15T16:28:31.080Z"
4622 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4623 "acceptedAt": "2026-09-16T00:30:11.555Z"
46244624 },
46254625 {
46264626 "type": "VERSION_CONFLICT",
……@@ -4634,8 +4634,8 @@
46344634 "tree": "003ac841449fcdda1abc3c87fee291a5d9596d245925a7298bd8ef5f8e423eae"
46354635 }
46364636 ],
4637 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4638 "acceptedAt": "2026-09-15T16:28:31.080Z"
4637 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4638 "acceptedAt": "2026-09-16T00:30:11.555Z"
46394639 },
46404640 {
46414641 "type": "VERSION_CONFLICT",
……@@ -4643,19 +4643,14 @@
46434643 "newVersion": "0.0.3",
46444644 "newTree": "f5a680144af3a0d302a8a7af1e4009d1d5bf9960a65d31f11595cd303a04498e",
46454645 "existing": [
4646 {
4647 "appName": "favela-peer",
4648 "version": "0.0.2",
4649 "tree": "7b3f314cf92558edbc90295c573885276a6add925318998c829c03d335edc765"
4650 },
46514646 {
46524647 "appName": "favela-bridge",
46534648 "version": "0.0.2",
46544649 "tree": "7b3f314cf92558edbc90295c573885276a6add925318998c829c03d335edc765"
46554650 }
46564651 ],
4657 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4658 "acceptedAt": "2026-09-15T16:28:31.080Z"
4652 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4653 "acceptedAt": "2026-09-16T00:30:11.555Z"
46594654 },
46604655 {
46614656 "type": "VERSION_CONFLICT",
……@@ -4669,23 +4664,8 @@
46694664 "tree": "c486fe73805bf013e9c5de91b5780d1aa5d2b2295306f411f7863757aaa05e60"
46704665 }
46714666 ],
4672 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4673 "acceptedAt": "2026-09-15T16:28:31.080Z"
4674 },
4675 {
4676 "type": "VERSION_CONFLICT",
4677 "dep": "hyper-overlay",
4678 "newVersion": "0.3.13",
4679 "newTree": "53a5f9ac2fc36efa56ea01c853485ebb1baa4f8431402d40a1edc1ed4df4578d",
4680 "existing": [
4681 {
4682 "appName": "pgh",
4683 "version": "0.3.12",
4684 "tree": "10193a86e2c6f80c2d4377379a0aae4a959f8a40fd9e852b85fee081bfe6675a"
4685 }
4686 ],
4687 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4688 "acceptedAt": "2026-09-15T16:28:31.080Z"
4667 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4668 "acceptedAt": "2026-09-16T00:30:11.555Z"
46894669 },
46904670 {
46914671 "type": "VERSION_CONFLICT",
……@@ -4699,8 +4679,8 @@
46994679 "tree": "20b1ecd2937f3e35102892288234b86a8ec6deb3711016d96ff3509474ac465b"
47004680 }
47014681 ],
4702 "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs",
4703 "acceptedAt": "2026-09-15T16:28:31.080Z"
4682 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4683 "acceptedAt": "2026-09-16T00:30:11.555Z"
47044684 }
47054685 ]
47064686}
lib/public-timeline.js +7 -1
……@@ -197,7 +197,7 @@ function renderReplies (r, host) {
197197 return '<div class="replies">' + kids.map((k) => renderRow(k, host)).join('') + '</div>'
198198}
199199
200export function publicTimeline ({ host = '', rows = [], offset = 0, limit = 50, total = 0, who = null, unaddressable = 0 } = {}) {
200export function publicTimeline ({ host = '', rows = [], offset = 0, limit = 50, total = 0, who = null, unaddressable = 0, silent = [] } = {}) {
201201 const list = rows.length
202202 ? rows.map((r) => renderRow(r, host)).join('')
203203 : '<p class="none">Nothing public here yet.</p>'
……@@ -230,6 +230,12 @@ export function publicTimeline ({ host = '', rows = [], offset = 0, limit = 50,
230230 ? '<br>' + unaddressable + ' of our interaction(s) could not be projected to ActivityPub — ' +
231231 'they act on posts that have no address there. They are shown here, linked to where they live.'
232232 : '') +
233 // ⛔ NAMED, NEVER SILENTLY ABSENT. A peer that is quiet and a peer that is unreachable look
234 // identical on a page that just leaves them out, and telling them apart is the difference between
235 // "nothing happened" and "we could not see".
236 (silent && silent.length
237 ? '<br>' + silent.map(esc).join(', ') + ' did not answer in time — this page is rendered without them.'
238 : '') +
233239 '</footer>'
234240}
235241
lib/router.js +30 -2
……@@ -323,6 +323,27 @@ async function collection (id, ctx) {
323323 * ⚠ MERGED, THEN PAGED. Paging each source separately and interleaving afterwards yields short pages
324324 * and an offset that no longer means what the link said — the mistake the old handler had to note.
325325 */
326// ⛔ A HANG IS NOT AN EXCEPTION, which is what the try/catch below could not know. Measured in
327// production 2026-09-15: a handset held over the swarm stopped answering, and because the combined
328// page reads every peer it hung all of them — /healthz and the other two outboxes were answering in
329// milliseconds throughout. A page that renders two peers and NAMES the third is strictly better than
330// a page that renders nobody.
331//
332// ⚠ The deadline is per PEER, not per page: one slow source must not spend the budget of the peers
333// after it in the loop.
334const PEER_DEADLINE_MS = 4000
335
336function withDeadline (promise, ms, onTimeout) {
337 return new Promise((resolve) => {
338 let done = false
339 const timer = setTimeout(() => { if (!done) { done = true; resolve(onTimeout()) } }, ms)
340 promise.then(
341 (v) => { if (!done) { done = true; clearTimeout(timer); resolve(v) } },
342 () => { if (!done) { done = true; clearTimeout(timer); resolve(onTimeout()) } }
343 )
344 })
345}
346
326347// What one peer contributes to a feed. `c` is a ctx already bound to that peer, so its records,
327348// its interactions and its name all come from the same place the rest of the router gets them.
328349async function gatherFor (c) {
……@@ -366,9 +387,15 @@ async function feedPage (query, ctx, { combined = false } = {}) {
366387 const peers = combined && ctx.users ? ctx.users.all() : [null]
367388 let flat = []
368389 let unaddressable = 0
390 const silent = []
369391 for (const u of peers) {
370392 const c = u ? withUser(ctx, u) : ctx
371 const g = await gatherFor(c)
393 const who = (c.user && c.user.username) || (c.config && c.config.username) || 'this peer'
394 const g = await withDeadline(gatherFor(c), PEER_DEADLINE_MS, () => {
395 silent.push(who)
396 ctx.log?.({ msg: 'feed: peer did not answer within ' + PEER_DEADLINE_MS + 'ms — rendering without it', peer: who })
397 return { notes: [], own: [], unprojectable: [], unaddressable: 0, recipient: who, isPrimary: false }
398 })
372399 unaddressable += g.unaddressable
373400 flat = flat.concat(buildFeed({
374401 self: { handle: g.recipient ? '@' + g.recipient + '@' + host : null, name: (c.user && c.user.displayName) || (!u && ctx.config.displayName) || null },
……@@ -394,7 +421,8 @@ async function feedPage (query, ctx, { combined = false } = {}) {
394421 offset,
395422 limit,
396423 total: rows.length,
397 unaddressable
424 unaddressable,
425 silent
398426 }))
399427}
400428
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "ap-bridge",
3 "version": "0.10.9",
3 "version": "0.10.10",
44 "description": "ActivityPub membrane \u2014 a second projection of the self-custody atproto peer",
55 "type": "module",
66 "main": "index.js",
test/router.test.js +41 -0
……@@ -557,3 +557,44 @@ test('⚠ a bridge with no observation store behaves exactly as before', async (
557557 t.is(res.status, 202, 'still acked promptly')
558558 t.is((await inbound.list({})).length, 1, 'and still kept in the inbound timeline')
559559})
560
561// ── one unreachable peer must not take the page down ──────────────────────────────────────────────
562//
563// ⛔ MEASURED IN PRODUCTION 2026-09-15, minutes after deploying 0.10.9 and wrongly suspected of being
564// it. /timeline returned nothing while /healthz, /ap/actor and /ap/outbox answered in milliseconds:
565//
566// p2p outbox 200 in 0.008s
567// fleeky outbox 000 in 8.002s ← a handset, held over the swarm, not answering
568// ashatron outbox 200 in 0.083s
569//
570// The combined page reads EVERY peer, so one silent feed hung all three. gatherFor already says "a
571// record source that is down must not blank the page" — and guards it with try/catch, which catches a
572// throw and waits for ever on a hang. A page that renders two peers and names the third is strictly
573// better than a page that renders nobody.
574
575test('★★★ a peer whose source never answers is skipped, and the others still render', async (t) => {
576 const { Users } = await import('../lib/users.js')
577 const users = new Users([{ username: 'p2p', primary: true }, { username: 'slow' }], 'p2p.7rnx.net')
578 const notes = { p2p: [{ id: 'https://p2p.7rnx.net/ap/objects/1', type: 'Note', content: '<p>from p2p</p>', published: '2026-09-15T09:00:00Z' }] }
579
580 const ctx = makeCtx({
581 users,
582 inbound: new MemoryInboundStore(),
583 records: { async listNotes () { return { notes: notes.p2p } } },
584 // The handset's shape: it accepts the call and never comes back.
585 recordsFor: (u) => (u.username === 'slow'
586 ? { async listNotes () { return new Promise(() => {}) } }
587 : { async listNotes () { return { notes: notes.p2p } } })
588 })
589
590 const started = Date.now()
591 const res = await handleRequest({ method: 'GET', path: '/timeline', headers: {} }, ctx)
592 const took = Date.now() - started
593
594 t.is(res.status, 200, 'the page is served at all — this is the whole point')
595 t.ok(took < 8000, 'and it does not wait for ever (' + took + 'ms)')
596 t.ok(/from p2p/.test(res.body), 'the readable peer is rendered')
597 // ⚠ SAID, not silently absent. A quiet peer and an unreachable one look identical otherwise, and
598 // that is the failure shape this whole area keeps producing.
599 t.ok(/did not answer/.test(res.body), 'and the one that did not answer is named')
600})