orcs_runtime/session/
store.rs1use super::{SessionAsset, StorageError};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::future::Future;
10use std::path::PathBuf;
11
12pub trait SessionStore: Send + Sync {
34 fn save(&self, asset: &SessionAsset) -> impl Future<Output = Result<(), StorageError>> + Send;
38
39 fn load(&self, id: &str) -> impl Future<Output = Result<SessionAsset, StorageError>> + Send;
45
46 fn list(&self) -> impl Future<Output = Result<Vec<SessionMeta>, StorageError>> + Send;
50
51 fn delete(&self, id: &str) -> impl Future<Output = Result<(), StorageError>> + Send;
57
58 fn exists(&self, id: &str) -> impl Future<Output = Result<bool, StorageError>> + Send;
60
61 fn sync_status(&self) -> SyncStatus {
65 SyncStatus::offline()
66 }
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct SessionMeta {
75 pub id: String,
77
78 pub name: Option<String>,
80
81 pub created_at: DateTime<Utc>,
83
84 pub updated_at: DateTime<Utc>,
86
87 pub project_path: Option<PathBuf>,
89
90 pub turn_count: usize,
92
93 pub sync_state: SyncState,
95}
96
97impl SessionMeta {
98 pub fn from_asset(asset: &SessionAsset) -> Self {
100 Self {
101 id: asset.id.clone(),
102 name: asset.project_context.name.clone(),
103 created_at: DateTime::from_timestamp_millis(asset.created_at_ms as i64)
104 .unwrap_or_else(Utc::now),
105 updated_at: DateTime::from_timestamp_millis(asset.updated_at_ms as i64)
106 .unwrap_or_else(Utc::now),
107 project_path: asset.project_context.root_path.clone(),
108 turn_count: asset.history.len(),
109 sync_state: SyncState::LocalOnly,
110 }
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
116pub enum SyncState {
117 #[default]
119 LocalOnly,
120
121 Synced,
123
124 LocalAhead,
126
127 RemoteAhead,
129
130 Conflict,
132}
133
134impl SyncState {
135 pub fn is_synced(&self) -> bool {
137 matches!(self, Self::Synced)
138 }
139
140 pub fn has_pending_changes(&self) -> bool {
142 matches!(self, Self::LocalAhead | Self::RemoteAhead)
143 }
144
145 pub fn has_conflict(&self) -> bool {
147 matches!(self, Self::Conflict)
148 }
149}
150
151#[derive(Debug, Clone, Default)]
153pub struct SyncStatus {
154 pub mode: SyncMode,
156
157 pub last_sync: Option<DateTime<Utc>>,
159
160 pub pending_uploads: usize,
162
163 pub pending_downloads: usize,
165
166 pub conflicts: Vec<String>,
168}
169
170impl SyncStatus {
171 pub fn offline() -> Self {
173 Self {
174 mode: SyncMode::Offline,
175 ..Default::default()
176 }
177 }
178
179 pub fn online() -> Self {
181 Self {
182 mode: SyncMode::Online,
183 last_sync: Some(Utc::now()),
184 ..Default::default()
185 }
186 }
187
188 pub fn is_fully_synced(&self) -> bool {
190 self.mode == SyncMode::Online
191 && self.pending_uploads == 0
192 && self.pending_downloads == 0
193 && self.conflicts.is_empty()
194 }
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
199pub enum SyncMode {
200 #[default]
202 Offline,
203
204 Online,
206
207 Syncing,
209
210 Error,
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn sync_state_predicates() {
220 assert!(SyncState::Synced.is_synced());
221 assert!(!SyncState::LocalOnly.is_synced());
222
223 assert!(SyncState::LocalAhead.has_pending_changes());
224 assert!(!SyncState::Synced.has_pending_changes());
225
226 assert!(SyncState::Conflict.has_conflict());
227 assert!(!SyncState::LocalOnly.has_conflict());
228 }
229
230 #[test]
231 fn sync_status_offline() {
232 let status = SyncStatus::offline();
233 assert_eq!(status.mode, SyncMode::Offline);
234 assert!(status.last_sync.is_none());
235 }
236
237 #[test]
238 fn sync_status_fully_synced() {
239 let status = SyncStatus::online();
240 assert!(status.is_fully_synced());
241
242 let status_with_pending = SyncStatus {
243 mode: SyncMode::Online,
244 pending_uploads: 1,
245 ..Default::default()
246 };
247 assert!(!status_with_pending.is_fully_synced());
248 }
249}