Skip to content

Replace appview reads with Constellation and direct PDS hydration - #75

Merged
disnet merged 7 commits into
mainfrom
radial/impl-13bf7bc9a404
Oct 13, 2026
Merged

disnet merged 7 commits into
mainfrom
radial/impl-13bf7bc9a404

Conversation

@disnet

@disnet disnet commented Aug 24, 2026 •

Copy link
Copy Markdown
Owner

Replaces Skyboard's custom appview read path with Constellation discovery, direct PDS hydration, and browser-side Jetstream updates.

Radial artifact

Delivered

  • Web board loads use Constellation plus participant PDSes; Jetstream applies live record deltas directly to Dexie.
  • Snapshot pruning protects writes created during in-flight loads, owner collections are hydrated despite empty index responses, and PDS cache expiry is consistent.
  • CLI reads use Constellation plus direct PDS reads, removing the final active appview dependency.
  • CLI snapshot reads fail explicitly when Constellation discovery, participant PDS resolution, or any PDS pagination request is incomplete, preventing read and mutation commands from acting on plausible partial state.
  • CLI board-record reads distinguish a missing board from an unreadable one: getRecord returns null only when the PDS reports the record, its repo, or the owning identity as missing, and raises BoardReadError for resolution failures, transport errors, non-success responses, unreadable bodies, and records that fail validation. sb prints those errors as one red line and exits 1.
  • Documentation retains the legacy appview only for the rollout soak.

Verification

  • npm --prefix cli test (84 tests passed; 15 new regression tests for owner-board 404 vs 503 vs throw, unresolvable and nonexistent identities, malformed responses, invalid board records)
  • npm --prefix cli run build
  • npm run check (0 errors; 22 pre-existing warnings remain)
  • npm run build
  • Live read against a public board still returns 14 tasks / 1 trust / 37 comments / 2 participants; a missing rkey, an unknown identity, and an impossible record key all report the board as absent, while an unresolvable DID method fails descriptively.
  • Touched files pass Prettier and git diff --check

Remaining risk

The two-browser live-edit and logged-out public-viewer UI flows still require a manual browser pass before deployment. Keep the legacy Fly appview alive during the rollout soak for already-open old web bundles; decommissioning remains a separate operational step.

The web client still collapses "board record unreadable" into "Board not found" on the board page — pre-existing appview-era behaviour, not covered by this change.

The repository-wide formatting check scans ignored cli/dist output after the CLI build and reports those generated files; the changed source and test files are clean.

claudebot.disnetdev.com (did:plc:n6ku5xddiuguwze3f356evla) and others added 2 commits August 24, 2026 22:37
The web app no longer talks to the Skyboard appview. Board state is
assembled in the browser from public atproto infrastructure:

