Refactor upload and polling logic
This commit consolidates the upload and polling functionalities into a single `ServerSync` worker. The `oneliner_poller` and `uploader` modules have been removed, and their responsibilities are now handled by `server_sync`. The `ServerSync` worker manages both uploading pending recordings and polling the `/api/oneliners` endpoint. Uploads take precedence, and polling only occurs when the pending upload directory is empty. The worker now uses a unified HTTP client and a single `tokio::select!` loop for managing these tasks. Key changes include: - Removal of `oneliner_poller.rs` and `uploader.rs`. - Introduction of `server_sync.rs` and `upload.rs` in `doctate-client-core`. - `ServerSync` now handles upload events (`UploadEvent`) and provides a `WorkerSnapshot` for UI feedback. - Upload logic has been refactored to handle transient and terminal errors more robustly, including file deletion for terminal failures. - Polling logic is integrated into the `ServerSync` loop, utilizing the existing `SnapshotCache` and `CaseStore`. - The `App` component in `client-desktop` has been updated to use `ServerSync` instead of the separate poller and uploader. - Documentation comments have been updated to reflect the new structure.
This commit is contained in:
@@ -140,6 +140,30 @@ impl CaseStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark an existing case as confirmed-by-server. Called by the
|
||||
/// `ServerSync` worker immediately after a successful upload ACK — so
|
||||
/// the `synced_to_server` flag reflects reality without waiting for
|
||||
/// the next `/api/oneliners` poll to round-trip.
|
||||
///
|
||||
/// No-op if the marker does not exist locally (e.g. `clear_all` was
|
||||
/// called between `submit` and `ACK`). `last_activity_at` is **not**
|
||||
/// touched: its semantics are "last user activity" (recording end),
|
||||
/// not "last server confirmation".
|
||||
pub async fn mark_synced(&self, case_id: Uuid) -> Result<(), CaseStoreError> {
|
||||
let mut state = self.state.lock().await;
|
||||
let Some(marker) = state.get_mut(&case_id) else {
|
||||
return Ok(());
|
||||
};
|
||||
if marker.synced_to_server {
|
||||
return Ok(()); // already true, skip the disk write
|
||||
}
|
||||
marker.synced_to_server = true;
|
||||
let marker_clone = marker.clone();
|
||||
write_marker_file(&self.dir, &marker_clone).await?;
|
||||
self.broadcast(&state);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply a server `/api/oneliners` response. For every entry in the
|
||||
/// snapshot: insert (with `synced_to_server = true`) or update the
|
||||
/// local marker (oneliner + last_activity_at from server timestamps,
|
||||
@@ -647,6 +671,40 @@ mod tests {
|
||||
assert_eq!(store.list().await.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mark_synced_flips_flag_without_touching_activity() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let (store, _rx) = CaseStore::open(tmp.path().to_owned()).await.unwrap();
|
||||
let id = Uuid::new_v4();
|
||||
store.create_local(id, t("2026-04-18T10:00:00Z")).await.unwrap();
|
||||
assert!(!store.list().await[0].synced_to_server);
|
||||
|
||||
store.mark_synced(id).await.unwrap();
|
||||
let list = store.list().await;
|
||||
assert!(list[0].synced_to_server);
|
||||
assert_eq!(list[0].last_activity_at, "2026-04-18T10:00:00Z");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mark_synced_is_noop_when_marker_missing() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let (store, _rx) = CaseStore::open(tmp.path().to_owned()).await.unwrap();
|
||||
// No marker at all — must not fail, must not create one.
|
||||
store.mark_synced(Uuid::new_v4()).await.unwrap();
|
||||
assert!(store.list().await.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mark_synced_is_idempotent() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let (store, _rx) = CaseStore::open(tmp.path().to_owned()).await.unwrap();
|
||||
let id = Uuid::new_v4();
|
||||
store.create_local(id, t("2026-04-18T10:00:00Z")).await.unwrap();
|
||||
store.mark_synced(id).await.unwrap();
|
||||
store.mark_synced(id).await.unwrap(); // second call: no disk churn
|
||||
assert!(store.list().await[0].synced_to_server);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sorted_newest_first() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user