Skip to main content

kanban_persistence/
traits.rs

1use crate::PersistenceResult;
2use async_trait::async_trait;
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::path::{Path, PathBuf};
6use uuid::Uuid;
7
8/// Metadata for persistence operations
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct PersistenceMetadata {
11    /// ID of the instance that performed the save
12    pub instance_id: Uuid,
13    /// When this data was saved
14    pub saved_at: DateTime<Utc>,
15}
16
17impl PersistenceMetadata {
18    pub fn new(instance_id: Uuid) -> Self {
19        Self {
20            instance_id,
21            saved_at: Utc::now(),
22        }
23    }
24}
25
26/// Point-in-time snapshot of all data that needs to be persisted
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct StoreSnapshot {
29    /// Raw JSON bytes representing all boards, columns, cards, etc.
30    pub data: Vec<u8>,
31    /// Metadata about this snapshot
32    pub metadata: PersistenceMetadata,
33}
34
35/// Events that can be emitted during persistence operations
36#[derive(Debug, Clone)]
37pub enum PersistenceEvent {
38    /// Data was successfully saved
39    Saved(PersistenceMetadata),
40    /// External changes were detected
41    ExternalChangeDetected {
42        path: PathBuf,
43        saved_at: DateTime<Utc>,
44    },
45    /// A conflict occurred (our changes vs external changes)
46    ConflictDetected { reason: String },
47    /// An error occurred during persistence
48    Error(String),
49}
50
51/// Trait for abstract storage operations
52/// Implementations handle different backend storage (file, database, etc.)
53#[async_trait]
54pub trait PersistenceStore: Send + Sync {
55    /// Save a snapshot to the store
56    async fn save(&self, snapshot: StoreSnapshot) -> PersistenceResult<PersistenceMetadata>;
57
58    /// Load the current snapshot from the store
59    async fn load(&self) -> PersistenceResult<(StoreSnapshot, PersistenceMetadata)>;
60
61    /// Check if the store file exists
62    async fn exists(&self) -> bool;
63
64    /// Get the path to the store file
65    fn path(&self) -> &Path;
66
67    /// Get the unique instance ID for this store
68    fn instance_id(&self) -> uuid::Uuid;
69
70    /// Sync the command log from the in-memory backend to persistent storage.
71    /// Default implementation is a no-op (SQLite backend writes directly).
72    async fn sync_command_log(
73        &self,
74        _batches: &[Vec<kanban_domain::commands::Command>],
75        _cursor: u64,
76        _baseline: Option<&[u8]>,
77    ) -> PersistenceResult<()> {
78        Ok(())
79    }
80
81    /// Retrieve the command log, undo cursor, and optional baseline snapshot.
82    /// Returns `(batches, cursor, baseline_bytes)`. Default returns empty.
83    #[allow(clippy::type_complexity)]
84    fn get_command_log(
85        &self,
86    ) -> PersistenceResult<(
87        Vec<Vec<kanban_domain::commands::Command>>,
88        u64,
89        Option<Vec<u8>>,
90    )> {
91        Ok((vec![], 0, None))
92    }
93
94    /// Load the store synchronously (no async runtime required).
95    /// Returns `Ok(None)` when the backing file does not exist.
96    ///
97    /// The default implementation returns an error; backends that support
98    /// synchronous loading (e.g. `JsonFileStore`) override this.
99    #[allow(clippy::type_complexity)]
100    fn load_sync(&self) -> PersistenceResult<Option<(StoreSnapshot, PersistenceMetadata)>> {
101        Err(crate::PersistenceError::Unsupported(
102            "load_sync not supported by this backend".into(),
103        ))
104    }
105}
106
107/// Trait for detecting changes to the storage file
108/// Used for multi-instance coordination
109#[async_trait]
110pub trait ChangeDetector: Send + Sync {
111    /// Start watching the file for changes
112    async fn start_watching(&self, path: PathBuf) -> PersistenceResult<()>;
113
114    /// Stop watching the file
115    async fn stop_watching(&self) -> PersistenceResult<()>;
116
117    /// Subscribe to change events
118    /// Returns a broadcast receiver that yields `ChangeEvent` when the file changes
119    fn subscribe(&self) -> tokio::sync::broadcast::Receiver<ChangeEvent>;
120
121    /// Check if currently watching
122    fn is_watching(&self) -> bool;
123}
124
125/// Event indicating a change to the watched file
126#[derive(Debug, Clone)]
127pub struct ChangeEvent {
128    /// Path to the file that changed
129    pub path: PathBuf,
130    /// When the change was detected
131    pub detected_at: DateTime<Utc>,
132}
133
134/// Trait for serialization/deserialization strategies
135/// Allows swapping JSON for binary formats, databases, etc.
136pub trait Serializer<T: Send + Sync>: Send + Sync {
137    /// Serialize data to bytes
138    fn serialize(&self, data: &T) -> PersistenceResult<Vec<u8>>;
139
140    /// Deserialize data from bytes
141    fn deserialize(&self, bytes: &[u8]) -> PersistenceResult<T>;
142}
143
144/// Format versions for migration tracking
145#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
146pub enum FormatVersion {
147    V1,
148    V2,
149    V3,
150    V4,
151    V5,
152}
153
154impl FormatVersion {
155    pub fn as_u32(self) -> u32 {
156        match self {
157            Self::V1 => 1,
158            Self::V2 => 2,
159            Self::V3 => 3,
160            Self::V4 => 4,
161            Self::V5 => 5,
162        }
163    }
164
165    pub fn from_u32(v: u32) -> Option<Self> {
166        match v {
167            1 => Some(Self::V1),
168            2 => Some(Self::V2),
169            3 => Some(Self::V3),
170            4 => Some(Self::V4),
171            5 => Some(Self::V5),
172            _ => None,
173        }
174    }
175}
176
177/// Trait for migration strategies between format versions
178#[async_trait]
179pub trait MigrationStrategy: Send + Sync {
180    /// Detect the version of a file on disk
181    async fn detect_version(&self, path: &Path) -> PersistenceResult<FormatVersion>;
182
183    /// Migrate from one version to another
184    /// Returns the path to the migrated file
185    async fn migrate(
186        &self,
187        from: FormatVersion,
188        to: FormatVersion,
189        path: &Path,
190    ) -> PersistenceResult<PathBuf>;
191}
192
193/// Trait for conflict resolution between local and external changes
194pub trait ConflictResolver: Send + Sync {
195    /// Determine whether local or external change wins
196    /// Returns true if external changes should be used, false for local changes
197    fn should_use_external(
198        &self,
199        local_metadata: &PersistenceMetadata,
200        external_metadata: &PersistenceMetadata,
201    ) -> bool;
202
203    /// Get a human-readable description of the conflict resolution
204    fn explain_resolution(
205        &self,
206        local_metadata: &PersistenceMetadata,
207        external_metadata: &PersistenceMetadata,
208    ) -> String;
209}