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:
parent
f17535d61d
commit
43adbbdf7d
15 changed files with 1546 additions and 618 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -12,7 +12,7 @@ use tokio::sync::Mutex;
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
|
|
||||||
use crate::blob::BlobStore;
|
use crate::blob::BlobStore;
|
||||||
use crate::storage::Storage;
|
use crate::storage::{Storage, StoragePool};
|
||||||
use crate::types::PostVisibility;
|
use crate::types::PostVisibility;
|
||||||
|
|
||||||
/// Connection budget: 5 content slots, 15 redirect slots, 1 per IP.
|
/// Connection budget: 5 content slots, 15 redirect slots, 1 per IP.
|
||||||
|
|
@ -104,7 +104,7 @@ impl HttpBudget {
|
||||||
/// Run the HTTP server on the given port. Blocks forever.
|
/// Run the HTTP server on the given port. Blocks forever.
|
||||||
pub async fn run_http_server(
|
pub async fn run_http_server(
|
||||||
port: u16,
|
port: u16,
|
||||||
storage: Arc<Mutex<Storage>>,
|
storage: Arc<StoragePool>,
|
||||||
blob_store: Arc<BlobStore>,
|
blob_store: Arc<BlobStore>,
|
||||||
downstream_addrs: Arc<Mutex<HashMap<[u8; 32], Vec<SocketAddr>>>>,
|
downstream_addrs: Arc<Mutex<HashMap<[u8; 32], Vec<SocketAddr>>>>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
|
|
@ -180,7 +180,7 @@ async fn handle_connection(
|
||||||
mut stream: TcpStream,
|
mut stream: TcpStream,
|
||||||
_ip: IpAddr,
|
_ip: IpAddr,
|
||||||
slot: SlotKind,
|
slot: SlotKind,
|
||||||
storage: &Arc<Mutex<Storage>>,
|
storage: &Arc<StoragePool>,
|
||||||
blob_store: &Arc<BlobStore>,
|
blob_store: &Arc<BlobStore>,
|
||||||
downstream_addrs: &Arc<Mutex<HashMap<[u8; 32], Vec<SocketAddr>>>>,
|
downstream_addrs: &Arc<Mutex<HashMap<[u8; 32], Vec<SocketAddr>>>>,
|
||||||
) {
|
) {
|
||||||
|
|
@ -281,12 +281,12 @@ fn validate_hex64(s: &str) -> Option<[u8; 32]> {
|
||||||
async fn serve_post(
|
async fn serve_post(
|
||||||
stream: &mut TcpStream,
|
stream: &mut TcpStream,
|
||||||
post_id: &[u8; 32],
|
post_id: &[u8; 32],
|
||||||
storage: &Arc<Mutex<Storage>>,
|
storage: &Arc<StoragePool>,
|
||||||
blob_store: &Arc<BlobStore>,
|
blob_store: &Arc<BlobStore>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
// Look up post + visibility
|
// Look up post + visibility
|
||||||
let result = {
|
let result = {
|
||||||
let store = storage.lock().await;
|
let store = storage.get().await;
|
||||||
store.get_post_with_visibility(post_id)
|
store.get_post_with_visibility(post_id)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -301,7 +301,7 @@ async fn serve_post(
|
||||||
|
|
||||||
// Look up author name
|
// Look up author name
|
||||||
let author_name = {
|
let author_name = {
|
||||||
let store = storage.lock().await;
|
let store = storage.get().await;
|
||||||
store
|
store
|
||||||
.get_profile(&post.author)
|
.get_profile(&post.author)
|
||||||
.ok()
|
.ok()
|
||||||
|
|
@ -321,12 +321,12 @@ async fn serve_post(
|
||||||
async fn serve_blob(
|
async fn serve_blob(
|
||||||
stream: &mut TcpStream,
|
stream: &mut TcpStream,
|
||||||
blob_id: &[u8; 32],
|
blob_id: &[u8; 32],
|
||||||
storage: &Arc<Mutex<Storage>>,
|
storage: &Arc<StoragePool>,
|
||||||
blob_store: &Arc<BlobStore>,
|
blob_store: &Arc<BlobStore>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
// Verify this blob belongs to a public post
|
// Verify this blob belongs to a public post
|
||||||
let (mime_type, _post_id) = {
|
let (mime_type, _post_id) = {
|
||||||
let store = storage.lock().await;
|
let store = storage.get().await;
|
||||||
match find_public_blob_info(&store, blob_id) {
|
match find_public_blob_info(&store, blob_id) {
|
||||||
Some(info) => info,
|
Some(info) => info,
|
||||||
None => return false, // not found or not public — hard close
|
None => return false, // not found or not public — hard close
|
||||||
|
|
@ -367,12 +367,12 @@ fn find_public_blob_info(store: &Storage, blob_id: &[u8; 32]) -> Option<(String,
|
||||||
async fn try_redirect(
|
async fn try_redirect(
|
||||||
stream: &mut TcpStream,
|
stream: &mut TcpStream,
|
||||||
post_id: &[u8; 32],
|
post_id: &[u8; 32],
|
||||||
storage: &Arc<Mutex<Storage>>,
|
storage: &Arc<StoragePool>,
|
||||||
_downstream_addrs: &Arc<Mutex<HashMap<[u8; 32], Vec<SocketAddr>>>>,
|
_downstream_addrs: &Arc<Mutex<HashMap<[u8; 32], Vec<SocketAddr>>>>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
// Get downstream peers for this post
|
// Get downstream peers for this post
|
||||||
let downstream_peers = {
|
let downstream_peers = {
|
||||||
let store = storage.lock().await;
|
let store = storage.get().await;
|
||||||
// Verify post exists and is public first
|
// Verify post exists and is public first
|
||||||
match store.get_post_with_visibility(post_id) {
|
match store.get_post_with_visibility(post_id) {
|
||||||
Ok(Some((_, PostVisibility::Public))) => {}
|
Ok(Some((_, PostVisibility::Public))) => {}
|
||||||
|
|
@ -383,7 +383,7 @@ async fn try_redirect(
|
||||||
|
|
||||||
// Get addresses for downstream peers
|
// Get addresses for downstream peers
|
||||||
let candidates: Vec<SocketAddr> = {
|
let candidates: Vec<SocketAddr> = {
|
||||||
let store = storage.lock().await;
|
let store = storage.get().await;
|
||||||
let mut addrs = Vec::new();
|
let mut addrs = Vec::new();
|
||||||
for peer_id in &downstream_peers {
|
for peer_id in &downstream_peers {
|
||||||
if let Ok(Some(peer)) = store.get_peer_record(peer_id) {
|
if let Ok(Some(peer)) = store.get_peer_record(peer_id) {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ use crate::protocol::{
|
||||||
PullSyncRequestPayload, PullSyncResponsePayload, RefuseRedirectPayload,
|
PullSyncRequestPayload, PullSyncResponsePayload, RefuseRedirectPayload,
|
||||||
SocialAddressUpdatePayload, SocialDisconnectNoticePayload, SyncPost, ALPN_V2,
|
SocialAddressUpdatePayload, SocialDisconnectNoticePayload, SyncPost, ALPN_V2,
|
||||||
};
|
};
|
||||||
use crate::storage::Storage;
|
use crate::storage::{Storage, StoragePool};
|
||||||
use crate::types::{
|
use crate::types::{
|
||||||
DeleteRecord, DeviceProfile, DeviceRole, NodeId, PeerSlotKind, PeerWithAddress, Post, PostId,
|
DeleteRecord, DeviceProfile, DeviceRole, NodeId, PeerSlotKind, PeerWithAddress, Post, PostId,
|
||||||
PostVisibility, PublicProfile, SessionReachMethod, WormResult,
|
PostVisibility, PublicProfile, SessionReachMethod, WormResult,
|
||||||
|
|
@ -27,7 +27,7 @@ use crate::types::{
|
||||||
/// The network layer: manages the iroh endpoint and mesh connections
|
/// The network layer: manages the iroh endpoint and mesh connections
|
||||||
pub struct Network {
|
pub struct Network {
|
||||||
endpoint: iroh::Endpoint,
|
endpoint: iroh::Endpoint,
|
||||||
storage: Arc<Mutex<Storage>>,
|
storage: Arc<StoragePool>,
|
||||||
our_node_id: NodeId,
|
our_node_id: NodeId,
|
||||||
is_anchor: Arc<AtomicBool>,
|
is_anchor: Arc<AtomicBool>,
|
||||||
conn_mgr: Arc<Mutex<ConnectionManager>>,
|
conn_mgr: Arc<Mutex<ConnectionManager>>,
|
||||||
|
|
@ -79,7 +79,7 @@ pub(crate) fn is_publicly_routable(addr: &SocketAddr) -> bool {
|
||||||
impl Network {
|
impl Network {
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
secret_key: iroh::SecretKey,
|
secret_key: iroh::SecretKey,
|
||||||
storage: Arc<Mutex<Storage>>,
|
storage: Arc<StoragePool>,
|
||||||
bind_addr: Option<SocketAddr>,
|
bind_addr: Option<SocketAddr>,
|
||||||
secret_seed: [u8; 32],
|
secret_seed: [u8; 32],
|
||||||
blob_store: Arc<BlobStore>,
|
blob_store: Arc<BlobStore>,
|
||||||
|
|
@ -469,7 +469,7 @@ impl Network {
|
||||||
|
|
||||||
// Store peer with their address
|
// Store peer with their address
|
||||||
{
|
{
|
||||||
let storage = this.storage.lock().await;
|
let storage = this.storage.get().await;
|
||||||
let _ = storage.upsert_peer(&remote_node_id, &[remote_sock], None);
|
let _ = storage.upsert_peer(&remote_node_id, &[remote_sock], None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -512,7 +512,7 @@ impl Network {
|
||||||
async fn handle_incoming_connection(
|
async fn handle_incoming_connection(
|
||||||
conn_mgr: Arc<Mutex<ConnectionManager>>,
|
conn_mgr: Arc<Mutex<ConnectionManager>>,
|
||||||
conn_handle: ConnHandle,
|
conn_handle: ConnHandle,
|
||||||
storage: Arc<Mutex<Storage>>,
|
storage: Arc<StoragePool>,
|
||||||
conn: iroh::endpoint::Connection,
|
conn: iroh::endpoint::Connection,
|
||||||
remote_node_id: NodeId,
|
remote_node_id: NodeId,
|
||||||
remote_sock: SocketAddr,
|
remote_sock: SocketAddr,
|
||||||
|
|
@ -608,7 +608,7 @@ impl Network {
|
||||||
/// Uses ConnHandle for all state access — no direct conn_mgr lock.
|
/// Uses ConnHandle for all state access — no direct conn_mgr lock.
|
||||||
async fn try_mesh_upgrade(
|
async fn try_mesh_upgrade(
|
||||||
conn_handle: &ConnHandle,
|
conn_handle: &ConnHandle,
|
||||||
storage: &Arc<Mutex<Storage>>,
|
storage: &Arc<StoragePool>,
|
||||||
conn: &iroh::endpoint::Connection,
|
conn: &iroh::endpoint::Connection,
|
||||||
remote_node_id: NodeId,
|
remote_node_id: NodeId,
|
||||||
remote_sock: SocketAddr,
|
remote_sock: SocketAddr,
|
||||||
|
|
@ -638,7 +638,7 @@ impl Network {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let s = storage.lock().await;
|
let s = storage.get().await;
|
||||||
let _ = s.upsert_peer(&remote_node_id, &[remote_sock], None);
|
let _ = s.upsert_peer(&remote_node_id, &[remote_sock], None);
|
||||||
let _ = s.add_mesh_peer(&remote_node_id, PeerSlotKind::Local, 0);
|
let _ = s.add_mesh_peer(&remote_node_id, PeerSlotKind::Local, 0);
|
||||||
if s.has_social_route(&remote_node_id).unwrap_or(false) {
|
if s.has_social_route(&remote_node_id).unwrap_or(false) {
|
||||||
|
|
@ -663,6 +663,7 @@ impl Network {
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(peer = hex::encode(remote_node_id), error = ?e, "Initial exchange failed");
|
error!(peer = hex::encode(remote_node_id), error = ?e, "Initial exchange failed");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -692,13 +693,12 @@ impl Network {
|
||||||
// Store addresses so they're available during initial exchange
|
// Store addresses so they're available during initial exchange
|
||||||
let addrs: Vec<std::net::SocketAddr> = addr.ip_addrs().copied().collect();
|
let addrs: Vec<std::net::SocketAddr> = addr.ip_addrs().copied().collect();
|
||||||
if !addrs.is_empty() {
|
if !addrs.is_empty() {
|
||||||
let storage = self.storage.lock().await;
|
let storage = self.storage.get().await;
|
||||||
let _ = storage.upsert_peer(&peer_id, &addrs, None);
|
let _ = storage.upsert_peer(&peer_id, &addrs, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
// QUIC connect OUTSIDE the conn_mgr lock — this can take 60+ seconds
|
// QUIC connect OUTSIDE the conn_mgr lock with 15s timeout
|
||||||
// on unreachable peers and must not block other tasks
|
let conn = ConnectionManager::connect_to_unlocked(&self.endpoint, addr).await?;
|
||||||
let conn = self.endpoint.connect(addr, ALPN_V2).await?;
|
|
||||||
|
|
||||||
// Register the established connection
|
// Register the established connection
|
||||||
self.conn_handle.register_connection(peer_id, conn.clone(), addrs, PeerSlotKind::Local).await;
|
self.conn_handle.register_connection(peer_id, conn.clone(), addrs, PeerSlotKind::Local).await;
|
||||||
|
|
@ -734,7 +734,7 @@ impl Network {
|
||||||
let addrs: Vec<SocketAddr> = redir.a.iter()
|
let addrs: Vec<SocketAddr> = redir.a.iter()
|
||||||
.filter_map(|a| a.parse::<SocketAddr>().ok())
|
.filter_map(|a| a.parse::<SocketAddr>().ok())
|
||||||
.collect();
|
.collect();
|
||||||
let s = self.storage.lock().await;
|
let s = self.storage.get().await;
|
||||||
let _ = s.upsert_peer(&redir_id, &addrs, None);
|
let _ = s.upsert_peer(&redir_id, &addrs, None);
|
||||||
drop(s);
|
drop(s);
|
||||||
self.conn_handle.notify_growth();
|
self.conn_handle.notify_growth();
|
||||||
|
|
@ -837,7 +837,7 @@ impl Network {
|
||||||
// Build full state: all current N1 and N2 as "added", nothing removed
|
// Build full state: all current N1 and N2 as "added", nothing removed
|
||||||
let all_n1 = self.conn_handle.connected_peers().await;
|
let all_n1 = self.conn_handle.connected_peers().await;
|
||||||
let all_n2 = {
|
let all_n2 = {
|
||||||
let storage = self.storage.lock().await;
|
let storage = self.storage.get().await;
|
||||||
storage.build_n2_share().unwrap_or_default()
|
storage.build_n2_share().unwrap_or_default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -891,7 +891,7 @@ impl Network {
|
||||||
}
|
}
|
||||||
PostVisibility::GroupEncrypted { group_id, .. } => {
|
PostVisibility::GroupEncrypted { group_id, .. } => {
|
||||||
// Push to all group members
|
// Push to all group members
|
||||||
match self.storage.lock().await.get_all_group_members() {
|
match self.storage.get().await.get_all_group_members() {
|
||||||
Ok(map) => map.get(group_id).cloned().unwrap_or_default().into_iter().collect(),
|
Ok(map) => map.get(group_id).cloned().unwrap_or_default().into_iter().collect(),
|
||||||
Err(_) => return 0,
|
Err(_) => return 0,
|
||||||
}
|
}
|
||||||
|
|
@ -1041,7 +1041,7 @@ impl Network {
|
||||||
manifest: &crate::types::CdnManifest,
|
manifest: &crate::types::CdnManifest,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
let downstream = {
|
let downstream = {
|
||||||
let storage = self.storage.lock().await;
|
let storage = self.storage.get().await;
|
||||||
storage.get_blob_downstream(cid).unwrap_or_default()
|
storage.get_blob_downstream(cid).unwrap_or_default()
|
||||||
};
|
};
|
||||||
let payload = crate::protocol::ManifestPushPayload {
|
let payload = crate::protocol::ManifestPushPayload {
|
||||||
|
|
@ -1156,7 +1156,7 @@ impl Network {
|
||||||
}
|
}
|
||||||
|
|
||||||
let audience_members: Vec<NodeId> = {
|
let audience_members: Vec<NodeId> = {
|
||||||
match self.storage.lock().await.list_audience_members() {
|
match self.storage.get().await.list_audience_members() {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(_) => return 0,
|
Err(_) => return 0,
|
||||||
}
|
}
|
||||||
|
|
@ -1318,7 +1318,7 @@ impl Network {
|
||||||
Ok(()) => Ok(()),
|
Ok(()) => Ok(()),
|
||||||
Err(e) if e.to_string().contains("mesh refused") => {
|
Err(e) if e.to_string().contains("mesh refused") => {
|
||||||
// Anchor refused mesh — reconnect as session for registration
|
// Anchor refused mesh — reconnect as session for registration
|
||||||
let conn = self.endpoint.connect(addr, ALPN_V2).await?;
|
let conn = ConnectionManager::connect_to_unlocked(&self.endpoint, addr).await?;
|
||||||
self.conn_handle.add_session(peer_id, conn, crate::types::SessionReachMethod::Direct, None).await;
|
self.conn_handle.add_session(peer_id, conn, crate::types::SessionReachMethod::Direct, None).await;
|
||||||
self.conn_handle.log_activity(
|
self.conn_handle.log_activity(
|
||||||
ActivityLevel::Info,
|
ActivityLevel::Info,
|
||||||
|
|
@ -1352,7 +1352,7 @@ impl Network {
|
||||||
Ok(ExchangeResult::Accepted) => {
|
Ok(ExchangeResult::Accepted) => {
|
||||||
self.conn_handle.register_connection(peer_id, conn.clone(), vec![], PeerSlotKind::Local).await;
|
self.conn_handle.register_connection(peer_id, conn.clone(), vec![], PeerSlotKind::Local).await;
|
||||||
{
|
{
|
||||||
let s = self.storage.lock().await;
|
let s = self.storage.get().await;
|
||||||
let _ = s.add_mesh_peer(&peer_id, PeerSlotKind::Local, 0);
|
let _ = s.add_mesh_peer(&peer_id, PeerSlotKind::Local, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1464,7 +1464,7 @@ impl Network {
|
||||||
let addrs: Vec<SocketAddr> = redir.a.iter()
|
let addrs: Vec<SocketAddr> = redir.a.iter()
|
||||||
.filter_map(|a| a.parse::<SocketAddr>().ok())
|
.filter_map(|a| a.parse::<SocketAddr>().ok())
|
||||||
.collect();
|
.collect();
|
||||||
let _ = self.storage.lock().await.upsert_peer(&redir_id, &addrs, None);
|
let _ = self.storage.get().await.upsert_peer(&redir_id, &addrs, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1568,7 +1568,7 @@ impl Network {
|
||||||
} else {
|
} else {
|
||||||
// Network resolution: get reporter connections, resolve outside lock
|
// Network resolution: get reporter connections, resolve outside lock
|
||||||
let reporters_and_conns = {
|
let reporters_and_conns = {
|
||||||
let storage = self.storage.lock().await;
|
let storage = self.storage.get().await;
|
||||||
let n2 = storage.find_in_n2(&candidate_id).unwrap_or_default();
|
let n2 = storage.find_in_n2(&candidate_id).unwrap_or_default();
|
||||||
let n3 = storage.find_in_n3(&candidate_id).unwrap_or_default();
|
let n3 = storage.find_in_n3(&candidate_id).unwrap_or_default();
|
||||||
drop(storage);
|
drop(storage);
|
||||||
|
|
@ -1660,7 +1660,7 @@ impl Network {
|
||||||
|
|
||||||
// Find N2 reporter(s) who told us about this peer — they can introduce us
|
// Find N2 reporter(s) who told us about this peer — they can introduce us
|
||||||
let reporters = {
|
let reporters = {
|
||||||
let storage = self.storage.lock().await;
|
let storage = self.storage.get().await;
|
||||||
storage.find_in_n2(&candidate_id).unwrap_or_default()
|
storage.find_in_n2(&candidate_id).unwrap_or_default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1724,7 +1724,7 @@ impl Network {
|
||||||
|
|
||||||
/// Send a uni-stream message to all audience members (persistent if available, ephemeral otherwise).
|
/// Send a uni-stream message to all audience members (persistent if available, ephemeral otherwise).
|
||||||
async fn send_to_audience<T: Serialize>(&self, msg_type: MessageType, payload: &T) -> usize {
|
async fn send_to_audience<T: Serialize>(&self, msg_type: MessageType, payload: &T) -> usize {
|
||||||
let audience: Vec<NodeId> = match self.storage.lock().await.list_audience_members() {
|
let audience: Vec<NodeId> = match self.storage.get().await.list_audience_members() {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(_) => return 0,
|
Err(_) => return 0,
|
||||||
};
|
};
|
||||||
|
|
@ -1741,7 +1741,7 @@ impl Network {
|
||||||
pub async fn pull_from_peer(&self, peer_id: &NodeId) -> anyhow::Result<PullStats> {
|
pub async fn pull_from_peer(&self, peer_id: &NodeId) -> anyhow::Result<PullStats> {
|
||||||
let conn = self.get_connection(peer_id).await?;
|
let conn = self.get_connection(peer_id).await?;
|
||||||
let (our_follows, follows_sync) = {
|
let (our_follows, follows_sync) = {
|
||||||
let storage = self.storage.lock().await;
|
let storage = self.storage.get().await;
|
||||||
(
|
(
|
||||||
storage.list_follows()?,
|
storage.list_follows()?,
|
||||||
storage.get_follows_with_last_sync().unwrap_or_default(),
|
storage.get_follows_with_last_sync().unwrap_or_default(),
|
||||||
|
|
@ -1768,7 +1768,7 @@ impl Network {
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.as_millis() as u64;
|
.as_millis() as u64;
|
||||||
let storage = self.storage.lock().await;
|
let storage = self.storage.get().await;
|
||||||
let mut posts_received = 0;
|
let mut posts_received = 0;
|
||||||
let mut vis_updates = 0;
|
let mut vis_updates = 0;
|
||||||
for sp in &response.posts {
|
for sp in &response.posts {
|
||||||
|
|
@ -1968,7 +1968,7 @@ impl Network {
|
||||||
Ok(Ok(result)) if result.accepted => {
|
Ok(Ok(result)) if result.accepted => {
|
||||||
let our_profile = self.conn_handle.our_nat_profile().await;
|
let our_profile = self.conn_handle.our_nat_profile().await;
|
||||||
let peer_profile = {
|
let peer_profile = {
|
||||||
let s = self.storage.lock().await;
|
let s = self.storage.get().await;
|
||||||
s.get_peer_nat_profile(peer_id)
|
s.get_peer_nat_profile(peer_id)
|
||||||
};
|
};
|
||||||
if let Some(conn) = crate::connection::hole_punch_with_scanning(&self.endpoint, peer_id, &result.target_addresses, our_profile, peer_profile).await {
|
if let Some(conn) = crate::connection::hole_punch_with_scanning(&self.endpoint, peer_id, &result.target_addresses, our_profile, peer_profile).await {
|
||||||
|
|
@ -2010,7 +2010,7 @@ impl Network {
|
||||||
pub async fn addr_from_storage(&self, peer_id: &NodeId) -> Option<iroh::EndpointAddr> {
|
pub async fn addr_from_storage(&self, peer_id: &NodeId) -> Option<iroh::EndpointAddr> {
|
||||||
let endpoint_id = iroh::EndpointId::from_bytes(peer_id).ok()?;
|
let endpoint_id = iroh::EndpointId::from_bytes(peer_id).ok()?;
|
||||||
let mut addr = iroh::EndpointAddr::from(endpoint_id);
|
let mut addr = iroh::EndpointAddr::from(endpoint_id);
|
||||||
let storage = self.storage.lock().await;
|
let storage = self.storage.get().await;
|
||||||
if let Ok(Some(rec)) = storage.get_peer_record(peer_id) {
|
if let Ok(Some(rec)) = storage.get_peer_record(peer_id) {
|
||||||
for sock in &rec.addresses {
|
for sock in &rec.addresses {
|
||||||
addr = addr.with_ip_addr(*sock);
|
addr = addr.with_ip_addr(*sock);
|
||||||
|
|
@ -2158,7 +2158,7 @@ impl Network {
|
||||||
|
|
||||||
/// Check if a peer is a known anchor.
|
/// Check if a peer is a known anchor.
|
||||||
pub async fn is_anchor_peer(&self, node_id: &NodeId) -> bool {
|
pub async fn is_anchor_peer(&self, node_id: &NodeId) -> bool {
|
||||||
let storage = self.storage.lock().await;
|
let storage = self.storage.get().await;
|
||||||
storage.is_peer_anchor(node_id).unwrap_or(false)
|
storage.is_peer_anchor(node_id).unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2222,7 +2222,7 @@ impl Network {
|
||||||
|
|
||||||
let our_profile = self.conn_handle.our_nat_profile().await;
|
let our_profile = self.conn_handle.our_nat_profile().await;
|
||||||
let peer_profile = {
|
let peer_profile = {
|
||||||
let s = self.storage.lock().await;
|
let s = self.storage.get().await;
|
||||||
s.get_peer_nat_profile(&target)
|
s.get_peer_nat_profile(&target)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -2301,7 +2301,7 @@ impl Network {
|
||||||
exclude_peer: &crate::types::NodeId,
|
exclude_peer: &crate::types::NodeId,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
let downstream = {
|
let downstream = {
|
||||||
let storage = self.storage.lock().await;
|
let storage = self.storage.get().await;
|
||||||
storage.get_post_downstream(post_id).unwrap_or_default()
|
storage.get_post_downstream(post_id).unwrap_or_default()
|
||||||
};
|
};
|
||||||
let mut sent = 0;
|
let mut sent = 0;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -287,7 +287,7 @@ pub struct RefuseRedirectPayload {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Worm lookup query (bi-stream) — searches for nodes, posts, or blobs
|
/// Worm lookup query (bi-stream) — searches for nodes, posts, or blobs
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct WormQueryPayload {
|
pub struct WormQueryPayload {
|
||||||
pub worm_id: WormId,
|
pub worm_id: WormId,
|
||||||
pub target: NodeId,
|
pub target: NodeId,
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,44 @@ pub struct Storage {
|
||||||
conn: Connection,
|
conn: Connection,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pool of Storage connections for concurrent SQLite access in WAL mode.
|
||||||
|
/// Each connection is independently locked — readers don't block each other.
|
||||||
|
/// Uses tokio::sync::Mutex so guards are Send (safe across .await points).
|
||||||
|
pub struct StoragePool {
|
||||||
|
slots: Vec<tokio::sync::Mutex<Storage>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_POOL_SIZE: usize = 8;
|
||||||
|
|
||||||
|
impl StoragePool {
|
||||||
|
/// Create a pool of Storage connections to the same database.
|
||||||
|
pub fn open(path: impl AsRef<std::path::Path>) -> anyhow::Result<Self> {
|
||||||
|
let mut slots = Vec::with_capacity(STORAGE_POOL_SIZE);
|
||||||
|
// First connection does schema init + migration
|
||||||
|
let first = Storage::open(path.as_ref())?;
|
||||||
|
slots.push(tokio::sync::Mutex::new(first));
|
||||||
|
// Additional connections just open + WAL mode (schema already exists)
|
||||||
|
for _ in 1..STORAGE_POOL_SIZE {
|
||||||
|
let conn = Connection::open(path.as_ref())?;
|
||||||
|
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;")?;
|
||||||
|
slots.push(tokio::sync::Mutex::new(Storage { conn }));
|
||||||
|
}
|
||||||
|
Ok(Self { slots })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an available Storage connection. Tries each slot with try_lock;
|
||||||
|
/// if all busy, awaits the first (rare under normal load).
|
||||||
|
pub async fn get(&self) -> tokio::sync::MutexGuard<'_, Storage> {
|
||||||
|
for slot in &self.slots {
|
||||||
|
if let Ok(guard) = slot.try_lock() {
|
||||||
|
return guard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// All busy — await the first
|
||||||
|
self.slots[0].lock().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Current schema version. Bump this when making schema or data changes
|
/// Current schema version. Bump this when making schema or data changes
|
||||||
/// that require migration. Old databases with a lower version will be migrated.
|
/// that require migration. Old databases with a lower version will be migrated.
|
||||||
/// If the gap is too large (major version mismatch), the DB is reset instead.
|
/// If the gap is too large (major version mismatch), the DB is reset instead.
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ async fn serve_post(stream: &mut TcpStream, path: &str, node: &Arc<Node>, browse
|
||||||
|
|
||||||
// Single lock: gather holders, local post, AND author name if local
|
// Single lock: gather holders, local post, AND author name if local
|
||||||
let (holders, local_post, local_author_name) = {
|
let (holders, local_post, local_author_name) = {
|
||||||
let store = node.storage.lock().await;
|
let store = node.storage.get().await;
|
||||||
|
|
||||||
let mut holders = Vec::new();
|
let mut holders = Vec::new();
|
||||||
if let Some(author) = author_id {
|
if let Some(author) = author_id {
|
||||||
|
|
@ -190,7 +190,7 @@ async fn serve_post(stream: &mut TcpStream, path: &str, node: &Arc<Node>, browse
|
||||||
Ok(Ok(Some(sync_post))) => {
|
Ok(Ok(Some(sync_post))) => {
|
||||||
// Single lock: store post AND get author name
|
// Single lock: store post AND get author name
|
||||||
let author_name = {
|
let author_name = {
|
||||||
let store = node.storage.lock().await;
|
let store = node.storage.get().await;
|
||||||
let _ = store.store_post_with_visibility(
|
let _ = store.store_post_with_visibility(
|
||||||
&sync_post.id, &sync_post.post, &sync_post.visibility,
|
&sync_post.id, &sync_post.post, &sync_post.visibility,
|
||||||
);
|
);
|
||||||
|
|
@ -230,7 +230,7 @@ async fn try_redirect(
|
||||||
use crate::types::NatMapping;
|
use crate::types::NatMapping;
|
||||||
|
|
||||||
let post_hex = hex::encode(post_id);
|
let post_hex = hex::encode(post_id);
|
||||||
let store = node.storage.lock().await;
|
let store = node.storage.get().await;
|
||||||
|
|
||||||
// Classify holders into tiers
|
// Classify holders into tiers
|
||||||
let mut direct_candidates: Vec<(NodeId, String)> = Vec::new(); // http_addr known
|
let mut direct_candidates: Vec<(NodeId, String)> = Vec::new(); // http_addr known
|
||||||
|
|
@ -354,7 +354,7 @@ async fn serve_blob(stream: &mut TcpStream, path: &str, node: &Arc<Node>) {
|
||||||
// Check blobs table first, then scan post attachments (for posts stored via PostFetch
|
// Check blobs table first, then scan post attachments (for posts stored via PostFetch
|
||||||
// which don't populate the blobs table).
|
// which don't populate the blobs table).
|
||||||
let (mime_type, author_id) = {
|
let (mime_type, author_id) = {
|
||||||
let store = node.storage.lock().await;
|
let store = node.storage.get().await;
|
||||||
// Try blobs table first
|
// Try blobs table first
|
||||||
if let Some(mime) = find_public_blob_mime(&store, &blob_id) {
|
if let Some(mime) = find_public_blob_mime(&store, &blob_id) {
|
||||||
let author = store.get_blob_post_id(&blob_id).ok().flatten().and_then(|pid| {
|
let author = store.get_blob_post_id(&blob_id).ok().flatten().and_then(|pid| {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "itsgoin-desktop"
|
name = "itsgoin-desktop"
|
||||||
version = "0.4.2"
|
version = "0.4.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|
|
||||||
|
|
@ -198,7 +198,7 @@ async fn post_to_dto(
|
||||||
|
|
||||||
// Resolve intent kind from storage
|
// Resolve intent kind from storage
|
||||||
let intent_kind = {
|
let intent_kind = {
|
||||||
let storage = node.storage.lock().await;
|
let storage = node.storage.get().await;
|
||||||
match storage.get_post_intent(id) {
|
match storage.get_post_intent(id) {
|
||||||
Ok(Some(intent)) => match intent {
|
Ok(Some(intent)) => match intent {
|
||||||
VisibilityIntent::Public => "public".to_string(),
|
VisibilityIntent::Public => "public".to_string(),
|
||||||
|
|
@ -237,14 +237,14 @@ async fn post_to_dto(
|
||||||
.collect();
|
.collect();
|
||||||
// Engagement data
|
// Engagement data
|
||||||
let reaction_counts = {
|
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()
|
storage.get_reaction_counts(id, &node.node_id).unwrap_or_default()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(emoji, count, reacted_by_me)| ReactionCountDto { emoji, count, reacted_by_me })
|
.map(|(emoji, count, reacted_by_me)| ReactionCountDto { emoji, count, reacted_by_me })
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
let comment_count = {
|
let comment_count = {
|
||||||
let storage = node.storage.lock().await;
|
let storage = node.storage.get().await;
|
||||||
storage.get_comment_count(id).unwrap_or(0)
|
storage.get_comment_count(id).unwrap_or(0)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -286,7 +286,7 @@ async fn decrypt_just_created(
|
||||||
}
|
}
|
||||||
PostVisibility::GroupEncrypted { group_id, epoch, wrapped_cek } => {
|
PostVisibility::GroupEncrypted { group_id, epoch, wrapped_cek } => {
|
||||||
let seed_info = {
|
let seed_info = {
|
||||||
let storage = node.storage.lock().await;
|
let storage = node.storage.get().await;
|
||||||
storage.get_all_group_seeds_map().ok()
|
storage.get_all_group_seeds_map().ok()
|
||||||
.and_then(|map| map.get(&(*group_id, *epoch)).copied())
|
.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
|
// Prefer external address (UPnP, public IPv6, observed) over local bind address
|
||||||
let external_addr = node.network.http_addr();
|
let external_addr = node.network.http_addr();
|
||||||
let observed_addr = if external_addr.is_none() {
|
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()
|
storage.get_peer_record(&node.node_id).ok().flatten()
|
||||||
.and_then(|r| r.addresses.first().map(|a| a.to_string()))
|
.and_then(|r| r.addresses.first().map(|a| a.to_string()))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -475,7 +475,7 @@ async fn get_blob_path(
|
||||||
if let Some(ref pid_hex) = post_id_hex {
|
if let Some(ref pid_hex) = post_id_hex {
|
||||||
if let Ok(pid_bytes) = hex::decode(pid_hex) {
|
if let Ok(pid_bytes) = hex::decode(pid_hex) {
|
||||||
if let Ok(post_id) = <[u8; 32]>::try_from(pid_bytes.as_slice()) {
|
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 let Ok(Some((_post, vis))) = storage.get_post_with_visibility(&post_id) {
|
||||||
if !matches!(vis, PostVisibility::Public) {
|
if !matches!(vis, PostVisibility::Public) {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
|
@ -524,7 +524,7 @@ async fn resolve_blob_data(
|
||||||
// Try fetching from network if post_id provided
|
// Try fetching from network if post_id provided
|
||||||
if let Some(pid) = post_id {
|
if let Some(pid) = post_id {
|
||||||
let post = {
|
let post = {
|
||||||
let storage = node.storage.lock().await;
|
let storage = node.storage.get().await;
|
||||||
storage.get_post(&pid).map_err(|e| e.to_string())?
|
storage.get_post(&pid).map_err(|e| e.to_string())?
|
||||||
};
|
};
|
||||||
if let Some(post) = post {
|
if let Some(post) = post {
|
||||||
|
|
@ -665,7 +665,7 @@ async fn connect_peer(
|
||||||
// Store peer with addresses
|
// Store peer with addresses
|
||||||
let ip_addrs: Vec<_> = addr.ip_addrs().copied().collect();
|
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() {
|
if ip_addrs.is_empty() {
|
||||||
storage.add_peer(&nid).map_err(|e| e.to_string())?;
|
storage.add_peer(&nid).map_err(|e| e.to_string())?;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -720,7 +720,7 @@ async fn list_follows(state: State<'_, AppState>) -> Result<Vec<PeerDto>, String
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
// Try to get peer record for address info
|
// 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();
|
let rec = storage.get_peer_record(nid).ok().flatten();
|
||||||
drop(storage);
|
drop(storage);
|
||||||
let is_online = node.network.is_connected(nid).await
|
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)
|
.map(|(nid, _, _)| nid)
|
||||||
.collect();
|
.collect();
|
||||||
let (social_ids, n2_ids, n3_ids) = {
|
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
|
let social: std::collections::HashSet<_> = storage
|
||||||
.list_social_routes()
|
.list_social_routes()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
|
|
@ -994,7 +994,7 @@ async fn set_anchors(
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn list_anchor_peers(state: State<'_, AppState>) -> Result<Vec<PeerDto>, String> {
|
async fn list_anchor_peers(state: State<'_, AppState>) -> Result<Vec<PeerDto>, String> {
|
||||||
let node = state.inner();
|
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())?;
|
let records = storage.list_anchor_peers().map_err(|e| e.to_string())?;
|
||||||
drop(storage);
|
drop(storage);
|
||||||
let mut dtos = Vec::with_capacity(records.len());
|
let mut dtos = Vec::with_capacity(records.len());
|
||||||
|
|
@ -1026,7 +1026,7 @@ struct KnownAnchorDto {
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn list_known_anchors(state: State<'_, AppState>) -> Result<Vec<KnownAnchorDto>, String> {
|
async fn list_known_anchors(state: State<'_, AppState>) -> Result<Vec<KnownAnchorDto>, String> {
|
||||||
let node = state.inner();
|
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())?;
|
let anchors = storage.list_known_anchors().map_err(|e| e.to_string())?;
|
||||||
drop(storage);
|
drop(storage);
|
||||||
let mut dtos = Vec::with_capacity(anchors.len());
|
let mut dtos = Vec::with_capacity(anchors.len());
|
||||||
|
|
@ -1444,7 +1444,7 @@ async fn get_badge_counts(
|
||||||
last_feed_view_ms: u64,
|
last_feed_view_ms: u64,
|
||||||
) -> Result<BadgeCountsDto, String> {
|
) -> Result<BadgeCountsDto, String> {
|
||||||
let node = state.inner();
|
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
|
// 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())?;
|
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 (n2, n3) = {
|
||||||
let storage = node.storage.lock().await;
|
let storage = node.storage.get().await;
|
||||||
let n2 = storage.count_distinct_n2().unwrap_or(0);
|
let n2 = storage.count_distinct_n2().unwrap_or(0);
|
||||||
let n3 = storage.count_distinct_n3().unwrap_or(0);
|
let n3 = storage.count_distinct_n3().unwrap_or(0);
|
||||||
(n2, n3)
|
(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),
|
// Try known_anchors table first (populated by anchor register cycle),
|
||||||
// fall back to anchor peers from the peers table (is_anchor = true)
|
// fall back to anchor peers from the peers table (is_anchor = true)
|
||||||
let anchors: Vec<(NodeId, Vec<std::net::SocketAddr>)> = {
|
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();
|
let known = storage.list_known_anchors().unwrap_or_default();
|
||||||
if !known.is_empty() {
|
if !known.is_empty() {
|
||||||
known
|
known
|
||||||
|
|
@ -2096,7 +2096,7 @@ pub fn run() {
|
||||||
|
|
||||||
// Start blob eviction cycle (every 5 min)
|
// Start blob eviction cycle (every 5 min)
|
||||||
let cache_max_bytes: u64 = {
|
let cache_max_bytes: u64 = {
|
||||||
let storage = n.storage.lock().await;
|
let storage = n.storage.get().await;
|
||||||
storage.get_setting("cache_size_bytes")
|
storage.get_setting("cache_size_bytes")
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"productName": "itsgoin",
|
"productName": "itsgoin",
|
||||||
"version": "0.4.2",
|
"version": "0.4.3",
|
||||||
"identifier": "com.itsgoin.app",
|
"identifier": "com.itsgoin.app",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../../frontend",
|
"frontendDist": "../../frontend",
|
||||||
|
|
|
||||||
|
|
@ -3007,22 +3007,20 @@ $('#circle-profiles-toggle').addEventListener('click', () => {
|
||||||
|
|
||||||
// --- Notifications popover ---
|
// --- Notifications popover ---
|
||||||
// Text size toggle
|
// Text size toggle
|
||||||
const TEXT_SIZE_SCALES = { small: '100%', normal: '150%', large: '200%' };
|
const TEXT_SIZE_SCALES = { xsmall: '75%', small: '100%', normal: '125%', large: '150%', xlarge: '200%' };
|
||||||
// Apply text size immediately (default Normal = 150%)
|
// Apply text size immediately from localStorage cache (no async wait)
|
||||||
document.documentElement.style.fontSize = '150%';
|
const _cachedTextSize = localStorage.getItem('text_size') || 'normal';
|
||||||
(async () => {
|
document.documentElement.style.fontSize = TEXT_SIZE_SCALES[_cachedTextSize] || '125%';
|
||||||
const saved = await invoke('get_setting', { key: 'text_size' }).catch(() => null) || 'normal';
|
|
||||||
document.documentElement.style.fontSize = TEXT_SIZE_SCALES[saved] || '150%';
|
|
||||||
document.querySelectorAll('.text-size-opt').forEach(b => {
|
document.querySelectorAll('.text-size-opt').forEach(b => {
|
||||||
b.classList.toggle('active', b.dataset.size === saved);
|
b.classList.toggle('active', b.dataset.size === _cachedTextSize);
|
||||||
});
|
});
|
||||||
})();
|
|
||||||
document.querySelectorAll('.text-size-opt').forEach(btn => {
|
document.querySelectorAll('.text-size-opt').forEach(btn => {
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
const size = btn.dataset.size;
|
const size = btn.dataset.size;
|
||||||
document.documentElement.style.fontSize = TEXT_SIZE_SCALES[size] || '';
|
document.documentElement.style.fontSize = TEXT_SIZE_SCALES[size] || '';
|
||||||
document.querySelectorAll('.text-size-opt').forEach(b => b.classList.remove('active'));
|
document.querySelectorAll('.text-size-opt').forEach(b => b.classList.remove('active'));
|
||||||
btn.classList.add('active');
|
btn.classList.add('active');
|
||||||
|
localStorage.setItem('text_size', size);
|
||||||
await invoke('set_setting', { key: 'text_size', value: size }).catch(() => {});
|
await invoke('set_setting', { key: 'text_size', value: size }).catch(() => {});
|
||||||
toast('Text size updated');
|
toast('Text size updated');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,11 @@
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<nav id="tabs">
|
<nav id="tabs">
|
||||||
<button class="tab" data-tab="feed">Feed</button>
|
<button class="tab" data-tab="feed"><span class="tab-icon">📰</span><span class="tab-label">Feed</span></button>
|
||||||
<button class="tab" data-tab="myposts">My Posts</button>
|
<button class="tab" data-tab="myposts"><span class="tab-icon">✍</span><span class="tab-label">My Posts</span></button>
|
||||||
<button class="tab" data-tab="people">People</button>
|
<button class="tab" data-tab="people"><span class="tab-icon">👥</span><span class="tab-label">People</span></button>
|
||||||
<button class="tab" data-tab="messages">Messages</button>
|
<button class="tab" data-tab="messages"><span class="tab-icon">💬</span><span class="tab-label">Messages</span></button>
|
||||||
<button class="tab" data-tab="settings">Settings</button>
|
<button class="tab" data-tab="settings"><span class="tab-icon">⚙</span><span class="tab-label">Settings</span></button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Welcome (shown on startup) -->
|
<!-- Welcome (shown on startup) -->
|
||||||
|
|
@ -229,9 +229,11 @@
|
||||||
<div class="section-card">
|
<div class="section-card">
|
||||||
<h3>Text Size</h3>
|
<h3>Text Size</h3>
|
||||||
<div id="text-size-btns" style="display:flex;gap:0.4rem;margin-top:0.3rem">
|
<div id="text-size-btns" style="display:flex;gap:0.4rem;margin-top:0.3rem">
|
||||||
<button class="notif-opt text-size-opt" data-size="small">Small</button>
|
<button class="notif-opt text-size-opt" data-size="xsmall">XS</button>
|
||||||
<button class="notif-opt text-size-opt active" data-size="normal">Normal</button>
|
<button class="notif-opt text-size-opt" data-size="small">S</button>
|
||||||
<button class="notif-opt text-size-opt" data-size="large">Large</button>
|
<button class="notif-opt text-size-opt active" data-size="normal">M</button>
|
||||||
|
<button class="notif-opt text-size-opt" data-size="large">L</button>
|
||||||
|
<button class="notif-opt text-size-opt" data-size="xlarge">XL</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,13 +81,25 @@ header h1 { font-size: clamp(1.4rem, 2.5vw, 2rem); color: #7fdbca; margin: 0; }
|
||||||
.compose-right { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
|
.compose-right { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
|
||||||
.compose-left { display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; }
|
.compose-left { display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; }
|
||||||
|
|
||||||
/* Tabs */
|
/* Tabs — desktop (top bar) */
|
||||||
#tabs { display: flex; gap: 0; margin-bottom: 1rem; border-bottom: 1px solid #333; }
|
#tabs { display: flex; gap: 0; margin-bottom: 1rem; border-bottom: 1px solid #333; }
|
||||||
.tab { background: none; border: none; color: #99a; padding: 0.5rem 0.6rem; cursor: pointer; border-bottom: 2px solid transparent; font-size: 0.82rem; transition: color 0.15s, border-color 0.15s; position: relative; flex: 1; text-align: center; white-space: nowrap; }
|
.tab { background: none; border: none; color: #99a; padding: 0.5rem 0.6rem; cursor: pointer; border-bottom: 2px solid transparent; font-size: 0.82rem; transition: color 0.15s, border-color 0.15s; position: relative; flex: 1; text-align: center; white-space: nowrap; }
|
||||||
.tab:hover { color: #ccd; }
|
.tab:hover { color: #ccd; }
|
||||||
.tab.active { color: #7fdbca; border-bottom-color: #7fdbca; }
|
.tab.active { color: #7fdbca; border-bottom-color: #7fdbca; }
|
||||||
|
.tab-icon { display: none; }
|
||||||
.tab-badge { display: inline-flex; align-items: center; justify-content: center; background: #0f3460; color: #7fdbca; font-size: 0.6rem; min-width: 1.1rem; height: 1.1rem; border-radius: 0.55rem; padding: 0 0.3rem; margin-left: 0.25rem; font-family: system-ui, sans-serif; vertical-align: middle; }
|
.tab-badge { display: inline-flex; align-items: center; justify-content: center; background: #0f3460; color: #7fdbca; font-size: 0.6rem; min-width: 1.1rem; height: 1.1rem; border-radius: 0.55rem; padding: 0 0.3rem; margin-left: 0.25rem; font-family: system-ui, sans-serif; vertical-align: middle; }
|
||||||
|
|
||||||
|
/* Tabs — mobile/tablet (bottom nav bar) */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#tabs { position: fixed; bottom: 0; left: 0; right: 0; z-index: 900; background: #0a0a1a; border-bottom: none; border-top: 1px solid #333; margin-bottom: 0; padding: 0; padding-bottom: env(safe-area-inset-bottom, 0); }
|
||||||
|
.tab { flex-direction: column; align-items: center; padding: 0.4rem 0.2rem 0.3rem; border-bottom: none; border-top: 2px solid transparent; font-size: 0.6rem; gap: 0.15rem; display: flex; }
|
||||||
|
.tab.active { border-bottom: none; border-top-color: #7fdbca; }
|
||||||
|
.tab-icon { display: block; font-size: 1.2rem; line-height: 1; }
|
||||||
|
.tab-badge { position: absolute; top: 0.1rem; right: 0.2rem; margin-left: 0; font-size: 0.5rem; min-width: 0.9rem; height: 0.9rem; border-radius: 0.45rem; }
|
||||||
|
main { padding-bottom: 4rem; }
|
||||||
|
.toast { bottom: 4.5rem; }
|
||||||
|
}
|
||||||
|
|
||||||
/* Views / tab content transitions */
|
/* Views / tab content transitions */
|
||||||
.view { display: none; animation: viewFadeIn 0.2s ease-out; }
|
.view { display: none; animation: viewFadeIn 0.2s ease-out; }
|
||||||
.view.active { display: block; }
|
.view.active { display: block; }
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,8 @@
|
||||||
<p>This is the canonical technical reference for ItsGoin. It describes the vision, the architecture, and the current state of every subsystem — with full implementation detail. This document is versioned; each update records what changed.</p>
|
<p>This is the canonical technical reference for ItsGoin. It describes the vision, the architecture, and the current state of every subsystem — with full implementation detail. This document is versioned; each update records what changed.</p>
|
||||||
<div class="card" style="margin-top: 1rem;">
|
<div class="card" style="margin-top: 1rem;">
|
||||||
<strong style="font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em;">Changelog</strong>
|
<strong style="font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em;">Changelog</strong>
|
||||||
<p style="margin-top: 0.5rem;"><strong>v0.4.2</strong> (2026-03-22): Welcome screen — startup shows “How’s it goin?” with staggered counters (connections, posts, messages, reacts, comments) while backend bootstraps. Status ticker — header ticker for new posts, messages, reactions, comments, connection changes. Notification improvements — Tauri plugin → Web Notification → notify-rust fallback chain, Linux native notifications. Responsive text scaling — Small/Normal/Large (100%/150%/200%), persisted via settings. Diagnostics popover — diagnostics moved from inline section to overlay, connections on-demand, timers removed. Share details lightbox with QR code. Connect string prefers external address (UPnP/public IPv6/observed). Stale N1 fix — disconnected social routes excluded from N1 share. Replication handler fix — actively fetches posts + blobs from requester after accepting replication. Hole punch fix — target-side registers publicly routable remote address for relay introduction. Replication semaphore (3 concurrent max). Peer labels show truncated node ID.</p>
|
<p style="margin-top: 0.5rem;"><strong>v0.4.3</strong> (2026-03-22): Lock contention overhaul — all conn_mgr lock holds during network I/O eliminated. PostFetch, TcpPunch, PullFromPeer, FetchEngagement, ResolveAddress, AnchorProbe, WormLookup, ContentSearch now use brief locks for data gathering only. Bi-stream handlers (BlobRequest, WormQuery, RelayIntroduce, PostFetchRequest, ManifestRefresh) fully lock-free for I/O. ConnectionActor hoists shared Arcs (storage, blob_store, endpoint) for lock-free access. ResolveAddress adds 5s per-query timeout (was unbounded). Worm cascade uses connection snapshots. Initial exchange failure now aborts mesh upgrade (was silently continuing). connect_to_peer/connect_to_anchor use 15s timeout. StoragePool — 8 concurrent SQLite connections in WAL mode replace single Mutex<Storage>. Reads run fully parallel; writes serialize only at SQLite level. Bottom nav bar for mobile/tablet (≤768px) with icon tabs. Text sizes: XS 75%, S 100%, M 125% (default), L 150%, XL 200%. Text size persisted to localStorage for instant restore.</p>
|
||||||
|
<p><strong>v0.4.2</strong> (2026-03-22): Welcome screen — startup shows “How’s it goin?” with staggered counters (connections, posts, messages, reacts, comments) while backend bootstraps. Status ticker — header ticker for new posts, messages, reactions, comments, connection changes. Notification improvements — Tauri plugin → Web Notification → notify-rust fallback chain, Linux native notifications. Responsive text scaling — Small/Normal/Large (100%/150%/200%), persisted via settings. Diagnostics popover — diagnostics moved from inline section to overlay, connections on-demand, timers removed. Share details lightbox with QR code. Connect string prefers external address (UPnP/public IPv6/observed). Stale N1 fix — disconnected social routes excluded from N1 share. Replication handler fix — actively fetches posts + blobs from requester after accepting replication. Hole punch fix — target-side registers publicly routable remote address for relay introduction. Replication semaphore (3 concurrent max). Peer labels show truncated node ID.</p>
|
||||||
<p><strong>v0.4.1</strong> (2026-03-21): Security hardening — reaction signatures (ed25519), comment signature verification on receipt, reaction removal authorization, BlobHeader author verification. Lock contention fixes — ManifestPush discovery (cm lock released during I/O), pull request handler (filter without lock), pull sender (split into brief locks), engagement checker (batch writes per chunk). Data cleanup — post deletion cleans downstream/upstream/seen tables.</p>
|
<p><strong>v0.4.1</strong> (2026-03-21): Security hardening — reaction signatures (ed25519), comment signature verification on receipt, reaction removal authorization, BlobHeader author verification. Lock contention fixes — ManifestPush discovery (cm lock released during I/O), pull request handler (filter without lock), pull sender (split into brief locks), engagement checker (batch writes per chunk). Data cleanup — post deletion cleans downstream/upstream/seen tables.</p>
|
||||||
<p><strong>v0.4.0</strong> (2026-03-21): Protocol v4 — header-driven sync. ManifestPush as primary post notification. Slim PullSyncRequest (per-author timestamps, not full post ID list). Tiered engagement checks (5min/1hr/4hr/24hr by content age). Multi-upstream (3 max) with fallback chain. Auto-prefetch followed authors <90d. Self Last Encounter per-author tracking. Encrypted-but-not-for-us CDN caching. Serial engagement polling. ~90% bandwidth reduction for established nodes.</p>
|
<p><strong>v0.4.0</strong> (2026-03-21): Protocol v4 — header-driven sync. ManifestPush as primary post notification. Slim PullSyncRequest (per-author timestamps, not full post ID list). Tiered engagement checks (5min/1hr/4hr/24hr by content age). Multi-upstream (3 max) with fallback chain. Auto-prefetch followed authors <90d. Self Last Encounter per-author tracking. Encrypted-but-not-for-us CDN caching. Serial engagement polling. ~90% bandwidth reduction for established nodes.</p>
|
||||||
<p><strong>v0.3.6</strong> (2026-03-20): Active CDN replication — all devices proactively replicate recent posts to peers (desktops > anchors > phones priority). ReplicationRequest/Response (0xE1/0xE2). Device roles (Intermittent/Available/Persistent) advertised in InitialExchange. Bandwidth budgets: replication (pull to cache) + delivery (serve requests), hourly auto-reset, phones 100MB/1GB, desktops 200MB/2GB, anchors 200MB/1GB. Cache management: 1GB default, configurable, eviction cycle activated with share-link priority boost. Engagement distribution fix — BlobHeader JSON rebuilt after diff ops. Tombstone system — deleted reactions/comments tombstoned, propagate via pull sync. Persistent notifications via seen_engagement/seen_messages tables. DOS hardening: fan-out cap (10), prefetch cap (20), downstream registration cap (50), delivery budget enforcement. Pull preference reordered: non-anchors first. Network indicator — header dot (black/red/yellow/green) + capability labels. Tab badges — contextual counts (new posts, engagement, online, unread). Message read tracking on open/close/send. Stats bar removed.</p>
|
<p><strong>v0.3.6</strong> (2026-03-20): Active CDN replication — all devices proactively replicate recent posts to peers (desktops > anchors > phones priority). ReplicationRequest/Response (0xE1/0xE2). Device roles (Intermittent/Available/Persistent) advertised in InitialExchange. Bandwidth budgets: replication (pull to cache) + delivery (serve requests), hourly auto-reset, phones 100MB/1GB, desktops 200MB/2GB, anchors 200MB/1GB. Cache management: 1GB default, configurable, eviction cycle activated with share-link priority boost. Engagement distribution fix — BlobHeader JSON rebuilt after diff ops. Tombstone system — deleted reactions/comments tombstoned, propagate via pull sync. Persistent notifications via seen_engagement/seen_messages tables. DOS hardening: fan-out cap (10), prefetch cap (20), downstream registration cap (50), delivery budget enforcement. Pull preference reordered: non-anchors first. Network indicator — header dot (black/red/yellow/green) + capability labels. Tab badges — contextual counts (new posts, engagement, online, unread). Message read tracking on open/close/send. Stats bar removed.</p>
|
||||||
|
|
|
||||||
|
|
@ -25,16 +25,16 @@
|
||||||
<section>
|
<section>
|
||||||
<h1 style="font-size: 2rem; font-weight: 800; letter-spacing: -0.03em; margin-bottom: 0.25rem;">Download ItsGoin</h1>
|
<h1 style="font-size: 2rem; font-weight: 800; letter-spacing: -0.03em; margin-bottom: 0.25rem;">Download ItsGoin</h1>
|
||||||
<p>Available for Android and Linux. Free and open source.</p>
|
<p>Available for Android and Linux. Free and open source.</p>
|
||||||
<p style="color: var(--text-muted); font-size: 0.85rem;">Version 0.4.2 — March 22, 2026</p>
|
<p style="color: var(--text-muted); font-size: 0.85rem;">Version 0.4.3 — March 22, 2026</p>
|
||||||
|
|
||||||
<div class="downloads">
|
<div class="downloads">
|
||||||
<a href="itsgoin-0.4.2.apk" class="download-btn btn-android">
|
<a href="itsgoin-0.4.3.apk" class="download-btn btn-android">
|
||||||
Android APK
|
Android APK
|
||||||
<span class="sub">v0.4.2</span>
|
<span class="sub">v0.4.3</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="itsgoin_0.4.2_amd64.AppImage" class="download-btn btn-linux">
|
<a href="itsgoin_0.4.3_amd64.AppImage" class="download-btn btn-linux">
|
||||||
Linux AppImage
|
Linux AppImage
|
||||||
<span class="sub">v0.4.2</span>
|
<span class="sub">v0.4.3</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
<h3 style="color: var(--accent);">Android</h3>
|
<h3 style="color: var(--accent);">Android</h3>
|
||||||
<ol class="steps">
|
<ol class="steps">
|
||||||
<li><strong>Download the APK</strong> — Tap the button above. Your browser may warn that this type of file can be harmful — tap <strong>Download anyway</strong>.</li>
|
<li><strong>Download the APK</strong> — Tap the button above. Your browser may warn that this type of file can be harmful — tap <strong>Download anyway</strong>.</li>
|
||||||
<li><strong>Open the file</strong> — When the download finishes, tap the notification or find <code>itsgoin-0.4.2.apk</code> in your Downloads folder and tap it.</li>
|
<li><strong>Open the file</strong> — When the download finishes, tap the notification or find <code>itsgoin-0.4.3.apk</code> in your Downloads folder and tap it.</li>
|
||||||
<li><strong>Allow installation</strong> — Android will ask you to allow installs from this source. Tap <strong>Settings</strong>, toggle <strong>"Allow from this source"</strong>, then go back and tap <strong>Install</strong>.</li>
|
<li><strong>Allow installation</strong> — Android will ask you to allow installs from this source. Tap <strong>Settings</strong>, toggle <strong>"Allow from this source"</strong>, then go back and tap <strong>Install</strong>.</li>
|
||||||
<li><strong>Launch the app</strong> — Once installed, tap <strong>Open</strong> or find ItsGoin in your app drawer.</li>
|
<li><strong>Launch the app</strong> — Once installed, tap <strong>Open</strong> or find ItsGoin in your app drawer.</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
@ -59,8 +59,8 @@
|
||||||
<h3 style="color: var(--green);">Linux (AppImage)</h3>
|
<h3 style="color: var(--green);">Linux (AppImage)</h3>
|
||||||
<ol class="steps">
|
<ol class="steps">
|
||||||
<li><strong>Download the AppImage</strong> — Click the button above to download.</li>
|
<li><strong>Download the AppImage</strong> — Click the button above to download.</li>
|
||||||
<li><strong>Make it executable</strong> — Open a terminal and run:<br><code>chmod +x itsgoin_0.4.2_amd64.AppImage</code></li>
|
<li><strong>Make it executable</strong> — Open a terminal and run:<br><code>chmod +x itsgoin_0.4.3_amd64.AppImage</code></li>
|
||||||
<li><strong>Run it</strong> — Double-click the file, or from the terminal:<br><code>./itsgoin_0.4.2_amd64.AppImage</code></li>
|
<li><strong>Run it</strong> — Double-click the file, or from the terminal:<br><code>./itsgoin_0.4.3_amd64.AppImage</code></li>
|
||||||
</ol>
|
</ol>
|
||||||
<div class="note">
|
<div class="note">
|
||||||
<strong>Note:</strong> If it doesn't launch, you may need to install FUSE:<br><code>sudo apt install libfuse2</code> (Debian/Ubuntu) or <code>sudo dnf install fuse</code> (Fedora).
|
<strong>Note:</strong> If it doesn't launch, you may need to install FUSE:<br><code>sudo apt install libfuse2</code> (Debian/Ubuntu) or <code>sudo dnf install fuse</code> (Fedora).
|
||||||
|
|
@ -71,6 +71,17 @@
|
||||||
<section>
|
<section>
|
||||||
<h2>Changelog</h2>
|
<h2>Changelog</h2>
|
||||||
<div class="changelog">
|
<div class="changelog">
|
||||||
|
<div class="changelog-date">v0.4.3 — March 22, 2026</div>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Lock contention overhaul</strong> — All conn_mgr lock holds during network I/O eliminated across 14 handlers. Brief locks for data gathering only; all network operations run lock-free.</li>
|
||||||
|
<li><strong>StoragePool</strong> — 8 concurrent SQLite connections in WAL mode replace the single Mutex. Reads run fully parallel; writes serialize only at the SQLite level.</li>
|
||||||
|
<li><strong>Initial exchange fix</strong> — Failed initial exchanges now abort the mesh upgrade instead of silently continuing with a broken connection.</li>
|
||||||
|
<li><strong>Connect timeout</strong> — connect_to_peer and connect_to_anchor now use a consistent 15s timeout. ResolveAddress adds 5s per-query timeout (was unbounded).</li>
|
||||||
|
<li><strong>Worm cascade unlock</strong> — WormLookup, ContentSearch, and WormQuery use connection snapshots for lock-free fan-out.</li>
|
||||||
|
<li><strong>Bottom nav bar</strong> — Mobile/tablet (≤768px) gets a fixed bottom navigation bar with icon tabs. Desktop keeps the top tab bar.</li>
|
||||||
|
<li><strong>Text size update</strong> — Five options: XS (75%), S (100%), M (125% default), L (150%), XL (200%). Persisted to localStorage for instant restore on startup.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<div class="changelog-date">v0.4.2 — March 22, 2026</div>
|
<div class="changelog-date">v0.4.2 — March 22, 2026</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Welcome screen</strong> — Startup shows “How’s it goin?” with staggered counters (connections, posts, messages, reacts, comments) while the backend bootstraps. Replaces the blank-screen wait.</li>
|
<li><strong>Welcome screen</strong> — Startup shows “How’s it goin?” with staggered counters (connections, posts, messages, reacts, comments) while the backend bootstraps. Replaces the blank-screen wait.</li>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue