OverviewFilesCommitsIssuesDocsReleasesPeers

favela-peer commits / 8ead0aa

0.0.59: autobee 2.12.1 — a stranger's knock no longer pins a holder's core
⛔ THE INCIDENT (2026-10-12). ap-bridge on stortron averaged a full core for days. One stranger's knock on fleeky's feed (a single optimistic block from a writer not on the roster) reached a holder opened with `optimistic: false`. In autobee 2.1.1 `_bumpPendingWriters` can apply neither branch, returns `true` without `removePending()`, and `_drain` reselects the same writer forever. Measured live: 110% CPU, drains frozen. Fixed upstream in autobee 2.5.1 (ed166f4, "remove writer as pending if not picked by any consumer"). test/holder-after-knock.test.js is the guard, and it ran red first: - on 2.1.1, a holder opened `optimistic: false` never returns from update(); - on 2.12.1 it does. Its second case pins the other half, which the upgrade does NOT fix: a holder must open feeds the way favela nodes do (`optimistic: true`), or it can never follow the owner past an acked knock. That bug is ap-bridge's FeedHolder. ★ One behaviour change upstream, in doorbell-wire.test.js. A knocker's later non-optimistic append is now refused AT append() with `Not writable`, where 2.1.1 accepted it locally and left apply to refuse it. Either refusal satisfies the property ("a doorbell grants no write"), so the test accepts both. Compatibility, measured 2026-10-12: - 2.12.1 reads the live fleeky and ashatron feeds (113/113, 95/95); - a 2.1.1 node (stortron's production tree) follows a 2.12.1 writer over the real swarm (5/5 entries, no freeze). Plain appends only: admission, knocks and encrypted feeds were NOT tested against a 2.1.1 reader. Lockfile: autobee's own closure moved (hypercore 11.38.0, hyperbee2 2.18.0, compact-encoding 3.5.2, hyperschema 1.26.2, read-write-mutexify 2.1.0; autobee-encryption is inlined upstream). protomux-rpc is now a real install, not a link into ap-bridge. Sibling entries caught up to their real versions. 397/397 tests, 943 asserts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8ead0aa73f7deedcbcd4abc48e1ffa452d8c3e41
by plankatron <fleeky@gmail.com> · 2026-10-12T20:26:54.000Z

Changed files (5) +220 -58

README.md +1 -1
……@@ -157,7 +157,7 @@ nothing on this side can send one yet. The bridge half is built; see hyper-socia
157157
158158## Status
159159
160Working. `npm test` — 395 tests, 938 asserts, passing (`brittle-bare test/*.test.js`, 50 files,
160Working. `npm test` — 397 tests, 943 asserts, passing (`brittle-bare test/*.test.js`, 50 files,
161161~43 s).
162162
163163Deployed as a [pgh](../pgh) app. Measured 2026-09-20: 0.0.57 installed and running on tabertron and
package-lock.json +85 -54
……@@ -1,21 +1,22 @@
11{
22 "name": "favela-peer",
3 "version": "0.0.38",
3 "version": "0.0.58",
44 "lockfileVersion": 3,
55 "requires": true,
66 "packages": {
77 "": {
88 "name": "favela-peer",
9 "version": "0.0.38",
9 "version": "0.0.58",
1010 "dependencies": {
1111 "anproto": "file:../anproto",
12 "autobee": "^2.1.0",
12 "autobee": "^2.12.1",
1313 "b4a": "^1.6.0",
1414 "bare-fs": "^4.8.1",
1515 "bare-net": "^2.3.3",
1616 "bare-os": "^3.9.3",
1717 "bare-path": "^3.1.2",
1818 "bare-process": "^4.5.1",
19 "bare-subprocess": "^6.1.0",
1920 "bip39-mnemonic": "^2.2.0",
2021 "blind-pairing": "^2.3.1",
2122 "compact-encoding": "^3.3.0",
……@@ -25,9 +26,11 @@
2526 "hyper-identity2": "file:../packages/hyper-identity2",
2627 "hyper-overlay": "file:../hyper-overlay",
2728 "hypercore-crypto": "^3.7.0",
29 "hyperident": "file:../repos/hyperident",
2830 "hyperswarm": "^4.17.0",
2931 "keet-identity-key": "3.2.0",
3032 "protomux": "^3.11.0",
33 "protomux-rpc": "^1.10.0",
3134 "sodium-hmac": "^2.0.1"
3235 },
3336 "bin": {
……@@ -50,10 +53,11 @@
5053 }
5154 },
5255 "../favela-identity": {
53 "version": "0.0.2",
56 "version": "0.0.4",
5457 "license": "MIT",
5558 "dependencies": {
5659 "b4a": "^1.6.0",
60 "bare-crypto": "^1.5.0",
5761 "bare-fs": "^4.0.0",
5862 "bare-net": "^1.3.0",
5963 "bare-path": "^3.0.0",
……@@ -66,7 +70,7 @@
6670 }
6771 },
6872 "../favela-markdown": {
69 "version": "0.1.5",
73 "version": "0.2.0",
7074 "license": "MIT",
7175 "dependencies": {
7276 "bare-fs": "^4.8.1",
……@@ -87,7 +91,7 @@
8791 }
8892 },
8993 "../hyper-overlay": {
90 "version": "0.3.13",
94 "version": "0.3.17",
9195 "license": "AGPL-3.0",
9296 "dependencies": {
9397 "b4a": "^1.0.0",
……@@ -126,6 +130,30 @@
126130 "corestore": "^7.12.3"
127131 }
128132 },
133 "../repos/hyperident": {
134 "version": "0.2.6",
135 "license": "Apache-2.0",
136 "dependencies": {
137 "b4a": "^1.6.7",
138 "bare-crypto": "^1.11.1",
139 "bare-fs": "^4.7.2",
140 "bare-os": "^2.4.2",
141 "bare-path": "^2.1.3",
142 "bare-process": "^4.2.2",
143 "bare-subprocess": "^3.1.4",
144 "hypercore-id-encoding": "^1.3.0",
145 "hyperswarm": "^4.12.1",
146 "keet-identity-key": "3.2.0",
147 "sodium-universal": "^5.0.1"
148 },
149 "bin": {
150 "hyperident": "index.js"
151 },
152 "devDependencies": {
153 "brittle": "^3.0.0",
154 "pear-interface": "^1.0.0"
155 }
156 },
129157 "node_modules/@hyperswarm/secret-stream": {
130158 "version": "6.9.1",
131159 "resolved": "https://registry.npmjs.org/@hyperswarm/secret-stream/-/secret-stream-6.9.1.tgz",
……@@ -192,45 +220,25 @@
192220 "link": true
193221 },
194222 "node_modules/autobee": {
195 "version": "2.1.1",
196 "resolved": "https://registry.npmjs.org/autobee/-/autobee-2.1.1.tgz",
197 "integrity": "sha512-El9Pzu3N3Ytsrtsj2bCpG+ph4ThRqeN3ghiaDq3rF+LbRKigAbo6WyMRb+eLupiLXXYLxpgmUudHRHKny5by+Q==",
223 "version": "2.12.1",
224 "resolved": "https://registry.npmjs.org/autobee/-/autobee-2.12.1.tgz",
225 "integrity": "sha512-iUo6ixwHPuoZ9YpHHhEZyv1O5uhvLCP9XTpO2/msrkno3l1n4iOHkEv1JkohK+qAsakaCwrhNDnV6j+lXmZcPw==",
198226 "license": "Apache-2.0",
199227 "dependencies": {
200 "autobee-encryption": "^0.1.2",
201228 "autobee-wakeup": "^1.1.3",
202229 "b4a": "^1.8.0",
203 "compact-encoding": "^3.3.0",
204 "hyperbee2": "^2.10.3",
205 "hypercore": "^11.33.2",
230 "compact-encoding": "^3.4.0",
231 "hyperbee2": "^2.18.0",
232 "hypercore": "^11.37.0",
206233 "hypercore-id-encoding": "^1.3.0",
207 "hyperschema": "^1.20.1",
234 "hyperschema": "^1.26.2",
235 "read-write-mutexify": "^2.1.0",
208236 "ready-guard": "^1.0.0",
209237 "ready-resource": "^1.2.0",
210238 "resolve-reject-promise": "^1.1.0",
211 "safety-catch": "^1.0.3"
212 }
213 },
214 "node_modules/autobee-encryption": {
215 "version": "0.1.3",
216 "resolved": "https://registry.npmjs.org/autobee-encryption/-/autobee-encryption-0.1.3.tgz",
217 "integrity": "sha512-tmO76eMM9LEXXBtjJUCRyUxhIq0UkN2ZTjSLpjoEjSKwOXIOI0acjzOp4M0NPGbyA7SFxoIv0w9gjmFO6zjKOw==",
218 "license": "Apache-2.0",
219 "dependencies": {
220 "b4a": "^1.8.0",
221 "compact-encoding": "^2.19.0",
222 "hypercore": "^11.27.14",
223 "hypercore-crypto": "^3.6.1",
224 "sodium-universal": "^5.0.1"
225 }
226 },
227 "node_modules/autobee-encryption/node_modules/compact-encoding": {
228 "version": "2.19.2",
229 "resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-2.19.2.tgz",
230 "integrity": "sha512-/YjhHQE/5L4F7l5Bht69dRbP9RV6zoJPeowi8bMKQxNKe3Nh6hOY8pBGoVE9fz5GaWfEd8fWJ2aU9sB4KZuMYg==",
231 "license": "Apache-2.0",
232 "dependencies": {
233 "b4a": "^1.3.0"
239 "safety-catch": "^1.0.3",
240 "signal-promise": "^1.0.3",
241 "sodium-native": "^5.1.0"
234242 }
235243 },
236244 "node_modules/autobee-wakeup": {
……@@ -313,7 +321,6 @@
313321 "version": "3.7.1",
314322 "resolved": "https://registry.npmjs.org/bare-buffer/-/bare-buffer-3.7.1.tgz",
315323 "integrity": "sha512-cIjZnSO+y89ykVX9x96OVnA6tGORBn7vGJ/e3PCbBHMEyfMNG/ixzOS0lLa6saMlWcs69bSKjxFyDwjK152OMg==",
316 "devOptional": true,
317324 "license": "Apache-2.0",
318325 "peer": true,
319326 "engines": {
……@@ -769,7 +776,6 @@
769776 "version": "1.6.0",
770777 "resolved": "https://registry.npmjs.org/bare-structured-clone/-/bare-structured-clone-1.6.0.tgz",
771778 "integrity": "sha512-AZjEERyqF7kAuudlmgrweT2KwwWM0LsGG4Gx/bWyFwJrKU7E3wNG8c09z2DIrdw93p3vDtfOX8fyHOcXfy5m+g==",
772 "dev": true,
773779 "license": "Apache-2.0",
774780 "dependencies": {
775781 "bare-buffer": "^3.6.0",
……@@ -796,7 +802,6 @@
796802 "version": "6.1.0",
797803 "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-6.1.0.tgz",
798804 "integrity": "sha512-8L6KtbmreDy4Fc1BdDqaDo9fg0nbedUmTSFQxYPV0uHIM2BBAX8+E0LyMDKgkk0I+mlKto80WYzOnIhT8VDdOg==",
799 "dev": true,
800805 "license": "Apache-2.0",
801806 "dependencies": {
802807 "bare-env": "^3.0.0",
……@@ -1084,9 +1089,9 @@
10841089 }
10851090 },
10861091 "node_modules/compact-encoding": {
1087 "version": "3.4.0",
1088 "resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-3.4.0.tgz",
1089 "integrity": "sha512-PWfHlCzU26lI/J/Fbch0xjt3T6uetZ6n/q+nlpCqTfp5Q9zAt3s2SO2qA0BEVFq9ASIq/JyNnFa4EraLXaeyqg==",
1092 "version": "3.5.2",
1093 "resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-3.5.2.tgz",
1094 "integrity": "sha512-rgGcWdfxyuLwJHMUXI0OrYGpJe3VN4RZyovbxWVlxPYIsUZutOTfP3shmDyDi/iaybwcxMsTJDDuxQa4xRG0RQ==",
10901095 "license": "Apache-2.0",
10911096 "dependencies": {
10921097 "b4a": "^1.3.0"
……@@ -1282,9 +1287,9 @@
12821287 "link": true
12831288 },
12841289 "node_modules/hyperbee2": {
1285 "version": "2.13.2",
1286 "resolved": "https://registry.npmjs.org/hyperbee2/-/hyperbee2-2.13.2.tgz",
1287 "integrity": "sha512-MbQnNflVnYrGLIO+KkLT1icmU4j5LmHg1RBgRGZx/+/YsnHSlf+NfoRErv5MI681CyVqFplF2MrdWScphqX28w==",
1290 "version": "2.18.0",
1291 "resolved": "https://registry.npmjs.org/hyperbee2/-/hyperbee2-2.18.0.tgz",
1292 "integrity": "sha512-EmRvk59koT/JAmJ2TCZMHwTPhZ/MS7F5xTD/VIGcjZZ/oJKHbelf0sF6YUPwhUKjIW6pOIYE+GcoyOplB5AUxA==",
12881293 "license": "Apache-2.0",
12891294 "dependencies": {
12901295 "b4a": "^1.6.7",
……@@ -1292,16 +1297,16 @@
12921297 "compact-encoding": "^3.0.0",
12931298 "hypercore": "^11.29.0",
12941299 "hypercore-errors": "^1.5.0",
1295 "hyperschema": "^1.14.0",
1300 "hyperschema": "^1.26.1",
12961301 "protocol-buffers-encodings": "^1.2.0",
12971302 "scope-lock": "^1.2.4",
12981303 "streamx": "^2.22.1"
12991304 }
13001305 },
13011306 "node_modules/hypercore": {
1302 "version": "11.35.4",
1303 "resolved": "https://registry.npmjs.org/hypercore/-/hypercore-11.35.4.tgz",
1304 "integrity": "sha512-TdTLYX2eK00l+o2uPJTcZocVSRoM28BprsIhlSe0WwFD9QeZxJgfX7NS0uVz4RbwqNt+x4IG7hEz6uIg+MErGw==",
1307 "version": "11.38.0",
1308 "resolved": "https://registry.npmjs.org/hypercore/-/hypercore-11.38.0.tgz",
1309 "integrity": "sha512-YOluEXnHorNwSOxX3BAfv+8uicMq6WRYUa7VRPbOqVHom543r7myJN23/BeqLoWkG0Dz/4aASWQ96+PXJbJxKw==",
13051310 "license": "MIT",
13061311 "dependencies": {
13071312 "@hyperswarm/secret-stream": "^6.0.0",
……@@ -1418,14 +1423,18 @@
14181423 "hyperschema": "^1.20.1"
14191424 }
14201425 },
1426 "node_modules/hyperident": {
1427 "resolved": "../repos/hyperident",
1428 "link": true
1429 },
14211430 "node_modules/hyperschema": {
1422 "version": "1.23.0",
1423 "resolved": "https://registry.npmjs.org/hyperschema/-/hyperschema-1.23.0.tgz",
1424 "integrity": "sha512-BxUxIyNoJf8qAeEbkNodN8npcdKSqZNtJweemQFq+nTkAr6dL3rSH6oPN3IabuGBjuJafg/weBT8c/ALebSkwA==",
1431 "version": "1.26.2",
1432 "resolved": "https://registry.npmjs.org/hyperschema/-/hyperschema-1.26.2.tgz",
1433 "integrity": "sha512-5Y7C5zCfH28yiK7RhuMl7VkWt24Mr0E5sp1r2ntd3Ab3+yOBdkDWau0/d/F5beC799Vd5e4xqIAu5NJLM7i//w==",
14251434 "license": "Apache-2.0",
14261435 "dependencies": {
14271436 "bare-fs": "^4.0.1",
1428 "compact-encoding": "^3.0.0",
1437 "compact-encoding": "^3.5.0",
14291438 "generate-object-property": "^2.0.0",
14301439 "generate-string": "^1.0.1"
14311440 }
……@@ -1570,6 +1579,22 @@
15701579 "unslab": "^1.3.0"
15711580 }
15721581 },
1582 "node_modules/protomux-rpc": {
1583 "version": "1.10.1",
1584 "resolved": "https://registry.npmjs.org/protomux-rpc/-/protomux-rpc-1.10.1.tgz",
1585 "integrity": "sha512-pFt+zoc32k5Ty6Mrg8EXmq2ja4wsODWeYsCV2MNyYjwGE9VuiUqou+OyyzbT4Lym0PCrWFmPoqny1W2ZD8pNJQ==",
1586 "license": "Apache-2.0",
1587 "dependencies": {
1588 "bits-to-bytes": "^1.0.0",
1589 "compact-encoding": "^3.0.0",
1590 "compact-encoding-bitfield": "^1.0.0",
1591 "protomux": "^3.7.0",
1592 "safety-catch": "^1.0.2"
1593 },
1594 "optionalDependencies": {
1595 "bare-events": "^2.2.0"
1596 }
1597 },
15731598 "node_modules/protomux-wakeup": {
15741599 "version": "2.9.0",
15751600 "resolved": "https://registry.npmjs.org/protomux-wakeup/-/protomux-wakeup-2.9.0.tgz",
……@@ -1617,6 +1642,12 @@
16171642 "integrity": "sha512-u7xCM93XqKEvPTP6xZp2ehttcAemKnh73oKNf1FvzuVCfpt6dILDt1Kxl1LeBjm2iNIeR49VGFhy4Iz3yOun+Q==",
16181643 "license": "MIT"
16191644 },
1645 "node_modules/read-write-mutexify": {
1646 "version": "2.1.0",
1647 "resolved": "https://registry.npmjs.org/read-write-mutexify/-/read-write-mutexify-2.1.0.tgz",
1648 "integrity": "sha512-fDw/p5/acI1ytVY1UbxEDma/ej1yJH/n9NcjS9YNzcE6sPBPWdlru3ydRa/UBowUg4zqOvNMD5SOGYJrlQ6MzQ==",
1649 "license": "MIT"
1650 },
16201651 "node_modules/ready-guard": {
16211652 "version": "1.0.0",
16221653 "resolved": "https://registry.npmjs.org/ready-guard/-/ready-guard-1.0.0.tgz",
package.json +2 -2
……@@ -1,6 +1,6 @@
11{
22 "name": "favela-peer",
3 "version": "0.0.58",
3 "version": "0.0.59",
44 "description": "The favela daemon \u2014 feeds and documents of markdown records over Hypercore. No atproto, no ActivityPub.",
55 "type": "module",
66 "scripts": {
……@@ -31,7 +31,7 @@
3131 },
3232 "dependencies": {
3333 "anproto": "file:../anproto",
34 "autobee": "^2.1.0",
34 "autobee": "^2.12.1",
3535 "b4a": "^1.6.0",
3636 "bare-fs": "^4.8.1",
3737 "bare-net": "^2.3.3",
test/doorbell-wire.test.js +14 -1
……@@ -107,9 +107,22 @@ test('⛔ and it grants nothing — a knock is not admission', async (t) => {
107107 // a local writer core, so autobase reports the base as writable from that moment — "I have a core I
108108 // can append to", not "my appends are admitted". Asserting on it would test the wrong thing and
109109 // pass for the wrong reason. The property that matters is whether a RECORD lands, so write one.
110 //
111 // ★ Two places can refuse it, and either one satisfies the property. On autobee 2.1.1 the append
112 // was accepted locally and refused by apply. From 2.12.1 autobee refuses it AT THE SOURCE: once the
113 // owner has acked the knock, the knocker's writer is acked-but-not-added (`isRemoved`), and a
114 // non-optimistic append from it throws `Not writable` (autobee index.js `append`). Measured
115 // 2026-10-12. So the throw is a refusal, not a test failure, and the read-back below still runs.
110116 const { POST } = await import('../lib/peer.js')
111 await aliceOnBob.put(POST, 'intruder', { frontmatter: { createdAt: '2026-09-15T00:00:00Z' }, body: 'not mine to write' })
117 let refusedLocally = false
118 try {
119 await aliceOnBob.put(POST, 'intruder', { frontmatter: { createdAt: '2026-09-15T00:00:00Z' }, body: 'not mine to write' })
120 } catch (err) {
121 if (!/Not writable/.test(err.message)) throw err
122 refusedLocally = true
123 }
112124 await aliceOnBob.update()
125 t.comment(refusedLocally ? 'refused at append (autobee ≥2.12)' : 'accepted locally, left to apply to refuse')
113126
114127 // Give it every chance to arrive before concluding it did not — a race that passes by being fast
115128 // is not a refusal.
test/holder-after-knock.test.js +118 -0
……@@ -0,0 +1,118 @@
1// A READ-ONLY HOLDER of a feed somebody has knocked on.
2//
3// ⛔ THE INCIDENT (2026-10-12). ap-bridge holds registered users' feeds read-only, opened with
4// `optimistic: false` (FeedHolder passes no opts, and Feed defaults the doorbell off). A stranger had
5// knocked on fleeky's feed: one optimistic block from a writer that is not on the roster. On autobee
6// 2.1.1 the holder pinned a core for days. `_bumpPendingWriters` could not apply that block (writer
7// not added, base not optimistic), returned `true` without `removePending()`, and `_drain` reselected
8// the same writer forever. Measured live: 110% CPU, drains frozen, the stranger's writer at
9// isAdded:false with 1 optimistic block. Fixed upstream in autobee 2.5.1 (ed166f4).
10//
11// ⛔ AND A SECOND DEFECT, WHICH THE UPGRADE DOES NOT FIX. The owner's apply ACKS a knock, so on the
12// owner it sticks, and everything the owner writes afterwards links past it. A holder that refuses
13// optimistic blocks cannot apply the knock, so every later owner entry is unlinkable on that holder.
14// Live, fleeky's writer sat at 106 of 113 applied: the bridge never saw the newest posts. So:
15// 1. the holder's update() finishes (the spin, fixed by autobee ≥2.5.1);
16// 2. a holder opened like a favela node (optimistic:true) follows the owner past the knock.
17
18import test from 'brittle'
19import fs from '#node:fs'
20import os from '#node:os'
21import { join } from '#node:path'
22import Corestore from 'corestore'
23import { RecordStore } from '../lib/record-store.js'
24import { Feed } from '../lib/feed-base.js'
25import { POST } from '../lib/peer.js'
26import { attest } from './helpers.js'
27
28const tmp = () => fs.mkdtempSync(join(os.tmpdir(), 'favela-holder-knock-'))
29
30function pipe (a, b) {
31 const s1 = a.replicate(true)
32 const s2 = b.replicate(false)
33 s1.pipe(s2).pipe(s1)
34 return () => { s1.destroy(); s2.destroy() }
35}
36
37async function until (fn, { timeout = 15000 } = {}) {
38 const deadline = Date.now() + timeout
39 while (Date.now() < deadline) {
40 const v = await fn()
41 if (v) return v
42 await new Promise((r) => setTimeout(r, 25))
43 }
44 return null
45}
46
47// update() bounded, because the defect under test is an update that never returns.
48const settles = (p, ms) => Promise.race([p.then(() => true), new Promise((r) => setTimeout(() => r(false), ms))])
49
50async function mkFeed (store, key, records, opts = {}) {
51 const f = new Feed(store, key, { store: new RecordStore(records), ...opts })
52 await f.ready()
53 return f
54}
55
56async function owned (records) {
57 const f = await mkFeed(new Corestore(tmp()), null, records, { optimistic: true })
58 await f.addDevice(await attest(f))
59 await f.update()
60 return f
61}
62
63// alice owns a feed; bob knocks on it; alice then posts. Returns alice and her records dir.
64async function knockedFeed (t) {
65 const records = tmp()
66 const alice = await owned(records)
67 const bob = await owned(tmp())
68 const aliceOnBob = await mkFeed(new Corestore(tmp()), alice.key, records, { optimistic: true })
69 const unpipe = pipe(alice, aliceOnBob)
70
71 await until(async () => { await aliceOnBob.update(); return (await aliceOnBob.devices()).length > 0 })
72 await aliceOnBob.knock(bob.key)
73 const knocked = await until(async () => { await alice.update(); return (await alice.knocks()).length })
74 t.ok(knocked, 'setup: the stranger\'s knock landed in the owner\'s feed')
75
76 await alice.put(POST, 'after-knock', { frontmatter: { createdAt: '2026-10-12T00:00:00Z' }, body: 'written after the knock' })
77 await alice.update()
78
79 return { alice, records, cleanup: async () => { unpipe(); await bob.close(); await aliceOnBob.close() } }
80}
81
82test('★ a read-only holder (optimistic:false, as ap-bridge opens it) is not wedged by a knock', async (t) => {
83 const { alice, records, cleanup } = await knockedFeed(t)
84
85 const holder = await mkFeed(new Corestore(tmp()), alice.key, records, { optimistic: false })
86 const unpipe = pipe(alice, holder)
87 t.teardown(async () => { unpipe(); await cleanup(); await alice.close(); await settles(holder.close(), 3000) })
88
89 // Let the knock replicate to the holder before asking it to settle.
90 await until(async () => (await settles(holder.update(), 2000)) && (await holder.devices()).length > 0, { timeout: 10000 })
91
92 // A — the spin. Red on 2.1.1, green on 2.12.1.
93 t.ok(await settles(holder.update(), 5000), 'A: the holder\'s update() returns (2.1.1 reselects the knock forever)')
94
95 // B is NOT asserted here, because with optimistic:false it cannot hold. MEASURED on 2.12.1: the
96 // holder settles but never sees `after-knock`. It refuses the knock the owner acked, so it can never
97 // link past it, and it computes a different view of the same log. That is a configuration defect in
98 // whoever opens a holder this way (ap-bridge's FeedHolder did), and the next test is its guard.
99})
100
101// ★ The configuration every favela node already uses for a held feed (node.js: the doorbell is on by
102// default). A holder must apply the log under the SAME rules as the owner, or it computes a different
103// view of it: the owner acks the knock, so a holder that refuses optimistic blocks can never link past
104// it.
105test('★ a holder opened the way favela nodes open feeds (optimistic:true) follows the owner past a knock', async (t) => {
106 const { alice, records, cleanup } = await knockedFeed(t)
107
108 const holder = await mkFeed(new Corestore(tmp()), alice.key, records, { optimistic: true })
109 const unpipe = pipe(alice, holder)
110 t.teardown(async () => { unpipe(); await cleanup(); await alice.close(); await settles(holder.close(), 3000) })
111
112 const seen = await until(async () => {
113 if (!(await settles(holder.update(), 2000))) return null
114 return (await holder.pointers(POST)).some((p) => p.name === 'after-knock') || null
115 }, { timeout: 10000 })
116 t.ok(seen, 'the holder sees what the owner wrote after the knock')
117 t.absent(holder.writable && (await holder.devices()).length > 1, 'and holding grants it nothing on the roster')
118})