jj workspaces over the network
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(cli): add offline support and operation queuing

+161
+161
crates/tandem-cli/src/offline.rs
··· 1 + use std::path::{Path, PathBuf}; 2 + use serde::{Serialize, Deserialize}; 3 + use chrono::{DateTime, Utc}; 4 + use tandem_core::types::{ChangeRecord, ChangeId}; 5 + 6 + #[derive(Debug, thiserror::Error)] 7 + pub enum OfflineError { 8 + #[error("IO error: {0}")] 9 + Io(#[from] std::io::Error), 10 + #[error("Serialization error: {0}")] 11 + Serialization(#[from] serde_json::Error), 12 + } 13 + 14 + /// Types of operations that can be queued offline 15 + #[derive(Debug, Clone, Serialize, Deserialize)] 16 + #[serde(tag = "type", rename_all = "snake_case")] 17 + pub enum QueuedOperation { 18 + /// A change was created or modified 19 + ChangeUpdated { 20 + record: ChangeRecord, 21 + timestamp: DateTime<Utc>, 22 + }, 23 + /// A bookmark was moved 24 + BookmarkMoved { 25 + name: String, 26 + target: ChangeId, 27 + timestamp: DateTime<Utc>, 28 + }, 29 + /// Presence was updated 30 + PresenceUpdated { 31 + change_id: ChangeId, 32 + timestamp: DateTime<Utc>, 33 + }, 34 + } 35 + 36 + /// Queue for offline operations 37 + #[derive(Debug, Default, Serialize, Deserialize)] 38 + pub struct OperationQueue { 39 + operations: Vec<QueuedOperation>, 40 + } 41 + 42 + impl OperationQueue { 43 + pub fn new() -> Self { 44 + Self::default() 45 + } 46 + 47 + /// Load queue from disk 48 + pub fn load(repo_path: &Path) -> Result<Self, OfflineError> { 49 + let queue_path = Self::queue_path(repo_path); 50 + 51 + if !queue_path.exists() { 52 + return Ok(Self::new()); 53 + } 54 + 55 + let content = std::fs::read_to_string(&queue_path)?; 56 + let queue: Self = serde_json::from_str(&content)?; 57 + Ok(queue) 58 + } 59 + 60 + /// Save queue to disk 61 + pub fn save(&self, repo_path: &Path) -> Result<(), OfflineError> { 62 + let queue_path = Self::queue_path(repo_path); 63 + 64 + // Create parent directory if needed 65 + if let Some(parent) = queue_path.parent() { 66 + std::fs::create_dir_all(parent)?; 67 + } 68 + 69 + let content = serde_json::to_string_pretty(self)?; 70 + std::fs::write(&queue_path, content)?; 71 + Ok(()) 72 + } 73 + 74 + /// Add operation to queue 75 + pub fn enqueue(&mut self, op: QueuedOperation) { 76 + self.operations.push(op); 77 + } 78 + 79 + /// Get number of queued operations 80 + pub fn len(&self) -> usize { 81 + self.operations.len() 82 + } 83 + 84 + /// Check if queue is empty 85 + pub fn is_empty(&self) -> bool { 86 + self.operations.is_empty() 87 + } 88 + 89 + /// Take all operations (clears queue) 90 + pub fn drain(&mut self) -> Vec<QueuedOperation> { 91 + std::mem::take(&mut self.operations) 92 + } 93 + 94 + /// Clear the queue and delete the file 95 + pub fn clear(&mut self, repo_path: &Path) -> Result<(), OfflineError> { 96 + self.operations.clear(); 97 + let queue_path = Self::queue_path(repo_path); 98 + if queue_path.exists() { 99 + std::fs::remove_file(&queue_path)?; 100 + } 101 + Ok(()) 102 + } 103 + 104 + fn queue_path(repo_path: &Path) -> PathBuf { 105 + repo_path.join(".jj").join("forge-queue.json") 106 + } 107 + } 108 + 109 + /// Replay queued operations to forge 110 + pub async fn replay_queue( 111 + repo_path: &Path, 112 + doc: &tandem_core::sync::ForgeDoc, 113 + ) -> Result<usize, OfflineError> { 114 + let mut queue = OperationQueue::load(repo_path)?; 115 + 116 + if queue.is_empty() { 117 + return Ok(0); 118 + } 119 + 120 + let operations = queue.drain(); 121 + let count = operations.len(); 122 + 123 + for op in operations { 124 + match op { 125 + QueuedOperation::ChangeUpdated { record, .. } => { 126 + doc.insert_change(&record); 127 + } 128 + QueuedOperation::BookmarkMoved { name, target, .. } => { 129 + doc.set_bookmark(&name, &target); 130 + } 131 + QueuedOperation::PresenceUpdated { .. } => { 132 + // Presence updates are ephemeral, skip old ones 133 + } 134 + } 135 + } 136 + 137 + // Clear the queue file 138 + queue.clear(repo_path)?; 139 + 140 + Ok(count) 141 + } 142 + 143 + /// Check if we're in offline mode (no connection to forge) 144 + pub fn is_offline(repo_path: &Path) -> bool { 145 + // Check for offline marker file 146 + let marker = repo_path.join(".jj").join("forge-offline"); 147 + marker.exists() 148 + } 149 + 150 + /// Set offline mode 151 + pub fn set_offline(repo_path: &Path, offline: bool) -> Result<(), OfflineError> { 152 + let marker = repo_path.join(".jj").join("forge-offline"); 153 + 154 + if offline { 155 + std::fs::write(&marker, "")?; 156 + } else if marker.exists() { 157 + std::fs::remove_file(&marker)?; 158 + } 159 + 160 + Ok(()) 161 + }