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,105 @@
|
||||
use anyhow::Result;
|
||||
use at_identity::DidHandleResolver;
|
||||
use at_shared::config::AppConfig;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::info;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
mod firehose;
|
||||
mod handle_sync;
|
||||
mod indexer;
|
||||
mod ingest;
|
||||
mod routes;
|
||||
mod state;
|
||||
|
||||
use state::AppState;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Install a rustls crypto provider before any TLS connection. `ring`
|
||||
// is the only one we currently support; using `aws_lc_rs` would
|
||||
// require a non-default feature on rustls.
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")))
|
||||
.init();
|
||||
|
||||
let cfg = AppConfig::from_env()?;
|
||||
let db = sqlx::PgPool::connect(&cfg.database_url_appview).await?;
|
||||
sqlx::migrate!("../../migrations/appview").run(&db).await?;
|
||||
|
||||
// Read the last persisted cursor so we resume after restart instead of
|
||||
// missing events that landed in the gap between (a) the last value we
|
||||
// wrote and (b) Jetstream's default backfill window.
|
||||
let start_cursor = indexer::cursor_get(&db).await.unwrap_or(0);
|
||||
if start_cursor > 0 {
|
||||
info!(cursor = start_cursor, "resuming Jetstream from last persisted cursor");
|
||||
}
|
||||
|
||||
// Bounded channel for "cursor wants to advance" signals. We push one
|
||||
// tick per ~100 events from the consumer thread; the flush task drains
|
||||
// the channel and writes a single batched UPDATE.
|
||||
let (cursor_tx, cursor_rx) = mpsc::channel::<i64>(32);
|
||||
|
||||
let stats = firehose::Stats::new();
|
||||
|
||||
// Spawn the cursor-flush task. It lives for the whole process — when
|
||||
// the receiver end drops (which only happens at shutdown), the task
|
||||
// does a final flush and exits.
|
||||
let _cursor_task = firehose::spawn_cursor_flush(db.clone(), cursor_rx, stats.clone());
|
||||
|
||||
{
|
||||
let mut jetstream = at_firehose::JetstreamConsumer::new(
|
||||
cfg.jetstream_url.clone(),
|
||||
cfg.jetstream_collections.clone(),
|
||||
)
|
||||
.with_connected_flag(stats.jetstream_connected_arc())
|
||||
.with_max_backoff_secs(30);
|
||||
if start_cursor > 0 {
|
||||
jetstream = jetstream.with_cursor(start_cursor);
|
||||
}
|
||||
let handler = firehose::IndexHandler::new(db.clone(), stats.clone(), cursor_tx.clone());
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = jetstream
|
||||
.run(move |ev| {
|
||||
let h = handler.clone();
|
||||
async move { h.handle(ev).await }
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::error!("jetstream terminated: {e:#}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let state = AppState::new(cfg.clone(), db.clone(), stats.clone());
|
||||
|
||||
// Back-fill the `handle` column on posts that the Jetstream
|
||||
// indexer inserted with an empty placeholder. The worker dispatches
|
||||
// by DID method: `did:plc:` → PLC directory, `did:web:` → a
|
||||
// WebResolver that fetches the host's `.well-known/did.json`.
|
||||
// Anything else (e.g. `did:key:`) is silently skipped.
|
||||
let plc: Arc<dyn DidHandleResolver> = Arc::new(at_identity::PlcClient::new(
|
||||
cfg.plc_directory_url.clone(),
|
||||
));
|
||||
let web: Arc<dyn DidHandleResolver> = Arc::new(at_identity::WebResolver::new());
|
||||
let handle_sync = handle_sync::HandleSyncWorker {
|
||||
db: db.clone(),
|
||||
plc_resolver: plc,
|
||||
web_resolver: web,
|
||||
interval_secs: cfg.appview_handle_sync_interval_secs,
|
||||
};
|
||||
tokio::spawn(async move {
|
||||
handle_sync.run_forever().await;
|
||||
});
|
||||
|
||||
let app = routes::router(state);
|
||||
let addr: SocketAddr = format!("{}:{}", cfg.appview_host, cfg.appview_port).parse()?;
|
||||
info!("appview listening on http://{addr}");
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
axum::serve(listener, app).await?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user