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:
tomdebone
2026-07-05 20:01:31 +02:00
commit c586fd39c9
134 changed files with 35279 additions and 0 deletions
+105
View File
@@ -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(())
}