| … | … | @@ -20,7 +20,8 @@ |
| 20 | 20 | import fs from 'bare-fs' |
| 21 | 21 | import { dispatch } from 'favela-peer/lib/rpc.js' |
| 22 | 22 | import { projectCycle } from 'favela-bridge/lib/project.js' |
| 23 | | import { BskySink } from 'favela-bridge/lib/bsky.js' |
| 23 | import { BskySink, POST_COLLECTION } from 'favela-bridge/lib/bsky.js' |
| 24 | import { tidFor } from 'favela-bridge/lib/tid.js' |
| 24 | 25 | |
| 25 | 26 | /** |
| 26 | 27 | * hpp's UDS control surface, as a function call. |
| … | … | @@ -134,7 +135,54 @@ export async function projectBsky (node, hpp, { send = false, limit = Infinity, |
| 134 | 135 | if (error) return { refused: error, sent: 0, skipped: 0, withheld: 0, held: 0 } |
| 135 | 136 | |
| 136 | 137 | 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 | }) |
| 138 | 186 | } |
| 139 | 187 | |
| 140 | 188 | export default { deviceClient, peerClient, pickNode, projectBsky } |