feat(appview): PDS-Firehose konsumieren

Gegenstück zum subscribeRepos-Endpoint: WebSocket-Consumer mit
persistiertem seq-Cursor, Reconnect-Backoff und Behandlung von
#info/OutdatedCursor.

Eigene Cursor-Tabelle statt einer Zeile in jetstream_cursor: dort steht ein
time_us in der Größenordnung 1.7e15, die seq ist ein kleiner Zähler ab 1.
Geteilt hätte GREATEST den PDS-Cursor sofort in eine Zukunft geschoben, die
die PDS nie erreicht.

Kein neuer Indexer-Pfad — jede Op wird in die Single-Op-Form übersetzt, die
apply_commit schon vom Jetstream kennt. Push und Firehose liefern denselben
Commit doppelt; das ist unkritisch, weil die Schreibpfade Upserts sind und
der Dedupe-Index der Notifications den Rest abfängt. Mit einem Test
festgehalten statt vorausgesetzt.

Der CAR-Reader ist neu (es gab nur einen Writer, und der liegt in einem
Binary-Crate ohne lib-Target). Der CBOR-Reader arbeitet mit explizitem
Offset, weil ein Frame zwei hintereinander geschriebene Werte sind, und
akzeptiert CID-Links in beiden Schreibweisen — die Blöcke tragen Strings.

/healthz meldet beide Ströme getrennt; sie fallen unabhängig voneinander
aus.

Verifiziert mit totem Push-Ziel: der Post kam trotzdem an.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013HC9HLrUU1LNwkzp8nkDLX
This commit is contained in:
tomdebone
2026-09-10 07:08:23 +02:00
co-authored by Claude Opus 5
parent d6947c2576
commit 124a90dc07
11 changed files with 2849 additions and 1 deletions
+25
View File
@@ -18,6 +18,21 @@ fn default_auth_required() -> bool {
true
}
/// Default for `PDS_FIREHOSE_ENABLED`.
///
/// `true` — the AppView consumes the local PDS's
/// `com.atproto.sync.subscribeRepos` stream. That stream is the only
/// *guaranteed* path for a local user's own records: the fast
/// `POST /internal/ingest-commit` push is best effort, and the public
/// Jetstream never sees this PDS, so a lost push means a permanently
/// missing post. Defaulting to on means an operator who never heard of
/// the variable gets the durable behaviour; switching it off is the
/// explicit choice (e.g. a PDS too old to serve the endpoint, or a
/// second AppView instance that should not double-index).
fn default_pds_firehose_enabled() -> bool {
true
}
#[derive(Debug, Clone, Deserialize)]
pub struct AppConfig {
pub pds_host: String,
@@ -78,6 +93,12 @@ pub struct AppConfig {
/// `APPVIEW_CORS_ORIGINS=tauri://localhost,http://127.0.0.1:1430`
#[serde(default)]
pub appview_cors_origins: Vec<String>,
/// Whether the AppView subscribes to the local PDS firehose
/// (`com.atproto.sync.subscribeRepos` on
/// [`AppConfig::pds_base_url`]). Default `true` — see
/// [`default_pds_firehose_enabled`] for why.
#[serde(default = "default_pds_firehose_enabled")]
pub pds_firehose_enabled: bool,
}
impl AppConfig {
@@ -122,6 +143,10 @@ impl AppConfig {
.ok()
.map(|s| parse_csv_env(&s))
.unwrap_or_default(),
pds_firehose_enabled: std::env::var("PDS_FIREHOSE_ENABLED")
.ok()
.map(|s| parse_bool_env(&s))
.unwrap_or_else(default_pds_firehose_enabled),
})
}