Files
maarcadetweet/crates/at-repo/src/rev.rs
T
tomdebone c586fd39c9 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.
2026-07-05 20:01:31 +02:00

176 lines
5.8 KiB
Rust

use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
pub const TID_BASE32: &[u8] = b"234567abcdefghijklmnopqrstuvwxyz";
/// Process-local monotonic counter used to disambiguate TIDs that would
/// otherwise collide on the same microsecond.
///
/// The wall clock gives us 13 base32 chars of timestamp (≈52 bits of
/// micros). Two writes in the same microsecond on the same PDS would
/// otherwise produce the identical TID, and `put_record` would silently
/// overwrite the prior record in the MST (same rkey, but actually
/// different content — the value CIDs differ but the MST key is the
/// TID, so the prior record becomes unreachable from the head commit).
///
/// We tack 12 low bits of a `fetch_add` counter into the encoded value
/// so back-to-back calls — even inside the same microsecond — always
/// yield different TIDs. The counter starts at 0; the first call's
/// fetch_add returns 0 and produces a TID encoding
/// `(now_micros << 12) | 0`. The counter is monotonic per process,
/// not globally — a process restart will reset it to 0, which means
/// a TID emitted by the new process may sort *before* a TID emitted
/// by its predecessor on the same wall-clock microsecond. That's
/// acceptable because TIDs are only used as MST rkeys within a
/// single repo's history; the protocol doesn't require cross-process
/// monotonicity.
static TID_COUNTER: AtomicU64 = AtomicU64::new(0);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Tid {
pub raw: String,
}
impl Tid {
pub fn new() -> Self {
Self {
raw: generate_tid(),
}
}
pub fn from_string(s: impl Into<String>) -> Self {
Self { raw: s.into() }
}
pub fn as_str(&self) -> &str {
&self.raw
}
}
impl Default for Tid {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for Tid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.raw)
}
}
pub fn generate_tid() -> String {
// Phase 5b H10 — the counter is 12 bits wide, so it would wrap after
// 4096 calls inside a single microsecond. To prevent that, we
// block until the wall clock advances whenever the low-12 counter
// has cycled back to 0 inside the same microsecond. In practice
// this never fires (4096 TIDs/µs ≈ 4 billion/sec from one process)
// but it's a cheap insurance policy.
let mut now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_micros() as u64;
let counter = loop {
let prev = TID_COUNTER.fetch_add(1, Ordering::Relaxed);
// The counter is reset to 0 at process start; the low 12 bits
// are an in-microsecond disambiguator. If we've wrapped back to
// 0 mid-microsecond, spin until the clock advances.
if (prev & 0xFFF) == 0 && prev != 0 {
let next = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_micros() as u64;
if next == now {
std::hint::spin_loop();
continue;
}
now = next;
}
break prev;
};
// Combine timestamp + 12-bit disambiguator. The wall clock fits
// comfortably in 52 bits, so the counter never spills into the
// timestamp portion for any realistic process lifetime (~year 2400).
let combined: u64 = (now << 12) | (counter & 0xFFF);
let mut s = String::with_capacity(13);
let mut n = combined;
for _ in 0..13 {
let idx = (n & 0x1F) as usize;
s.push(TID_BASE32[idx] as char);
n >>= 5;
}
s.chars().rev().collect()
}
pub fn compare_tid(a: &str, b: &str) -> std::cmp::Ordering {
a.cmp(b)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn tid_increases() {
let t1 = generate_tid();
std::thread::sleep(std::time::Duration::from_millis(2));
let t2 = generate_tid();
// Strict ordering: t2 must be greater than t1. Accepting `is_le`
// would mask the very bug this test exists to catch.
assert!(compare_tid(&t1, &t2).is_lt());
}
#[test]
fn tid_uses_lowercase_base32() {
let t = generate_tid();
for c in t.chars() {
assert!(matches!(c, '2'..='7' | 'a'..='z'));
}
}
/// Phase 5b H10 — two writes in the same microsecond used to
/// produce identical TIDs, which caused `put_record` to silently
/// overwrite the prior record (different value CID, but the same
/// rkey, so the new MST entry eclipsed the old). Verify a tight
/// burst of N calls yields N distinct TIDs.
#[test]
fn generate_tid_is_monotonic_per_process() {
let n = 1_000;
let mut seen = HashSet::with_capacity(n);
let mut prev: Option<String> = None;
for _ in 0..n {
let t = generate_tid();
assert!(
seen.insert(t.clone()),
"duplicate TID produced in tight loop: {t}"
);
if let Some(p) = prev.as_ref() {
assert!(
compare_tid(p, &t).is_lt(),
"TID must strictly increase per process: {p} >= {t}"
);
}
prev = Some(t);
}
assert_eq!(seen.len(), n);
}
/// Same as above but explicitly constructs the "same microsecond"
/// worst case by sampling TIDs back-to-back without sleeping. The
/// counter overlay must keep them distinct even when the wall
/// clock doesn't tick.
#[test]
fn generate_tid_avoids_same_microsecond_collisions() {
let n = 100;
let mut seen = HashSet::with_capacity(n);
for _ in 0..n {
let t = generate_tid();
assert!(seen.insert(t.clone()), "collision at {t}");
}
assert_eq!(seen.len(), n);
}
}