OverviewFilesCommitsIssuesDocsReleasesPeers

favela-host commits / c073187

name the subject: compute the CID, do not look it up
An atproto record's CID is a pure function of its bytes, so naming one of our own records needs no index and no round trip: `cidForValue(value).cid`. VERIFIED against the live repo — the computed CID for a published post matched the one pds.7rnx.net serves, exactly: bafyreiekzvg6iuptum46mrtsxczsb723ds4ce5t5ez5uomeve4esxq33ea. ⛔ atproto-repo WAS NOT RESOLVABLE FROM HERE, and the catch hid it. Every subject came back null, nothing would have projected, and the log would have said "the subject is not in the atproto repo yet" for each one — which is the message for an ordinary WAIT. A silent null is indistinguishable from a post that has not been published, which is exactly the pair a person cannot tell apart from outside. Symlinked as a sibling (the fleet convention, never npm install) and the failure now says itself. ⚠ resolveRoot is deliberately absent: with no chain to walk, planEdge falls back to parent-as-root, which is correct for a reply to a top-level post — every reply we can currently make, since a reply to a reply needs the foreign-subject decision to be useful anyway. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
c073187d0869676af868c19b1e3a271d956cffba
by plankatron <fleeky@gmail.com> · 2026-09-12T01:51:00.000Z

Changed files (2) +51 -2

lib/project.js +50 -2
……@@ -20,7 +20,8 @@
2020import fs from 'bare-fs'
2121import { dispatch } from 'favela-peer/lib/rpc.js'
2222import { projectCycle } from 'favela-bridge/lib/project.js'
23import { BskySink } from 'favela-bridge/lib/bsky.js'
23import { BskySink, POST_COLLECTION } from 'favela-bridge/lib/bsky.js'
24import { tidFor } from 'favela-bridge/lib/tid.js'
2425
2526/**
2627 * hpp's UDS control surface, as a function call.
……@@ -134,7 +135,54 @@ export async function projectBsky (node, hpp, { send = false, limit = Infinity,
134135 if (error) return { refused: error, sent: 0, skipped: 0, withheld: 0, held: 0 }
135136
136137 const bsky = new BskySink(null, { connect: async () => deviceClient(hppNode), log: (m, d) => log(m + (d ? ' ' + JSON.stringify(d) : '')) })
137 return projectCycle({ peer, bsky, roomType, send, limit, reproject, log, sinkLabel: 'this device (' + hppNode.did + ')' })
138
139 /**
140 * A favela target → the atproto subject that names it.
141 *
142 * ⛔ THE CID IS COMPUTED, NOT LOOKED UP. An atproto record's CID is a pure function of its bytes —
143 * `cidForValue(value)` — so naming our own record needs no extra index and no round trip to the PDS.
144 * Verified against the live repo: the computed CID for a published post matched the one the PDS
145 * serves, byte for byte.
146 *
147 * ⛔ AND IT RETURNS null RATHER THAN GUESSING. A subject that is not in the repo yet is the ordinary
148 * case when an edge is projected before the post it points at; planEdge reports that as a wait, and
149 * the next pass finds it. Inventing a CID would publish a like pointing at nothing.
150 */
151 const resolveSubject = async (target) => {
152 if (!target || target.collection !== 'post') return null
153 const rkey = tidFor(target.name)
154 let value = null
155 try { value = await bsky.getRecord(POST_COLLECTION, rkey) } catch { value = null }
156 if (!value || value.unknown) return null
157 try {
158 const { cidForValue } = await import('atproto-repo/lib/cid.js')
159 const c = await cidForValue(value)
160 return { uri: 'at://' + hppNode.did + '/' + POST_COLLECTION + '/' + rkey, cid: String(c.cid) }
161 } catch (err) {
162 // ⛔ SAID OUT LOUD. This catch hid a real defect once already: `atproto-repo` was not resolvable
163 // from this package, every subject came back null, and NOTHING was projected — with the log
164 // reporting each edge as "the subject is not in the atproto repo yet", which is the message for
165 // an ordinary wait. A silent null here is indistinguishable from a subject that has not been
166 // published, and that is precisely the pair a person cannot tell apart from the outside.
167 log('subject could not be named — ' + ((err && err.message) || String(err)))
168 return null
169 }
170 }
171
172 return projectCycle({
173 peer,
174 bsky,
175 roomType,
176 send,
177 limit,
178 reproject,
179 log,
180 sinkLabel: 'this device (' + hppNode.did + ')',
181 resolveSubject
182 // ⚠ resolveRoot is deliberately absent for now: with no chain to walk, planEdge falls back to
183 // parent-as-root, which is correct for a reply to a top-level post and is every reply we can
184 // currently make (a reply to a reply needs the foreign-subject decision to be useful anyway).
185 })
138186}
139187
140188export default { deviceClient, peerClient, pickNode, projectBsky }
package.json +1 -0
……@@ -13,6 +13,7 @@
1313 "dependencies": {
1414 "anproto": "file:../anproto",
1515 "atproto-identity": "file:../atproto-identity",
16 "atproto-repo": "file:../atproto-repo",
1617 "bare-fs": "^4.8.1",
1718 "bare-http1": "^4.5.7",
1819 "bare-net": "^2.3.3",