v0.4.3: Lock contention overhaul, StoragePool, mobile bottom nav, text scaling

Eliminate all conn_mgr lock holds during network I/O across 14 actor commands
and bi-stream handlers. PostFetch, TcpPunch, PullFromPeer, FetchEngagement,
ResolveAddress, AnchorProbe use brief locks for data gathering only. WormLookup,
ContentSearch, WormQuery use connection snapshots for lock-free cascade fan-out.
RelayIntroduce extracts forwarding data under brief lock, does I/O outside.
BlobRequest, PostFetchRequest, ManifestRefresh use Arc clones instead of conn_mgr
lock. ConnectionActor hoists shared Arcs (storage, blob_store, endpoint) for
lock-free access. ResolveAddress adds 5s per-query timeout (was unbounded).

Initial exchange failure now aborts mesh upgrade (was silently continuing with
broken connection). connect_to_peer/connect_to_anchor use consistent 15s timeout.
Rebalance connects outside the lock via pending_connects pattern.

StoragePool: 8 concurrent SQLite connections in WAL mode replace single
Mutex<Storage>. Reads run fully parallel; writes serialize at SQLite level only.
PRAGMA busy_timeout=5000 for graceful write contention.

Mobile bottom nav bar (<=768px) with icon tabs. Text sizes: XS/S/M/L/XL
(75%/100%/125%/150%/200%), default M. localStorage persistence for instant
restore. Toast repositioned above mobile nav.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-03-22 21:35:38 -04:00
parent f17535d61d
commit 43adbbdf7d
15 changed files with 1546 additions and 618 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "itsgoin-desktop"
version = "0.4.2"
version = "0.4.3"
edition = "2021"
[lib]

View file

