Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 45 additions & 5 deletions ui/src/components/app/freenet_api/freenet_synchronizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ pub enum SynchronizerMessage {
ConnectionLost,
/// Sent when page becomes visible after being hidden (e.g., after sleep/wake)
PageBecameVisible,
/// Sent to refresh all room states after reconnection (e.g., after sleep/wake)
/// This fetches current state for all rooms to catch any updates missed during suspension
RefreshAllRooms,
ApiResponse(Result<HostResponse, SynchronizerError>),
AcceptInvitation {
owner_vk: VerifyingKey,
Expand Down Expand Up @@ -206,16 +209,53 @@ impl FreenetSynchronizer {
error!("Failed to send Connect message after wake: {}", e);
}
} else {
// Connection appears active, trigger a room sync to verify
// This will fail fast if the connection is actually dead
info!("Connection appears active, triggering room sync to verify");
// Connection appears active, but we may have missed updates during suspension.
// First verify connection with ProcessRooms, then refresh all rooms to
// catch any updates that arrived while the page was hidden/PC was suspended.
info!("Connection appears active, refreshing all rooms to catch missed updates");
if let Err(e) =
message_tx.unbounded_send(SynchronizerMessage::ProcessRooms)
message_tx.unbounded_send(SynchronizerMessage::RefreshAllRooms)
{
error!("Failed to send ProcessRooms message after wake: {}", e);
error!("Failed to send RefreshAllRooms message after wake: {}", e);
}
}
}
SynchronizerMessage::RefreshAllRooms => {
// Refresh all room states by sending GET requests
// This catches any updates missed during PC suspension or page being hidden
info!("Refreshing all rooms to catch missed updates");
if !connection_manager.is_connected() {
info!(
"Connection not ready, deferring refresh and attempting to connect"
);
if let Err(e) = message_tx.unbounded_send(SynchronizerMessage::Connect)
{
error!("Failed to send Connect message: {}", e);
}
continue;
}
if let Err(e) = response_handler
.get_room_synchronizer_mut()
.refresh_all_rooms()
.await
{
error!("Error refreshing rooms: {}", e);
// Check if this is a WebSocket error that needs reconnection
let error_str = e.to_string();
if error_str.contains("WebSocket") || error_str.contains("not open") {
warn!(
"WebSocket error during room refresh, triggering reconnection"
);
if let Err(e) =
message_tx.unbounded_send(SynchronizerMessage::ConnectionLost)
{
error!("Failed to send ConnectionLost: {}", e);
}
}
} else {
info!("Successfully refreshed all rooms");
}
}
SynchronizerMessage::Connect => {
info!("Connecting to Freenet");
match connection_manager
Expand Down
65 changes: 62 additions & 3 deletions ui/src/components/app/freenet_api/response_handler/get_response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ pub async fn handle_get_response(
// First try to find the owner_vk from SYNC_INFO
let owner_vk = SYNC_INFO.read().get_owner_vk_for_instance_id(key.id());

// If we couldn't find it in SYNC_INFO, try to find it in PENDING_INVITES by checking contract keys
// If we couldn't find it in SYNC_INFO, try fallback mechanisms
let owner_vk = if owner_vk.is_none() {
// This is a fallback mechanism in case SYNC_INFO wasn't properly set up
warn!(
"Owner VK not found in SYNC_INFO for contract ID: {}, trying fallback",
key.id()
);

// First try PENDING_INVITES
let pending_invites = PENDING_INVITES.read();
let mut found_owner_vk = None;

Expand All @@ -50,15 +51,34 @@ pub async fn handle_get_response(
break;
}
}
drop(pending_invites);

// If not in pending invites, try ROOMS (for refresh after suspension)
if found_owner_vk.is_none() {
let rooms = ROOMS.read();
for (owner_key, room_data) in rooms.map.iter() {
if room_data.contract_key.id() == key.id() {
info!(
"Found matching owner key in existing rooms: {:?}",
MemberId::from(*owner_key)
);
found_owner_vk = Some(*owner_key);
break;
}
}
}

found_owner_vk
} else {
owner_vk
};

// Now check if this is for a pending invitation
// Now check if this is for a pending invitation or an existing room needing refresh
if let Some(owner_vk) = owner_vk {
if PENDING_INVITES.read().map.contains_key(&owner_vk) {
let is_pending_invite = PENDING_INVITES.read().map.contains_key(&owner_vk);
let is_existing_room = ROOMS.read().map.contains_key(&owner_vk);

if is_pending_invite {
info!("This is a subscription for a pending invitation, adding state");
let retrieved_state: ChatRoomStateV1 = from_cbor_slice::<ChatRoomStateV1>(&state);

Expand Down Expand Up @@ -304,6 +324,45 @@ pub async fn handle_get_response(
info!("Successfully triggered synchronization after joining room");
}
}
} else if is_existing_room {
// This is a refresh GET for an already-subscribed room (e.g., after wake from suspension)
info!("Processing GET response for existing room (refresh after suspension)");
let retrieved_state: ChatRoomStateV1 = from_cbor_slice::<ChatRoomStateV1>(&state);

ROOMS.with_mut(|rooms| {
if let Some(room_data) = rooms.map.get_mut(&owner_vk) {
// Create parameters for merge
let params = ChatRoomParametersV1 { owner: owner_vk };

// Clone current state to avoid borrow issues during merge
let current_state = room_data.room_state.clone();

// Merge the retrieved state into the existing state
match room_data
.room_state
.merge(&current_state, &params, &retrieved_state)
{
Ok(_) => {
info!(
"Successfully merged refreshed state for room {:?}",
MemberId::from(owner_vk)
);
}
Err(e) => {
error!(
"Failed to merge refreshed state for room {:?}: {}",
MemberId::from(owner_vk),
e
);
}
}
}
});

// Update sync info to reflect we received fresh state
SYNC_INFO.with_mut(|sync_info| {
sync_info.update_sync_status(&owner_vk, RoomSyncStatus::Subscribed);
});
}
}

Expand Down
63 changes: 63 additions & 0 deletions ui/src/components/app/freenet_api/room_synchronizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,69 @@ impl RoomSynchronizer {
});
}

