Skip to main content

vectorless/client/
workspace.rs

1// Copyright (c) 2026 vectorless developers
2// SPDX-License-Identifier: Apache-2.0
3
4//! Workspace management client.
5//!
6//! This module provides async CRUD operations for document persistence
7//! through the workspace abstraction.
8//!
9//! # Example
10//!
11//! ```rust,ignore
12//! let workspace = WorkspaceClient::new(workspace_storage).await;
13//!
14//! // Save a document
15//! workspace.save(&doc).await?;
16//!
17//! // Load a document
18//! let doc = workspace.load("doc-id").await?;
19//!
20//! // List all documents
21//! for doc in workspace.list().await? {
22//!     println!("{}: {}", doc.id, doc.name);
23//! }
24//! ```
25
26use std::sync::Arc;
27
28use tracing::{debug, info, warn};
29
30use crate::error::Result;
31use crate::storage::{PersistedDocument, Workspace};
32
33use super::events::{EventEmitter, WorkspaceEvent};
34use super::types::DocumentInfo;
35
36/// Workspace management client.
37///
38/// Provides async thread-safe CRUD operations for document persistence.
39/// All operations are async and can be safely called from multiple tasks.
40///
41/// # Thread Safety
42///
43/// The client is fully thread-safe and can be cloned cheaply
44/// (it uses `Arc` internally).
45#[derive(Clone)]
46pub struct WorkspaceClient {
47    /// Workspace storage.
48    workspace: Arc<Workspace>,
49
50    /// Event emitter.
51    events: EventEmitter,
52
53    /// Configuration.
54    config: WorkspaceClientConfig,
55}
56
57/// Workspace client configuration.
58#[derive(Debug, Clone)]
59pub struct WorkspaceClientConfig {
60    /// Auto-save interval in seconds (None = disabled).
61    pub auto_save_interval: Option<u64>,
62
63    /// Enable verbose logging.
64    pub verbose: bool,
65}
66
67impl Default for WorkspaceClientConfig {
68    fn default() -> Self {
69        Self {
70            auto_save_interval: None,
71            verbose: false,
72        }
73    }
74}
75
76impl WorkspaceClient {
77    /// Create a new workspace client.
78    pub async fn new(workspace: Workspace) -> Self {
79        Self {
80            workspace: Arc::new(workspace),
81            events: EventEmitter::new(),
82            config: WorkspaceClientConfig::default(),
83        }
84    }
85
86    /// Create with event emitter.
87    pub fn with_events(mut self, events: EventEmitter) -> Self {
88        self.events = events;
89        self
90    }
91
92    /// Create with configuration.
93    pub fn with_config(mut self, config: WorkspaceClientConfig) -> Self {
94        self.config = config;
95        self
96    }
97
98    /// Create from an existing workspace Arc.
99    pub(crate) fn from_arc(workspace: Arc<Workspace>, events: EventEmitter) -> Self {
100        Self {
101            workspace,
102            events,
103            config: WorkspaceClientConfig::default(),
104        }
105    }
106
107    /// Save a document to the workspace.
108    ///
109    /// # Errors
110    ///
111    /// Returns an error if the workspace write fails.
112    pub async fn save(&self, doc: &PersistedDocument) -> Result<()> {
113        let doc_id = doc.meta.id.clone();
114
115        self.workspace.add(doc).await?;
116
117        info!("Saved document: {}", doc_id);
118        self.events.emit_workspace(WorkspaceEvent::Saved { doc_id });
119
120        Ok(())
121    }
122
123    /// Load a document from the workspace.
124    ///
125    /// Returns `Ok(None)` if the document doesn't exist.
126    ///
127    /// # Errors
128    ///
129    /// Returns an error if the workspace read fails.
130    pub async fn load(&self, doc_id: &str) -> Result<Option<PersistedDocument>> {
131        if !self.workspace.contains(doc_id).await {
132            return Ok(None);
133        }
134
135        let doc = self.workspace.load_and_cache(doc_id).await?;
136        let cache_hit = doc.is_some();
137
138        if let Some(ref doc) = doc {
139            debug!("Loaded document: {} (cache={})", doc_id, cache_hit);
140        }
141
142        self.events.emit_workspace(WorkspaceEvent::Loaded {
143            doc_id: doc_id.to_string(),
144            cache_hit,
145        });
146
147        Ok(doc)
148    }
149
150    /// Remove a document from the workspace.
151    ///
152    /// Returns `Ok(true)` if the document was removed, `Ok(false)` if it didn't exist.
153    ///
154    /// # Errors
155    ///
156    /// Returns an error if the workspace write fails.
157    pub async fn remove(&self, doc_id: &str) -> Result<bool> {
158        let removed = self.workspace.remove(doc_id).await?;
159
160        if removed {
161            info!("Removed document: {}", doc_id);
162            self.events.emit_workspace(WorkspaceEvent::Removed {
163                doc_id: doc_id.to_string(),
164            });
165        }
166
167        Ok(removed)
168    }
169
170    /// Check if a document exists in the workspace.
171    ///
172    /// # Errors
173    ///
174    /// Returns an error if the workspace read fails.
175    pub async fn exists(&self, doc_id: &str) -> Result<bool> {
176        Ok(self.workspace.contains(doc_id).await)
177    }
178
179    /// List all documents in the workspace.
180    ///
181    /// # Errors
182    ///
183    /// Returns an error if the workspace read fails.
184    pub async fn list(&self) -> Result<Vec<DocumentInfo>> {
185        let doc_ids = self.workspace.list_documents().await;
186        let mut result = Vec::with_capacity(doc_ids.len());
187
188        for id in &doc_ids {
189            if let Some(meta) = self.workspace.get_meta(id).await {
190                result.push(DocumentInfo {
191                    id: meta.id,
192                    name: meta.doc_name,
193                    format: meta.doc_type,
194                    description: meta.doc_description,
195                    page_count: meta.page_count,
196                    line_count: meta.line_count,
197                });
198            }
199        }
200
201        Ok(result)
202    }
203
204    /// Get document info by ID.
205    ///
206    /// # Errors
207    ///
208    /// Returns an error if the workspace read fails.
209    pub async fn get_document_info(&self, doc_id: &str) -> Result<Option<DocumentInfo>> {
210        Ok(self
211            .workspace
212            .get_meta(doc_id)
213            .await
214            .map(|meta| DocumentInfo {
215                id: meta.id,
216                name: meta.doc_name,
217                format: meta.doc_type,
218                description: meta.doc_description,
219                page_count: meta.page_count,
220                line_count: meta.line_count,
221            }))
222    }
223
224    /// Remove multiple documents from the workspace.
225    ///
226    /// Returns the number of documents successfully removed.
227    ///
228    /// # Errors
229    ///
230    /// Returns an error if the workspace write fails.
231    pub async fn batch_remove(&self, doc_ids: &[&str]) -> Result<usize> {
232        let mut removed = 0;
233
234        for doc_id in doc_ids {
235            if self.workspace.remove(doc_id).await? {
236                removed += 1;
237                self.events.emit_workspace(WorkspaceEvent::Removed {
238                    doc_id: doc_id.to_string(),
239                });
240            }
241        }
242
243        if removed > 0 {
244            info!("Batch removed {} documents", removed);
245        }
246
247        Ok(removed)
248    }
249
250    /// Clear all documents from the workspace.
251    ///
252    /// Returns the number of documents removed.
253    ///
254    /// # Errors
255    ///
256    /// Returns an error if the workspace write fails.
257    pub async fn clear(&self) -> Result<usize> {
258        let doc_ids = self.workspace.list_documents().await;
259        let count = doc_ids.len();
260
261        for doc_id in &doc_ids {
262            let _ = self.workspace.remove(doc_id).await;
263        }
264
265        if count > 0 {
266            info!("Cleared workspace: {} documents removed", count);
267            self.events
268                .emit_workspace(WorkspaceEvent::Cleared { count });
269        }
270
271        Ok(count)
272    }
273
274    /// Get workspace statistics.
275    pub async fn stats(&self) -> Result<WorkspaceStats> {
276        Ok(WorkspaceStats {
277            document_count: self.workspace.len().await,
278        })
279    }
280
281    /// Get the number of documents in the workspace.
282    pub async fn len(&self) -> usize {
283        self.workspace.len().await
284    }
285
286    /// Check if the workspace is empty.
287    pub async fn is_empty(&self) -> bool {
288        self.workspace.is_empty().await
289    }
290
291    /// Get the underlying workspace Arc (for advanced use).
292    pub(crate) fn inner(&self) -> Arc<Workspace> {
293        Arc::clone(&self.workspace)
294    }
295}
296
297/// Workspace statistics.
298#[derive(Debug, Clone)]
299pub struct WorkspaceStats {
300    /// Number of documents in the workspace.
301    pub document_count: usize,
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307    use crate::storage::backend::MemoryBackend;
308    use std::sync::Arc as StdArc;
309
310    #[tokio::test]
311    async fn test_workspace_client_creation() {
312        let backend = StdArc::new(MemoryBackend::new());
313        let workspace = Workspace::with_backend(backend).await.unwrap();
314        let client = WorkspaceClient::new(workspace).await;
315        assert!(client.is_empty().await);
316    }
317
318    #[tokio::test]
319    async fn test_workspace_stats() {
320        let backend = StdArc::new(MemoryBackend::new());
321        let workspace = Workspace::with_backend(backend).await.unwrap();
322        let client = WorkspaceClient::new(workspace).await;
323
324        let stats = client.stats().await.unwrap();
325        assert_eq!(stats.document_count, 0);
326    }
327}