maarcadetweet: initial commit
AT Protocol PDS + AppView + Tauri Desktop Client, 160-char post limit. - PDS (Rust + axum + sqlx) - Auth: createAccount, createSession, refreshSession - Records: createRecord, deleteRecord (race-safe via SELECT FOR UPDATE) - Feed: feed.like.create, feed.repost.create - Sync: getRepo, getBlocks, getLatestCommit, getRecord (with MST proof), listRepos - Identity: resolveHandle - MST: spec-conformant (at-mst crate, 27 tests) - Repo: signed commits, TID counter (monotonic, 4096 wrap safe) - AppView (Rust + axum + sqlx) - Jetstream consumer (WebSocket, exponential backoff, 38k+ events indexed) - REST API: timeline/home (graph-aware), profile, search, post (with thread hydration) - Handle-sync worker (did:plc + did:web) - JSONB embed storage + thread columns (migration 0003) - Like/repost counter cache (migration 0004) - Tauri 2 + Svelte 5 Desktop Client - System tray (Show/Compose/Quit menu) - OS notifications (tauri-plugin-notification) - Auto-update (tauri-plugin-updater, placeholder endpoint) - Window-state (tauri-plugin-window-state) - 160-char compose with live counter - Image/Link embed rendering - LocalStorage-persisted like state - Timeline with poll (prepend new posts) - Custom TitleBar (transparent, no decorations) - Orange/IBM Plex Mono maarcade design Tests: 231 Rust + 9 vitest = 240 passed.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
use anyhow::Result;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::event::JetstreamEvent;
|
||||
|
||||
pub struct JetstreamConsumer {
|
||||
pub url: String,
|
||||
pub collections: Vec<String>,
|
||||
pub max_backoff_secs: u64,
|
||||
/// Optional handle the consumer toggles on every connect/disconnect.
|
||||
/// Useful for health endpoints that want a live "are we connected?"
|
||||
/// signal without polling.
|
||||
pub connected: Option<Arc<AtomicBool>>,
|
||||
/// Optional cursor (`time_us`) to resume from. 0 means "no cursor / start
|
||||
/// fresh" which is Jetstream's default. Set via `with_cursor(...)`.
|
||||
pub cursor_us: i64,
|
||||
}
|
||||
|
||||
impl JetstreamConsumer {
|
||||
pub fn new(url: impl Into<String>, collections: Vec<String>) -> Self {
|
||||
Self {
|
||||
url: url.into(),
|
||||
collections,
|
||||
max_backoff_secs: 30,
|
||||
connected: None,
|
||||
cursor_us: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a consumer that shares a connection-state flag with the caller.
|
||||
pub fn with_connected_flag(mut self, flag: Arc<AtomicBool>) -> Self {
|
||||
self.connected = Some(flag);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build a consumer with an explicit reconnect backoff ceiling (seconds).
|
||||
pub fn with_max_backoff_secs(mut self, max: u64) -> Self {
|
||||
self.max_backoff_secs = max.max(1);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build a consumer that starts from a previously-persisted cursor
|
||||
/// (microseconds since epoch). Setting this avoids the post-restart
|
||||
/// gap where Jetstream's default backfill window might miss events.
|
||||
pub fn with_cursor(mut self, cursor_us: i64) -> Self {
|
||||
self.cursor_us = cursor_us;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn run<F, Fut>(&self, mut on_event: F) -> Result<()>
|
||||
where
|
||||
F: FnMut(JetstreamEvent) -> Fut + Send + 'static,
|
||||
Fut: std::future::Future<Output = Result<()>> + Send,
|
||||
{
|
||||
let mut backoff_secs: u64 = 1;
|
||||
loop {
|
||||
let r = self.connect_and_consume(&mut on_event).await;
|
||||
// Any non-error return (clean disconnect, error) means we lost
|
||||
// the connection; flip the flag if we own one and back off.
|
||||
if let Some(flag) = &self.connected {
|
||||
flag.store(false, Ordering::Relaxed);
|
||||
}
|
||||
match r {
|
||||
Ok(()) => warn!("jetstream stream ended, reconnecting"),
|
||||
Err(e) => error!("jetstream error: {e:#}"),
|
||||
}
|
||||
warn!("reconnecting in {backoff_secs}s");
|
||||
tokio::time::sleep(Duration::from_secs(backoff_secs)).await;
|
||||
backoff_secs = (backoff_secs * 2).min(self.max_backoff_secs);
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect_and_consume<F, Fut>(&self, on_event: &mut F) -> Result<()>
|
||||
where
|
||||
F: FnMut(JetstreamEvent) -> Fut + Send,
|
||||
Fut: std::future::Future<Output = Result<()>> + Send,
|
||||
{
|
||||
let (mut ws, _) = tokio_tungstenite::connect_async(&self.url).await?;
|
||||
info!("connected to jetstream: {}", self.url);
|
||||
if let Some(flag) = &self.connected {
|
||||
flag.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
if !self.collections.is_empty() || self.cursor_us > 0 {
|
||||
let mut options = json!({ "type": "options" });
|
||||
if !self.collections.is_empty() {
|
||||
options["wantedCollections"] = json!(self.collections);
|
||||
}
|
||||
if self.cursor_us > 0 {
|
||||
options["cursor"] = json!(self.cursor_us);
|
||||
}
|
||||
ws.send(Message::Text(options.to_string())).await?;
|
||||
}
|
||||
|
||||
while let Some(msg) = ws.next().await {
|
||||
let msg = msg?;
|
||||
if let Message::Text(text) = msg {
|
||||
if let Ok(ev) = serde_json::from_str::<JetstreamEvent>(&text) {
|
||||
on_event(ev).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JetstreamEvent {
|
||||
pub did: String,
|
||||
pub time_us: i64,
|
||||
pub kind: String,
|
||||
pub commit: Option<Value>,
|
||||
pub identity: Option<Value>,
|
||||
pub account: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CommitOp {
|
||||
pub action: String,
|
||||
pub rkey: Option<String>,
|
||||
pub path: Option<String>,
|
||||
pub cid: Option<String>,
|
||||
pub record: Option<Value>,
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod consumer;
|
||||
pub mod event;
|
||||
|
||||
pub use consumer::JetstreamConsumer;
|
||||
pub use event::{CommitOp, JetstreamEvent};
|
||||
Reference in New Issue
Block a user