/// Refresh all room states by sending GET requests.
/// This is used after PC suspension/wake to catch any updates that were missed
/// while the page was hidden or the machine was suspended.
pub async fn refresh_all_rooms(&self) -> Result<(), SynchronizerError> {
info!("Refreshing all rooms to catch missed updates");

// Check if WebAPI is available
let web_api_available = WEB_API.read().is_some();
if !web_api_available {
warn!("WebAPI not available, skipping room refresh");
return Err(SynchronizerError::ApiNotInitialized);
}

// Collect all room owner keys that we're currently tracking
let room_owners: Vec<VerifyingKey> = ROOMS.read().map.keys().copied().collect();

if room_owners.is_empty() {
info!("No rooms to refresh");
return Ok(());
}

info!("Refreshing {} rooms", room_owners.len());

for owner_vk in room_owners {
let contract_key = owner_vk_to_contract_key(&owner_vk);

// Send a GET request to fetch the current state
// This will trigger a response that merges any missed updates
let get_request = ContractRequest::Get {
key: *contract_key.id(),
return_contract_code: false,
subscribe: false, // Already subscribed, just need the state
};

let client_request = ClientRequest::ContractOp(get_request);

if let Some(web_api) = WEB_API.write().as_mut() {
match web_api.send(client_request).await {
Ok(_) => {
info!(
"Sent refresh GET request for room {:?}",
MemberId::from(owner_vk)
);
}
Err(e) => {
// Don't fail the entire refresh if one room fails
error!(
"Error sending refresh GET for room {:?}: {}",
MemberId::from(owner_vk),
e
);
}
}
} else {
warn!("WebAPI became unavailable during refresh");
return Err(SynchronizerError::ApiNotInitialized);
}
}

info!("Finished sending refresh requests for all rooms");
Ok(())
}

/// Subscribe to a contract after a successful GET or PUT operation
pub async fn subscribe_to_contract(
&self,
Expand Down
16 changes: 10 additions & 6 deletions ui/src/example_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,12 +344,16 @@ mod tests {
);

// Verify room has at least basic configuration
assert!(!room_data
.room_state
.configuration
.configuration
.name
.is_empty());
assert!(
room_data
.room_state
.configuration
.configuration
.display
.name
.declared_len()
> 0
);

// Verify members list exists
assert!(!room_data.room_state.members.members.is_empty());
Expand Down