@ -198,7 +198,7 @@ async fn post_to_dto(
// Resolve intent kind from storage
let intent_kind = {
let storage = node.storage.lock().await;
let storage = node.storage.get().await;
match storage.get_post_intent(id) {
Ok(Some(intent)) => match intent {
VisibilityIntent::Public => "public".to_string(),
@ -237,14 +237,14 @@ async fn post_to_dto(
.collect();
// Engagement data
let reaction_counts = {
let storage = node.storage.lock().await;
let storage = node.storage.get().await;
storage.get_reaction_counts(id, &node.node_id).unwrap_or_default()
.into_iter()
.map(|(emoji, count, reacted_by_me)| ReactionCountDto { emoji, count, reacted_by_me })
.collect()
};
let comment_count = {
let storage = node.storage.lock().await;
let storage = node.storage.get().await;
storage.get_comment_count(id).unwrap_or(0)
};
@ -286,7 +286,7 @@ async fn decrypt_just_created(
}
PostVisibility::GroupEncrypted { group_id, epoch, wrapped_cek } => {
let seed_info = {
let storage = node.storage.lock().await;
let storage = node.storage.get().await;
storage.get_all_group_seeds_map().ok()
.and_then(|map| map.get(&(*group_id, *epoch)).copied())
};
@ -319,7 +319,7 @@ async fn get_node_info(state: State<'_, AppState>) -> Result<NodeInfoDto, String
// Prefer external address (UPnP, public IPv6, observed) over local bind address
let external_addr = node.network.http_addr();
let observed_addr = if external_addr.is_none() {
let storage = node.storage.lock().await;
let storage = node.storage.get().await;
storage.get_peer_record(&node.node_id).ok().flatten()
.and_then(|r| r.addresses.first().map(|a| a.to_string()))
} else {
@ -475,7 +475,7 @@ async fn get_blob_path(
if let Some(ref pid_hex) = post_id_hex {
if let Ok(pid_bytes) = hex::decode(pid_hex) {
if let Ok(post_id) = <[u8; 32]>::try_from(pid_bytes.as_slice()) {
let storage = node.storage.lock().await;
let storage = node.storage.get().await;
if let Ok(Some((_post, vis))) = storage.get_post_with_visibility(&post_id) {
if !matches!(vis, PostVisibility::Public) {
return Ok(None);
@ -524,7 +524,7 @@ async fn resolve_blob_data(
// Try fetching from network if post_id provided
if let Some(pid) = post_id {
let post = {
let storage = node.storage.lock().await;
let storage = node.storage.get().await;
storage.get_post(&pid).map_err(|e| e.to_string())?
};
if let Some(post) = post {
@ -665,7 +665,7 @@ async fn connect_peer(
// Store peer with addresses
let ip_addrs: Vec<_> = addr.ip_addrs().copied().collect();
{
let storage = node.storage.lock().await;
let storage = node.storage.get().await;
if ip_addrs.is_empty() {
storage.add_peer(&nid).map_err(|e| e.to_string())?;
} else {
@ -720,7 +720,7 @@ async fn list_follows(state: State<'_, AppState>) -> Result<Vec<PeerDto>, String
_ => None,
};
// Try to get peer record for address info
let storage = node.storage.lock().await;
let storage = node.storage.get().await;
let rec = storage.get_peer_record(nid).ok().flatten();
drop(storage);
let is_online = node.network.is_connected(nid).await
@ -766,7 +766,7 @@ async fn list_peers(state: State<'_, AppState>) -> Result<Vec<PeerDto>, String>
.map(|(nid, _, _)| nid)
.collect();
let (social_ids, n2_ids, n3_ids) = {
let storage = node.storage.lock().await;
let storage = node.storage.get().await;
let social: std::collections::HashSet<_> = storage
.list_social_routes()
.unwrap_or_default()
@ -994,7 +994,7 @@ async fn set_anchors(
#[tauri::command]
async fn list_anchor_peers(state: State<'_, AppState>) -> Result<Vec<PeerDto>, String> {
let node = state.inner();
let storage = node.storage.lock().await;
let storage = node.storage.get().await;
let records = storage.list_anchor_peers().map_err(|e| e.to_string())?;
drop(storage);
let mut dtos = Vec::with_capacity(records.len());
@ -1026,7 +1026,7 @@ struct KnownAnchorDto {
#[tauri::command]
async fn list_known_anchors(state: State<'_, AppState>) -> Result<Vec<KnownAnchorDto>, String> {
let node = state.inner();
let storage = node.storage.lock().await;
let storage = node.storage.get().await;
let anchors = storage.list_known_anchors().map_err(|e| e.to_string())?;
drop(storage);
let mut dtos = Vec::with_capacity(anchors.len());
@ -1444,7 +1444,7 @@ async fn get_badge_counts(
last_feed_view_ms: u64,
) -> Result<BadgeCountsDto, String> {
let node = state.inner();
let storage = node.storage.lock().await;
let storage = node.storage.get().await;
// Feed badge: count non-DM posts from others newer than last_feed_view_ms
let feed_posts = storage.get_feed().map_err(|e| e.to_string())?;
@ -1588,7 +1588,7 @@ async fn get_network_summary(state: State<'_, AppState>) -> Result<NetworkSummar
}
}
let (n2, n3) = {
let storage = node.storage.lock().await;
let storage = node.storage.get().await;
let n2 = storage.count_distinct_n2().unwrap_or(0);
let n3 = storage.count_distinct_n3().unwrap_or(0);
(n2, n3)
@ -1664,7 +1664,7 @@ async fn request_referrals(state: State<'_, AppState>) -> Result<String, String>
// Try known_anchors table first (populated by anchor register cycle),
// fall back to anchor peers from the peers table (is_anchor = true)
let anchors: Vec<(NodeId, Vec<std::net::SocketAddr>)> = {
let storage = node.storage.lock().await;
let storage = node.storage.get().await;
let known = storage.list_known_anchors().unwrap_or_default();
if !known.is_empty() {
known
@ -2096,7 +2096,7 @@ pub fn run() {
// Start blob eviction cycle (every 5 min)
let cache_max_bytes: u64 = {
let storage = n.storage.lock().await;
let storage = n.storage.get().await;
storage.get_setting("cache_size_bytes")
.ok()
.flatten()

View file

@ -1,6 +1,6 @@
{
"productName": "itsgoin",
"version": "0.4.2",
"version": "0.4.3",
"identifier": "com.itsgoin.app",
"build": {
"frontendDist": "../../frontend",