OverviewFilesCommitsIssuesDocsReleasesPeers

favela-peer commits / 2c8c590

0.0.61: a 0.0.57 knock row is not a knock after the upgrade
Measured with the installed 0.0.57 tree against this one: reopening a 0.0.57 store under 0.0.60 does not reindex the view. The legacy row `__knocks__/<feed key> -> {device}` survives, and knocks() reported it as a knock from root = that feed key, with no feed, which could never be admitted. A fresh 0.0.60 node computing the same log reports no knock. knocks() now drops rows without the `feed` field the proven apply always writes, so upgraded and fresh nodes agree. Live: one such row on stortron's feed, one on fleeky's; no listings anywhere. The same drill found the log layer compatible in every mix of 2.1.1 and 2.12.1 participants (owner, second device, knocking stranger, holder): no frozen writer, cross-version device admission both ways, all posts on every node. Suite 407/407 (961 asserts). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2c8c5902f93119282d11bfe8b713c5d4345e90ba
by plankatron <fleeky@gmail.com> · 2026-10-13T01:11:14.000Z

Changed files (4) +65 -3

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` — 405 tests, 959 asserts, passing (`brittle-bare test/*.test.js`, 50 files,
160Working. `npm test` — 407 tests, 961 asserts, passing (`brittle-bare test/*.test.js`, 53 files,
161161~43 s).
162162
163163Deployed as a [pgh](../pgh) app. Measured 2026-09-20: 0.0.57 installed and running on tabertron and
lib/feed-base.js +6 -1
……@@ -279,10 +279,15 @@ export class Feed {
279279 }
280280
281281 // Live knocks: the PROVEN identity root that knocked, the feed it asked about, and its writer core.
282 //
283 // ⛔ A row without `feed` was written by 0.0.57's apply (`__knocks__/<feed key>`, no proof). An
284 // upgrade does not reindex the view, so such rows survive; the proven-knock apply never writes one,
285 // so a fresh node computing the same log has no knock there. Dropping them here makes an upgraded
286 // node agree with a fresh one (test/knock-legacy.test.js).
282287 async knocks () {
283288 const map = await this._readKv().list(KNOCKS + '/')
284289 return Object.entries(map)
285 .filter(([, v]) => v && v.$deleted !== true)
290 .filter(([, v]) => v && v.$deleted !== true && typeof v.feed === 'string')
286291 .map(([k, v]) => ({ root: k.slice(KNOCKS.length + 1), feed: v.feed, device: v.device }))
287292 .sort((a, b) => (a.root < b.root ? -1 : a.root > b.root ? 1 : 0))
288293 }
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "favela-peer",
3 "version": "0.0.60",
3 "version": "0.0.61",
44 "description": "The favela daemon \u2014 feeds and documents of markdown records over Hypercore. No atproto, no ActivityPub.",
55 "type": "module",
66 "scripts": {
test/knock-legacy.test.js +57 -0
……@@ -0,0 +1,57 @@
1// ⛔ A knock written by 0.0.57 is not a knock under 0.0.60, and an upgraded node must agree.
2//
3// MEASURED 2026-10-12 (scratch compat/reopen.mjs, the installed 0.0.57 tree vs this one): reopening a
4// 0.0.57 store under 0.0.60 does NOT reindex the view. The old apply's row stays as written,
5// `__knocks__/<their feed key> → { device }`, and the new reader took the key segment for an identity
6// root: knocks() returned { root: <a feed key>, feed: undefined }. A FRESH 0.0.60 node computing the
7// same log returns no knock at all, because the op carries no proof. So two nodes on the same version
8// showed different doorbells, and the upgraded one showed a knock that could never be admitted
9// (Peer._knockRootFor matches on `feed`, which the row does not have).
10//
11// Live at the time: stortron's feed held one such row and fleeky's another. No listings existed
12// anywhere, so only knocks are affected.
13//
14// The rule: a row without the `feed` field the proven-knock apply always writes is a legacy, unproven
15// knock, and the read side drops it exactly as a fresh apply would. The knocker asks again, with proof.
16
17import test from 'brittle'
18import b4a from 'b4a'
19import { Feed } from '../lib/feed-base.js'
20import { KNOCKS } from '../lib/feed.js'
21
22const ROOT = 'aa'.repeat(32)
23const THEIR_FEED = 'ab'.repeat(32)
24const LEGACY_FEED = '5271cdd4b5a4c478ce3274f1fb4f1be2b944dfc2e69a968dd6d23209ff3eda23'
25const DEVICE = '10505577bfa35efbb2ef244968b2662a7a817932cb6ccc1532deddafb07cb6f5'
26
27// A Feed over a fixed view: the bytes an upgraded store actually holds, read by this version's code.
28function feedOver (rows) {
29 const sorted = Object.entries(rows).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
30 const bee = {
31 async get (k) { const v = rows[b4a.toString(k, 'utf8')]; return v === undefined ? null : { value: b4a.from(JSON.stringify(v)) } },
32 async * createReadStream ({ gte, lt } = {}) {
33 for (const [k, v] of sorted) {
34 const kb = b4a.from(k, 'utf8')
35 if (gte && b4a.compare(kb, gte) < 0) continue
36 if (lt && b4a.compare(kb, lt) >= 0) continue
37 yield { key: kb, value: b4a.from(JSON.stringify(v)) }
38 }
39 }
40 }
41 const f = Object.create(Feed.prototype)
42 f.base = { view: bee }
43 return f
44}
45
46test('⛔ a 0.0.57 knock row in an upgraded view is not reported as a knock', async (t) => {
47 const f = feedOver({ [KNOCKS + '/' + LEGACY_FEED]: { device: DEVICE } })
48 t.alike(await f.knocks(), [], 'no proven root, so no knock: the same answer a fresh node computes')
49})
50
51test('a proven knock beside a legacy row is still reported, and only it', async (t) => {
52 const f = feedOver({
53 [KNOCKS + '/' + LEGACY_FEED]: { device: DEVICE },
54 [KNOCKS + '/' + ROOT]: { device: DEVICE, feed: THEIR_FEED }
55 })
56 t.alike(await f.knocks(), [{ root: ROOT, feed: THEIR_FEED, device: DEVICE }])
57})