OverviewFilesCommitsIssuesDocsReleasesPeers

favela-peer commits / 477f15e

one RPC channel per connection — a retry must not fail on its own making
connectBridge built a fresh ProtomuxRPC every call. Protomux keys a channel by protocol on a stream, so a second attempt over an already-open connection opened a SECOND channel for the same protocol and failed with 'duplicate channel'. Observed by someone doing the most ordinary thing after an error: trying again. Worse than it sounds, because the message describes the plumbing rather than the act — a mistyped name gets answered with a sentence about channels. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
477f15ed5b58cb63713db9fe211579da47a87ae5
by plankatron <fleeky@gmail.com> · 2026-09-11T19:57:48.000Z

Changed files (3) +143 -52

.pgh-lock.json +71 -51
……@@ -1,9 +1,9 @@
11{
22 "name": "favela-peer",
3 "version": "0.0.48",
3 "version": "0.0.50",
44 "runtime": "bare",
5 "rootTreeHash": "5c3a76b7f78ee8a7acfe03eb902a411579e73959a1604bf02c6f65a606046c94",
6 "publishedAt": "2026-09-09T00:50:40.670Z",
5 "rootTreeHash": "d6422e37ce071e93589035c85bf6aea0db341b785fc763f6b6c77c3a3860586f",
6 "publishedAt": "2026-09-10T00:03:29.133Z",
77 "deps": [
88 {
99 "name": "@hyperswarm/secret-stream",
……@@ -1592,9 +1592,9 @@
15921592 },
15931593 {
15941594 "name": "favela-markdown",
1595 "version": "0.1.6",
1596 "tree": "97d64cb2674377910d3ec28f3f4246d7e3fbc1b9b06b8b9d1ef6360cc0765569",
1597 "normHash": "853f6fe2c24cea0c956653eaa7d5a13b7feca966cad5c9a6164c2dc1ff324881",
1595 "version": "0.2.0",
1596 "tree": "e980c91c65708beb2d24964f4f8bce19d01f9783d1d93e4eff8674851df64611",
1597 "normHash": "2b6a91ad3e40196bc8a2bed0be0d54af70e365e9159504e3cb86e73f034b8c5f",
15981598 "workspace": true
15991599 },
16001600 {
……@@ -3016,8 +3016,8 @@
30163016 "tree": "c5f18b3620514f54714aeb351f074adface216a335242e323e33d28d261376fe"
30173017 }
30183018 ],
3019 "reason": "version-splits only",
3020 "acceptedAt": "2026-09-09T00:50:40.684Z"
3019 "reason": "dep version-splits only; 0 code-differs",
3020 "acceptedAt": "2026-09-10T00:03:29.149Z"
30213021 },
30223022 {
30233023 "type": "VERSION_CONFLICT",
……@@ -3061,8 +3061,8 @@
30613061 "tree": "6bb5233025d1c30e4f181e29b6ab5af549d8909c7070c7b4438b30f1a4e76ce2"
30623062 }
30633063 ],
3064 "reason": "version-splits only",
3065 "acceptedAt": "2026-09-09T00:50:40.684Z"
3064 "reason": "dep version-splits only; 0 code-differs",
3065 "acceptedAt": "2026-09-10T00:03:29.149Z"
30663066 },
30673067 {
30683068 "type": "VERSION_CONFLICT",
……@@ -3076,8 +3076,8 @@
30763076 "tree": "58c0bf8698af19464d0ee8354a77ed1ac96d3a5fa18aa4e794686cfbbf080e5d"
30773077 }
30783078 ],
3079 "reason": "version-splits only",
3080 "acceptedAt": "2026-09-09T00:50:40.684Z"
3079 "reason": "dep version-splits only; 0 code-differs",
3080 "acceptedAt": "2026-09-10T00:03:29.149Z"
30813081 },
30823082 {
30833083 "type": "VERSION_CONFLICT",
……@@ -3101,8 +3101,8 @@
31013101 "tree": "bdebba5a532ce4846b49f1e5ffad774becfc44182d0c3737f7e441ab42f4c43d"
31023102 }
31033103 ],
3104 "reason": "version-splits only",
3105 "acceptedAt": "2026-09-09T00:50:40.684Z"
3104 "reason": "dep version-splits only; 0 code-differs",
3105 "acceptedAt": "2026-09-10T00:03:29.149Z"
31063106 },
31073107 {
31083108 "type": "VERSION_CONFLICT",
……@@ -3131,8 +3131,8 @@
31313131 "tree": "b799e6ba8726aa951b45f12cc7ecffe5f4e9ee1e8adf09a34d026d9d5658828a"
31323132 }
31333133 ],
3134 "reason": "version-splits only",
3135 "acceptedAt": "2026-09-09T00:50:40.684Z"
3134 "reason": "dep version-splits only; 0 code-differs",
3135 "acceptedAt": "2026-09-10T00:03:29.149Z"
31363136 },
31373137 {
31383138 "type": "VERSION_CONFLICT",
……@@ -3146,8 +3146,8 @@
31463146 "tree": "e92100a4f821a638ac99c1407e4c7e784f8eb9661e6d9119ecbec4faae77cc7e"
31473147 }
31483148 ],
3149 "reason": "version-splits only",
3150 "acceptedAt": "2026-09-09T00:50:40.684Z"
3149 "reason": "dep version-splits only; 0 code-differs",
3150 "acceptedAt": "2026-09-10T00:03:29.149Z"
31513151 },
31523152 {
31533153 "type": "VERSION_CONFLICT",
……@@ -3161,8 +3161,8 @@
31613161 "tree": "1f5de2daa08643a5cb8bce2153ca4d55d4504fb366f1f1ef80bca1700ec085f7"
31623162 }
31633163 ],
3164 "reason": "version-splits only",
3165 "acceptedAt": "2026-09-09T00:50:40.684Z"
3164 "reason": "dep version-splits only; 0 code-differs",
3165 "acceptedAt": "2026-09-10T00:03:29.149Z"
31663166 },
31673167 {
31683168 "type": "VERSION_CONFLICT",
……@@ -3176,8 +3176,8 @@
31763176 "tree": "579bdc1eaabbca726d3916f1fc74cfd4986653f6748632db586817a2a8caabea"
31773177 }
31783178 ],
3179 "reason": "version-splits only",
3180 "acceptedAt": "2026-09-09T00:50:40.684Z"
3179 "reason": "dep version-splits only; 0 code-differs",
3180 "acceptedAt": "2026-09-10T00:03:29.149Z"
31813181 },
31823182 {
31833183 "type": "VERSION_CONFLICT",
……@@ -3191,8 +3191,8 @@
31913191 "tree": "967cf214778dbacd7cc7cc3850ffdff6a6f145b4bd73de741d0938d7d84d33d5"
31923192 }
31933193 ],
3194 "reason": "version-splits only",
3195 "acceptedAt": "2026-09-09T00:50:40.684Z"
3194 "reason": "dep version-splits only; 0 code-differs",
3195 "acceptedAt": "2026-09-10T00:03:29.149Z"
31963196 },
31973197 {
31983198 "type": "VERSION_CONFLICT",
……@@ -3216,8 +3216,8 @@
32163216 "tree": "35b214bfbf85f8bbcb9f1926470e77f4e240d9b5045b07f81ce83fec24c1380c"
32173217 }
32183218 ],
3219 "reason": "version-splits only",
3220 "acceptedAt": "2026-09-09T00:50:40.684Z"
3219 "reason": "dep version-splits only; 0 code-differs",
3220 "acceptedAt": "2026-09-10T00:03:29.149Z"
32213221 },
32223222 {
32233223 "type": "VERSION_CONFLICT",
……@@ -3231,8 +3231,33 @@
32313231 "tree": "67c4c05f69061f65a2548595c43282be2da8e0934e6ce347afa034b29cc40c81"
32323232 }
32333233 ],
3234 "reason": "version-splits only",
3235 "acceptedAt": "2026-09-09T00:50:40.684Z"
3234 "reason": "dep version-splits only; 0 code-differs",
3235 "acceptedAt": "2026-09-10T00:03:29.149Z"
3236 },
3237 {
3238 "type": "VERSION_CONFLICT",
3239 "dep": "favela-markdown",
3240 "newVersion": "0.2.0",
3241 "newTree": "e980c91c65708beb2d24964f4f8bce19d01f9783d1d93e4eff8674851df64611",
3242 "existing": [
3243 {
3244 "appName": "ap-bridge",
3245 "version": "0.1.6",
3246 "tree": "08913adf8bf7452a12cc06528c49b4629c880de61fa45f01990e33b000a820d9"
3247 },
3248 {
3249 "appName": "favela-surface",
3250 "version": "0.1.6",
3251 "tree": "08913adf8bf7452a12cc06528c49b4629c880de61fa45f01990e33b000a820d9"
3252 },
3253 {
3254 "appName": "favela-bridge",
3255 "version": "0.1.6",
3256 "tree": "08913adf8bf7452a12cc06528c49b4629c880de61fa45f01990e33b000a820d9"
3257 }
3258 ],
3259 "reason": "dep version-splits only; 0 code-differs",
3260 "acceptedAt": "2026-09-10T00:03:29.149Z"
32363261 },
32373262 {
32383263 "type": "VERSION_CONFLICT",
……@@ -3256,8 +3281,8 @@
32563281 "tree": "4ef8a03ba96b1488cb7ee35dc582b14d096ae4b3fab5481186c94516d434fe18"
32573282 }
32583283 ],
3259 "reason": "version-splits only",
3260 "acceptedAt": "2026-09-09T00:50:40.684Z"
3284 "reason": "dep version-splits only; 0 code-differs",
3285 "acceptedAt": "2026-09-10T00:03:29.149Z"
32613286 },
32623287 {
32633288 "type": "VERSION_CONFLICT",
……@@ -3281,8 +3306,8 @@
32813306 "tree": "5fc49d698f6ddfd653b3cdaa72ec572d6804293e2e9c3e98303f69417115e903"
32823307 }
32833308 ],
3284 "reason": "version-splits only",
3285 "acceptedAt": "2026-09-09T00:50:40.684Z"
3309 "reason": "dep version-splits only; 0 code-differs",
3310 "acceptedAt": "2026-09-10T00:03:29.149Z"
32863311 },
32873312 {
32883313 "type": "VERSION_CONFLICT",
……@@ -3296,8 +3321,8 @@
32963321 "tree": "d0099e1743480f28a08c7374d20ad5048088475180ab606626b63acd59e365a5"
32973322 }
32983323 ],
3299 "reason": "version-splits only",
3300 "acceptedAt": "2026-09-09T00:50:40.684Z"
3324 "reason": "dep version-splits only; 0 code-differs",
3325 "acceptedAt": "2026-09-10T00:03:29.149Z"
33013326 },
33023327 {
33033328 "type": "VERSION_CONFLICT",
……@@ -3311,8 +3336,8 @@
33113336 "tree": "60429c7756f039ec2b3e5240f702dee450c1da19ad6de06900a3877c6c72bb52"
33123337 }
33133338 ],
3314 "reason": "version-splits only",
3315 "acceptedAt": "2026-09-09T00:50:40.684Z"
3339 "reason": "dep version-splits only; 0 code-differs",
3340 "acceptedAt": "2026-09-10T00:03:29.149Z"
33163341 },
33173342 {
33183343 "type": "VERSION_CONFLICT",
……@@ -3336,8 +3361,8 @@
33363361 "tree": "6fc80ad1ccbfb046613f36299032f21a4752e2614ff8d88a7a5bd7ce66fc1293"
33373362 }
33383363 ],
3339 "reason": "version-splits only",
3340 "acceptedAt": "2026-09-09T00:50:40.684Z"
3364 "reason": "dep version-splits only; 0 code-differs",
3365 "acceptedAt": "2026-09-10T00:03:29.149Z"
33413366 },
33423367 {
33433368 "type": "VERSION_CONFLICT",
……@@ -3349,15 +3374,10 @@
33493374 "appName": "pgh",
33503375 "version": "0.3.12",
33513376 "tree": "10193a86e2c6f80c2d4377379a0aae4a959f8a40fd9e852b85fee081bfe6675a"
3352 },
3353 {
3354 "appName": "hyper-proto-peer",
3355 "version": "0.3.12",
3356 "tree": "92424acff1a50468df203528e41739f44f602904abc4beb34500ce172c6d0ad2"
33573377 }
33583378 ],
3359 "reason": "version-splits only",
3360 "acceptedAt": "2026-09-09T00:50:40.684Z"
3379 "reason": "dep version-splits only; 0 code-differs",
3380 "acceptedAt": "2026-09-10T00:03:29.149Z"
33613381 },
33623382 {
33633383 "type": "VERSION_CONFLICT",
……@@ -3376,8 +3396,8 @@
33763396 "tree": "36d01631ddee74ac2ce62971abd7c0ac30562da8dfd6495480da56c534eb80b7"
33773397 }
33783398 ],
3379 "reason": "version-splits only",
3380 "acceptedAt": "2026-09-09T00:50:40.684Z"
3399 "reason": "dep version-splits only; 0 code-differs",
3400 "acceptedAt": "2026-09-10T00:03:29.149Z"
33813401 },
33823402 {
33833403 "type": "VERSION_CONFLICT",
……@@ -3391,8 +3411,8 @@
33913411 "tree": "bba869f5c85b11bce5671d57a87cd2793b490588867cd07fbe94cad796114970"
33923412 }
33933413 ],
3394 "reason": "version-splits only",
3395 "acceptedAt": "2026-09-09T00:50:40.684Z"
3414 "reason": "dep version-splits only; 0 code-differs",
3415 "acceptedAt": "2026-09-10T00:03:29.149Z"
33963416 },
33973417 {
33983418 "type": "VERSION_CONFLICT",
……@@ -3406,8 +3426,8 @@
34063426 "tree": "2ecc91a8c3595dda5dfc86ace62a70875520adc4e588a2ac14945dbc4313c450"
34073427 }
34083428 ],
3409 "reason": "version-splits only",
3410 "acceptedAt": "2026-09-09T00:50:40.684Z"
3429 "reason": "dep version-splits only; 0 code-differs",
3430 "acceptedAt": "2026-09-10T00:03:29.149Z"
34113431 }
34123432 ]
34133433}
lib/bridge.js +18 -1
……@@ -69,7 +69,24 @@ export async function connectBridge (swarm, bridgeKey, { timeoutMs = DEFAULT_TIM
6969 return { rpc: rpcOver(conn), close: () => {} }
7070}
7171
72const rpcOver = (conn) => new ProtomuxRPC(conn, { protocol: PROTOCOL, valueEncoding: c.json })
72// ⛔ ONE CHANNEL PER CONNECTION, cached. Protomux keys a channel by protocol on a given stream, so
73// opening a second one for the same protocol is a DUPLICATE CHANNEL — and that is what a person sees
74// when they simply try again after a failed attempt: the first try leaves the connection open, the
75// retry builds a fresh ProtomuxRPC over it, and the retry fails for a reason that has nothing to do
76// with what they were doing.
77//
78// ⚠ A WeakMap, so a connection that goes away takes its channel with it rather than pinning it.
79const channels = new WeakMap()
80
81const rpcOver = (conn) => {
82 const had = channels.get(conn)
83 if (had) return had
84 const rpc = new ProtomuxRPC(conn, { protocol: PROTOCOL, valueEncoding: c.json })
85 channels.set(conn, rpc)
86 // Dropped when the connection does, or a reconnect would hand back a channel on a dead stream.
87 conn.once('close', () => { try { channels.delete(conn) } catch {} })
88 return rpc
89}
7390
7491/**
7592 * The claim a peer sends. Pure — no network, no clock — so what gets signed is testable on its own.
test/bridge-channel.test.js +54 -0
……@@ -0,0 +1,54 @@
1// One RPC channel per connection — because a retry must not fail for a reason of its own making.
2//
3// ⛔ WHAT HAPPENED. `connectBridge` built a fresh ProtomuxRPC every call. Protomux keys a channel by
4// protocol on a given stream, so the second attempt over an already-open connection opened a SECOND
5// channel for the same protocol and failed with "duplicate channel". Observed by a person doing the
6// most ordinary thing possible after an error: trying again.
7//
8// ⚠ The failure is worse than it sounds, because the message describes the plumbing rather than the
9// act. Someone who mistyped a name is told about channels.
10
11import test from 'brittle'
12import { Duplex } from 'streamx'
13import b4a from 'b4a'
14import { connectBridge } from '../lib/bridge.js'
15
16const KEY = 'a'.repeat(64)
17
18// A swarm stub whose "connection" is already open to the key we ask for — the retry case.
19function swarmWith (conn) {
20 return { connections: new Set([conn]), join: () => ({ flushed: async () => {} }), on () {}, off () {} }
21}
22
23// ⚠ A REAL duplex. Protomux writes a channel-open frame the moment a channel is created, so a plain
24// object stub fails inside protomux rather than exercising anything this file is about.
25function fakeConn () {
26 const s = new Duplex({ write (data, cb) { cb(null) } })
27 s.remotePublicKey = b4a.from(KEY, 'hex')
28 return s
29}
30
31test('★★ asking twice over one connection reuses the channel', async (t) => {
32 const conn = fakeConn()
33 const swarm = swarmWith(conn)
34
35 const a = await connectBridge(swarm, KEY)
36 const b = await connectBridge(swarm, KEY)
37
38 t.is(a.rpc, b.rpc, 'the same channel — a second one is a "duplicate channel" error on retry')
39})
40
41test('⚠ a closed connection does not leave a dead channel behind', async (t) => {
42 const conn = fakeConn()
43 const swarm = swarmWith(conn)
44
45 const a = await connectBridge(swarm, KEY)
46 conn.emit('close')
47 const b = await connectBridge(swarm, KEY)
48
49 t.not(a.rpc, b.rpc, 'a reconnect gets a fresh channel rather than one on a dead stream')
50})
51
52test('⛔ a bad key is refused before any connection is attempted', async (t) => {
53 await t.exception(() => connectBridge(swarmWith(fakeConn()), 'nonsense'), /64 hex/)
54})