- Constellation (https://constellation.microcosm.blue) indexes every
  dev.skyboard.* record by its .boardUri field, so one target URI reveals
  which collections are non-empty and who participates in a board.
- Each participant's PDS stays the source of truth for record content,
  read via com.atproto.repo.getRecord / listRecords.
- Jetstream delivers live commits directly to the browser. Commit events
  carry the full record, so changes apply straight to Dexie instead of
  triggering a whole-board refetch.

Writes are unchanged — clients already only wrote to their own PDS.

src/lib/board-store.ts centralises the Dexie write rules both read paths
share (local pending wins, unchanged rows aren't rewritten, one
transaction per snapshot). It also prunes records that vanished upstream,
scoped to the (did, collection) pairs that were listed successfully, so
an unreachable PDS can't cause data loss. The appview never pruned, so
other users' deletes used to linger locally forever.

Records now arrive from arbitrary PDSes rather than through the appview,
so src/lib/record-schemas.ts shape-checks them, mirroring the appview's
zod schemas without adding zod to the browser bundle.

Degradation is bounded: if Constellation is unreachable, discovery falls
back to the board owner plus participants already known locally, and
boards already in Dexie keep rendering and syncing.

The appview server is left in place — cli/ still reads from it.

Co-Authored-By: claudebot.disnetdev.com (did:plc:n6ku5xddiuguwze3f356evla) <claudebot.disnetdev.com@noreply.radial>
Co-Authored-By: codexbot.disnetdev.com (did:plc:hbonvqr5ysrscg5wdyb5klie) <codexbot.disnetdev.com@noreply.radial>
@disnet disnet changed the title Replace the appview with Constellation + direct PDS/Jetstream reads Replace appview reads with Constellation and direct PDS hydration Aug 24, 2026
codexbot.disnetdev.com (did:plc:hbonvqr5ysrscg5wdyb5klie) and others added 5 commits August 24, 2026 23:06
Co-Authored-By: codexbot.disnetdev.com (did:plc:hbonvqr5ysrscg5wdyb5klie) <codexbot.disnetdev.com@noreply.radial>
CLI board reads reported every failure as "Board not found." A 5xx from
the owner's PDS, a transport error, a plc.directory outage, or a
malformed response all collapsed to null in getRecord and again in
fetchBoardFromPds, so fetchBoardData returned before its BoardReadError
handling and mutating commands could not tell absence from a degraded
read.

getRecord now returns null only when the PDS reports the record, its
repo, or the owning identity as missing (400 RecordNotFound/RepoNotFound,
the reference PDS's "Could not find repo" InvalidRequest, 404, or a
404/410 from the DID directory). Resolution failures, transport errors,
non-success responses, unreadable JSON, and responses without a record
raise a descriptive BoardReadError, as does a board record that fails
schema validation. resolvePds returns a three-way result so an identity
that does not exist stays distinct from one that could not be looked up.

Syntactically impossible record keys are answered locally, since
getRecord reports them with the same generic InvalidRequest as a broken
request and ralph's parseBoardRef probes with arbitrary strings.

fetchBoardData no longer swallows unexpected errors into a null return,
and sb now prints BoardReadError messages as a single red line with exit
status 1 instead of an unhandled rejection.

Adds 15 regression tests covering owner-board 404 vs 503 vs throw,
unresolvable and nonexistent identities, malformed responses, and
invalid board records.

Co-Authored-By: claudebot.disnetdev.com (did:plc:n6ku5xddiuguwze3f356evla) <claudebot.disnetdev.com@noreply.radial>
Web client:
- Track live Jetstream writes per record while a snapshot is in flight, so
  a snapshot whose listings predate an event no longer overwrites it,
  re-adds a deleted record, or prunes a freshly created one.
- Apply Jetstream events strictly in order through a queue.
- Connect Jetstream before the login catch-up and wait for the socket to
  open; if it opens late, reload all boards to cover the gap.
- Follow the Dexie board list and add watchBoard() so boards created,
  joined or opened after login get live events.
- Share participant listings across boards loaded together, taking every
  board's prune footprint before any listing starts.
- Re-resolve and retry once when a read against a cached PDS endpoint
  fails, so a migrated account doesn't stay unreadable for a day.
- Never return a partial Constellation DID list; retry a failed page once.
- Hand throttle slots directly to waiters so the concurrency cap holds.
- Percent-decode did:web segments (ports).

CLI:
- Skip untrusted participants whose repos cannot be read, with a warning,
  instead of failing the whole board. The owner, the current user and
  owner-trusted DIDs must still read completely.
- Percent-decode did:web segments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXc6YqRZ9fnPhWwkiD7DFi
Reads no longer list participants' whole repos. Both the web loader and
the CLI ask Constellation's /links for every record on a board and read
each with getRecord, so a load costs what the board holds rather than
what its participants have written elsewhere. The web loader also skips
refetching ops, trusts, approvals and reactions it already holds as
synced, since Skyboard never rewrites those in place.

Web:
- Prune only rows the network confirmed gone (RecordNotFound, moved to
  another board, or absent from a successful fallback listing). Local
  rows Constellation no longer links are checked with getRecord, so
  index lag can't prune a live record.
- Fall back to listing owner + known DIDs only when Constellation is
  unreachable or reports no links at all (an unindexed new board).
- Return false for a board record that fails validation instead of
  reporting success with nothing to show.
- Tell missing from failed PDS reads; only failures and missing repos
  evict the cached endpoint, and never within a minute of resolving it.
- Watch exactly the boards in Dexie plus those being loaded, so leaving
  or deleting a board stops its events (they used to resurrect it).
- Queue a follow-up resync when one is requested mid-resync instead of
  dropping it.
- After a tab was hidden, reopen the socket from the last cursor instead
  of reloading every board.
- Replay the first connection from a minute before it was requested, so
  loads needn't wait for it and a slow open no longer triggers a second
  full load; catch-up skips boards discovery just loaded, and discovery
  watches the boards it loads.
- Debounce notification scans off the Jetstream event queue.

CLI:
- Classify records by the collection they were linked from, not $type.
- Share in-flight DID resolutions across parallel reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QwVmcmFKxaooAybXQWa7Z
- Only treat a 404 as a missing record when it carries an XRPC error body.
  A bare 404 from a stale endpoint or proxy is now a failed read: the web
  client re-resolves the PDS and never prunes on it, and the CLI raises
  BoardReadError instead of reporting the record as absent.
- Count every local Dexie delete as a touch for open snapshots, so a board
  load that was in flight when the user deleted a row can't re-add it.
- In the CLI, require full reads from any participant with tasks on the
  board. Their tasks and own ops are materialized whether or not they're
  trusted, so skipping them hid cards and skewed `sb mv` positions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GHoHenjVLSWUDFHPQaN8cJ
@disnet
disnet merged commit f40f566 into main Oct 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants