OverviewFilesCommitsIssuesDocsReleasesPeers

favela-web / lib / reach.js

64 lines · javascript ref:
// reach — where a post actually went.
//
// THE GAP THIS FILLS. Every public post already reaches bsky AND the fediverse, from the same record
// bytes, automatically. Verified 2026-08-05: "need to package" (2026-08-05T13:38:23.294Z) is in
// app.bsky.feed.getAuthorFeed AND in ap.7rnx.net's outbox, same timestamp. But nothing in the client
// ever said so, so the one architecturally interesting fact about this project — one record, several
// networks, no copies — was invisible to the person using it.
//
// ★ OBSERVED vs ELIGIBLE — the distinction is the whole point, and collapsing it would be a lie.
//
//   fediverse  OBSERVED. ap-bridge's outbox is the actual list of what it projected. If an rkey is
//              there, it was published. One cheap fetch covers a whole page of posts.
//   bsky       ELIGIBLE, not observed. We know bsky's AppView indexes `app.bsky.*` lexicons and
//              ignores everything else, so the collection tells us whether the post was OFFERED.
//              Whether bsky actually indexed it needs a per-post API call — too expensive for a
//              timeline, and its absence would usually mean lag rather than rejection.
//   favela     always. We hold the record; that is what "we have it" means.
//
// So the UI must not render these three identically. "went to bsky" and "bsky has it" are different
// claims, and this codebase has repeatedly paid for surfaces that reported success they had not
// checked (#24, #26: "everything we control reports success").

import { scopeForCollection } from './scopes.js'

export const TIER = Object.freeze({
  FAVELA: 'favela',
  BSKY: 'bsky',
  FEDIVERSE: 'fediverse'
})

// -> [{ tier, state }] where state is 'observed' | 'eligible'.
//
// apRkeys is a Set of rkeys ap-bridge reports having projected, or null when the AP side is not
// configured/reachable. NULL IS NOT "NO" — it means unknown, and an unknown must not render as an
// absence. A post that really did federate showing "not on the fediverse" because one HTTP call
// failed is worse than showing nothing at all.
export function reachFor ({ collection, rkey, apRkeys = null } = {}) {
  const out = [{ tier: TIER.FAVELA, state: 'observed' }]
  if (!collection) return out

  // bsky indexes app.bsky.* and silently ignores unknown lexicons — that IS the local-only mechanism
  // (scopes.js), so the collection is exactly the signal for whether the post was offered to bsky.
  const isPublic = scopeForCollection(collection) === 'public'
  if (isPublic) out.push({ tier: TIER.BSKY, state: 'eligible' })

  // Only claim the fediverse when the outbox names it AND the post is in a collection ap-bridge
  // actually projects. Unknown stays silent.
  //
  // ⚠ THE COLLECTION GATE IS LOAD-BEARING, not belt-and-braces. ap-bridge's object ids are keyed by
  // RKEY ALONE (`objectIdFor({host, rkey})`), and an rkey is unique only WITHIN a collection. So a
  // local-only post sharing an rkey with a public one would match the outbox set and render as
  // "published to the fediverse" — announcing that we federated a post whose whole purpose was NOT
  // being federated. Caught by test/reach.test.mjs; it is the same cross-collection collision
  // documented in spec/record-vocabulary.md §3.1.
  if (isPublic && apKnown(apRkeys) && rkey && apRkeys.has(rkey)) {
    out.push({ tier: TIER.FEDIVERSE, state: 'observed' })
  }
  return out
}

// True when the AP side told us anything at all. The caller uses this to distinguish "no fediverse
// row because it wasn't projected" from "no fediverse row because we couldn't ask".
export function apKnown (apRkeys) { return apRkeys instanceof Set }