| … | … | @@ -1660,6 +1660,35 @@ const server = http.createServer(async (req, res) => { |
| 1660 | 1660 | // Posts tab); the appview keeps the lexicon default. An unknown value degrades inside getAuthorFeed |
| 1661 | 1661 | // rather than erroring, so a hand-edited query string cannot 500 a profile. |
| 1662 | 1662 | const filter = url.searchParams.get('filter') || 'posts_no_replies' |
| 1663 | // ⛔ A PROFILE WE DO NOT SUBSCRIBE TO GOES STALE SILENTLY, AND LOOKS CURRENT. Nothing tails an |
| 1664 | // identity outside the roster, so an earlier one-shot view renders as a complete, plausible profile |
| 1665 | // that is simply months out of date — old posts, and the old avatar CID because the PROFILE RECORD |
| 1666 | // is old too. Measured on a handset (hyper-social #53): ashatron's posts 16 days behind and their |
| 1667 | // previous avatar shown as though it were current. That reads as corruption, not as "not |
| 1668 | // subscribed", which is the worst way for this to fail. |
| 1669 | // |
| 1670 | // So looking at someone refreshes them. addPeer is the same idempotent runtime path /api/track |
| 1671 | // uses — join the topic, resolve a verify key, start the ingest loop — and the loop then keeps the |
| 1672 | // profile current for as long as this node runs. |
| 1673 | // |
| 1674 | // ⚠ GATED TO THE NODE'S OWN OPERATOR. `viewer.did === the DID this node signs for` is the same test |
| 1675 | // /api/follow uses. Without it, any anonymous visitor to a public instance could make it join an |
| 1676 | // arbitrary topic per URL — a page view must not be a remote "make this node subscribe" button. |
| 1677 | // On a handset the operator is the only viewer there is, which is the case this fixes. |
| 1678 | // |
| 1679 | // Detached and non-fatal: the page must render at snapshot speed. An unreachable DID (every |
| 1680 | // bsky-hosted account) simply resolves no peer, and the render is unaffected. |
| 1681 | if (viewer && !(HOSTED_DIDS || []).includes(did) && !ROSTER.includes(did)) { |
| 1682 | try { |
| 1683 | const st = await writer.state().catch(() => null) |
| 1684 | if (st && st.did === viewer.did) { |
| 1685 | addPeer(did).then( |
| 1686 | (p) => { if (p) console.log(JSON.stringify({ msg: 'profile view: subscribed, ingest running', did })) }, |
| 1687 | (e) => console.error(JSON.stringify({ msg: 'profile view: subscribe failed (page still rendered)', did, error: e.message })) |
| 1688 | ) |
| 1689 | } |
| 1690 | } catch { /* the page is the product; a refresh attempt must never break it */ } |
| 1691 | } |
| 1663 | 1692 | const result = await withLensBylines(await withMod(await av.getAuthorFeed(did, { limit: 50, cursor: url.searchParams.get('cursor') || null, viewerDid: viewer ? viewer.did : null, filter }), viewer ? viewer.did : null)) |
| 1664 | 1693 | // The profile header needs the SUBJECT's own profile — the same indexed record the per-post |
| 1665 | 1694 | // hydration uses, just looked up by the DID being viewed rather than by each post's author. |
| … | … | @@ -2275,6 +2304,24 @@ const server = http.createServer(async (req, res) => { |
| 2275 | 2304 | const record = { $type: 'app.bsky.graph.follow', subject, createdAt: new Date().toISOString() } |
| 2276 | 2305 | await writer.putRecord('app.bsky.graph.follow', rkey, record, { action: 'create' }) |
| 2277 | 2306 | await av.putFollow({ did: deviceDid, rkey, record }) |
| 2307 | // ⛔ SUBSCRIBE NOW, NOT AT THE NEXT BOOT. A follow is the durable, replicating statement of |
| 2308 | // interest in an identity, and it used to change nothing until the node restarted: FAVELA_ROSTER |
| 2309 | // is captured at import, and favela-android's syncFollows runs only inside boot(). So following |
| 2310 | // someone left their posts absent — or worse, left an earlier one-shot view on screen, ageing |
| 2311 | // silently, which is how a handset came to show a two-week-old profile and its previous avatar |
| 2312 | // as though they were current (hyper-social #53). |
| 2313 | // |
| 2314 | // addPeer is the same runtime path /api/track uses: it joins the DID's topic, resolves a verify |
| 2315 | // key and starts the ingest loop. Idempotent, so a re-follow costs nothing. |
| 2316 | // |
| 2317 | // Detached and non-fatal ON PURPOSE. The follow RECORD is the durable half and it is already |
| 2318 | // written; making the response wait on a DHT rendezvous would turn a local write into a |
| 2319 | // network-latency operation, and an unreachable peer (every bsky-hosted account, which no peer |
| 2320 | // serves) would read as "follow failed" when the follow succeeded perfectly. |
| 2321 | addPeer(subject).then( |
| 2322 | (p) => { if (!p) console.log(JSON.stringify({ msg: 'follow: nobody serves this DID yet — the follow stands, ingest starts when someone does', did: subject })) }, |
| 2323 | (e) => console.error(JSON.stringify({ msg: 'follow: subscribe failed (the follow itself is written)', did: subject, error: e.message })) |
| 2324 | ) |
| 2278 | 2325 | } |
| 2279 | 2326 | if (isForm) return redirect(res, home) |
| 2280 | 2327 | return json(res, 200, JSON.stringify({ ok: true, following: !existing })) |