use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use thiserror::Error;
use tokio::sync::RwLock;
#[derive(Debug, Clone)]
pub struct OfflineStorageManager {
config: OfflineStorageConfig,
stats: Arc<RwLock<OfflineStorageStats>>,
indexed_db: Arc<RwLock<IndexedDbManager>>,
local_storage: Arc<RwLock<LocalStorageManager>>,
sync_queue: Arc<RwLock<SyncQueue>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OfflineStorageConfig {
pub enable_indexed_db: bool,
pub enable_local_storage: bool,
pub enable_sync_queue: bool,
pub max_storage_size: usize,
pub sync_batch_size: usize,
pub conflict_resolution_strategy: ConflictResolutionStrategy,
pub data_compression_enabled: bool,
pub encryption_enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ConflictResolutionStrategy {
LastWriteWins,
FirstWriteWins,
Merge,
Manual,
ServerWins,
ClientWins,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexedDbManager {
pub database_name: String,
pub version: u32,
pub object_stores: HashMap<String, ObjectStore>,
pub total_records: u64,
pub total_size: usize,
pub last_backup: Option<std::time::SystemTime>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ObjectStore {
pub name: String,
pub key_path: String,
pub auto_increment: bool,
pub indexes: Vec<Index>,
pub record_count: u64,
pub size: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Index {
pub name: String,
pub key_path: String,
pub unique: bool,
pub multi_entry: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocalStorageManager {
pub storage_items: HashMap<String, StorageItem>,
pub total_size: usize,
pub quota_usage: f64,
pub last_cleanup: std::time::SystemTime,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageItem {
pub key: String,
pub value: Vec<u8>,
pub size: usize,
pub created_at: std::time::SystemTime,
pub last_accessed: std::time::SystemTime,
pub access_count: u64,
pub ttl: Option<u64>,
pub compressed: bool,
pub encrypted: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncQueue {
pub pending_operations: Vec<SyncOperation>,
pub failed_operations: Vec<SyncOperation>,
pub completed_operations: Vec<SyncOperation>,
pub sync_retry_count: HashMap<String, u32>,
pub last_sync_attempt: Option<std::time::SystemTime>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncOperation {
pub operation_id: String,
pub operation_type: SyncOperationType,
pub data: Vec<u8>,
pub timestamp: std::time::SystemTime,
pub retry_count: u32,
pub max_retries: u32,
pub priority: SyncPriority,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum SyncOperationType {
Create,
Update,
Delete,
Upsert,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum SyncPriority {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OfflineStorageStats {
pub total_data_size: usize,
pub indexed_db_size: usize,
pub local_storage_size: usize,
pub sync_queue_size: usize,
pub total_records: u64,
pub sync_operations: u64,
pub failed_sync_operations: u64,
pub compression_ratio: f64,
pub optimization_benefit: f64,
pub optimizations_applied: u32,
pub storage_efficiency: f64,
pub conflict_resolution_count: u64,
}
#[derive(Error, Debug)]
pub enum OfflineStorageError {
#[error("IndexedDB error: {message}")]
IndexedDbError { message: String },
#[error("Local storage error: {message}")]
LocalStorageError { message: String },
#[error("Sync queue error: {message}")]
SyncQueueError { message: String },
#[error("Configuration error: {message}")]
ConfigurationError { message: String },
#[error("Storage quota exceeded: {limit} vs {usage}")]
StorageQuotaExceeded { limit: usize, usage: usize },
#[error("Data corruption error: {message}")]
DataCorruptionError { message: String },
}
impl OfflineStorageManager {
pub fn new(config: OfflineStorageConfig) -> Self {
Self {
config,
stats: Arc::new(RwLock::new(OfflineStorageStats::default())),
indexed_db: Arc::new(RwLock::new(IndexedDbManager {
database_name: "helios_offline_db".to_string(),
version: 1,
object_stores: HashMap::new(),
total_records: 0,
total_size: 0,
last_backup: None,
})),
local_storage: Arc::new(RwLock::new(LocalStorageManager {
storage_items: HashMap::new(),
total_size: 0,
quota_usage: 0.0,
last_cleanup: std::time::SystemTime::now(),
})),
sync_queue: Arc::new(RwLock::new(SyncQueue {
pending_operations: Vec::new(),
failed_operations: Vec::new(),
completed_operations: Vec::new(),
sync_retry_count: HashMap::new(),
last_sync_attempt: None,
})),
}
}
pub async fn initialize(&self) -> Result<(), OfflineStorageError> {
if self.config.enable_indexed_db {
self.initialize_indexed_db().await?;
}
if self.config.enable_local_storage {
self.initialize_local_storage().await?;
}
if self.config.enable_sync_queue {
self.initialize_sync_queue().await?;
}
self.update_stats().await;
Ok(())
}
async fn initialize_indexed_db(&self) -> Result<(), OfflineStorageError> {
let mut indexed_db = self.indexed_db.write().await;
let charts_store = ObjectStore {
name: "charts".to_string(),
key_path: "id".to_string(),
auto_increment: false,
indexes: vec![
Index {
name: "created_at".to_string(),
key_path: "created_at".to_string(),
unique: false,
multi_entry: false,
},
Index {
name: "type".to_string(),
key_path: "type".to_string(),
unique: false,
multi_entry: false,
},
],
record_count: 50,
size: 1024 * 1024, };
let data_store = ObjectStore {
name: "data".to_string(),
key_path: "id".to_string(),
auto_increment: false,
indexes: vec![Index {
name: "source".to_string(),
key_path: "source".to_string(),
unique: false,
multi_entry: false,
}],
record_count: 100,
size: 2 * 1024 * 1024, };
indexed_db
.object_stores
.insert("charts".to_string(), charts_store);
indexed_db
.object_stores
.insert("data".to_string(), data_store);
indexed_db.total_records = 150;
indexed_db.total_size = 3 * 1024 * 1024; indexed_db.last_backup = Some(std::time::SystemTime::now());
Ok(())
}
async fn initialize_local_storage(&self) -> Result<(), OfflineStorageError> {
let mut local_storage = self.local_storage.write().await;
let items = vec![
("user_preferences".to_string(), b"preferences_data".to_vec()),
("chart_templates".to_string(), b"template_data".to_vec()),
("recent_files".to_string(), b"recent_files_data".to_vec()),
];
for (key, value) in items {
let item = StorageItem {
key: key.clone(),
value: value.clone(),
size: value.len(),
created_at: std::time::SystemTime::now(),
last_accessed: std::time::SystemTime::now(),
access_count: 1,
ttl: Some(86400), compressed: self.config.data_compression_enabled,
encrypted: self.config.encryption_enabled,
};
local_storage.storage_items.insert(key, item);
local_storage.total_size += value.len();
}
local_storage.quota_usage = 0.1; local_storage.last_cleanup = std::time::SystemTime::now();
Ok(())
}
async fn initialize_sync_queue(&self) -> Result<(), OfflineStorageError> {
let mut sync_queue = self.sync_queue.write().await;
let operations = vec![
SyncOperation {
operation_id: "op1".to_string(),
operation_type: SyncOperationType::Create,
data: b"new_chart_data".to_vec(),
timestamp: std::time::SystemTime::now(),
retry_count: 0,
max_retries: 3,
priority: SyncPriority::High,
},
SyncOperation {
operation_id: "op2".to_string(),
operation_type: SyncOperationType::Update,
data: b"updated_data".to_vec(),
timestamp: std::time::SystemTime::now(),
retry_count: 1,
max_retries: 3,
priority: SyncPriority::Medium,
},
];
for operation in operations {
sync_queue.pending_operations.push(operation);
}
sync_queue.last_sync_attempt = Some(std::time::SystemTime::now());
Ok(())
}
pub async fn get_data(&self, key: &str) -> Result<Vec<u8>, OfflineStorageError> {
if self.config.enable_indexed_db {
if let Ok(data) = self.get_from_indexed_db(key).await {
return Ok(data);
}
}
if self.config.enable_local_storage {
if let Ok(data) = self.get_from_local_storage(key).await {
return Ok(data);
}
}
Err(OfflineStorageError::IndexedDbError {
message: "Data not found".to_string(),
})
}
async fn get_from_indexed_db(&self, key: &str) -> Result<Vec<u8>, OfflineStorageError> {
let indexed_db = self.indexed_db.read().await;
if key == "chart_data" {
Ok(b"chart_data_from_indexed_db".to_vec())
} else if key == "data_item" {
Ok(b"data_from_indexed_db".to_vec())
} else {
Err(OfflineStorageError::IndexedDbError {
message: "Data not found in IndexedDB".to_string(),
})
}
}
async fn get_from_local_storage(&self, key: &str) -> Result<Vec<u8>, OfflineStorageError> {
let mut local_storage = self.local_storage.write().await;
if let Some(item) = local_storage.storage_items.get_mut(key) {
item.last_accessed = std::time::SystemTime::now();
item.access_count += 1;
Ok(item.value.clone())
} else {
Err(OfflineStorageError::LocalStorageError {
message: "Data not found in local storage".to_string(),
})
}
}
pub async fn store_data(&self, key: String, data: Vec<u8>) -> Result<(), OfflineStorageError> {
if self.config.enable_indexed_db {
self.store_in_indexed_db(key.clone(), data.clone()).await?;
}
if self.config.enable_local_storage {
self.store_in_local_storage(key, data).await?;
}
Ok(())
}
async fn store_in_indexed_db(
&self,
_key: String,
data: Vec<u8>,
) -> Result<(), OfflineStorageError> {
let mut indexed_db = self.indexed_db.write().await;
indexed_db.total_records += 1;
indexed_db.total_size += data.len();
Ok(())
}
async fn store_in_local_storage(
&self,
key: String,
data: Vec<u8>,
) -> Result<(), OfflineStorageError> {
let mut local_storage = self.local_storage.write().await;
let item = StorageItem {
key: key.clone(),
value: data.clone(),
size: data.len(),
created_at: std::time::SystemTime::now(),
last_accessed: std::time::SystemTime::now(),
access_count: 1,
ttl: Some(86400), compressed: self.config.data_compression_enabled,
encrypted: self.config.encryption_enabled,
};
local_storage.storage_items.insert(key, item);
local_storage.total_size += data.len();
Ok(())
}
pub async fn add_sync_operation(
&self,
operation: SyncOperation,
) -> Result<(), OfflineStorageError> {
let mut sync_queue = self.sync_queue.write().await;
sync_queue.pending_operations.push(operation);
Ok(())
}
pub async fn process_sync_queue(&self) -> Result<(), OfflineStorageError> {
let mut sync_queue = self.sync_queue.write().await;
let mut completed_operations = Vec::new();
let mut failed_operations = Vec::new();
for operation in sync_queue.pending_operations.drain(..) {
if operation.retry_count < operation.max_retries {
completed_operations.push(operation);
} else {
failed_operations.push(operation);
}
}
sync_queue.completed_operations.extend(completed_operations);
sync_queue.failed_operations.extend(failed_operations);
sync_queue.last_sync_attempt = Some(std::time::SystemTime::now());
Ok(())
}
pub async fn resolve_conflicts(
&self,
conflicts: Vec<DataConflict>,
) -> Result<Vec<ConflictResolution>, OfflineStorageError> {
let mut resolutions = Vec::new();
for conflict in conflicts {
let resolution = match self.config.conflict_resolution_strategy {
ConflictResolutionStrategy::LastWriteWins => {
if conflict.client_timestamp > conflict.server_timestamp {
ConflictResolution::UseClient
} else {
ConflictResolution::UseServer
}
}
ConflictResolutionStrategy::FirstWriteWins => {
if conflict.client_timestamp < conflict.server_timestamp {
ConflictResolution::UseClient
} else {
ConflictResolution::UseServer
}
}
ConflictResolutionStrategy::ServerWins => ConflictResolution::UseServer,
ConflictResolutionStrategy::ClientWins => ConflictResolution::UseClient,
ConflictResolutionStrategy::Merge => ConflictResolution::Merge,
ConflictResolutionStrategy::Manual => ConflictResolution::Manual,
};
resolutions.push(resolution);
}
Ok(resolutions)
}
pub async fn get_stats(&self) -> OfflineStorageStats {
self.stats.read().await.clone()
}
pub async fn update_config(
&mut self,
config: OfflineStorageConfig,
) -> Result<(), OfflineStorageError> {
self.config = config;
Ok(())
}
pub async fn get_indexed_db_manager(&self) -> IndexedDbManager {
self.indexed_db.read().await.clone()
}
pub async fn get_local_storage_manager(&self) -> LocalStorageManager {
self.local_storage.read().await.clone()
}
pub async fn get_sync_queue(&self) -> SyncQueue {
self.sync_queue.read().await.clone()
}
async fn update_stats(&self) {
let mut stats = self.stats.write().await;
let indexed_db = self.indexed_db.read().await;
let local_storage = self.local_storage.read().await;
let sync_queue = self.sync_queue.read().await;
stats.indexed_db_size = indexed_db.total_size;
stats.local_storage_size = local_storage.total_size;
stats.sync_queue_size =
sync_queue.pending_operations.len() + sync_queue.failed_operations.len();
stats.total_data_size = stats.indexed_db_size + stats.local_storage_size;
stats.total_records = indexed_db.total_records + local_storage.storage_items.len() as u64;
stats.sync_operations = sync_queue.completed_operations.len() as u64;
stats.failed_sync_operations = sync_queue.failed_operations.len() as u64;
stats.compression_ratio = if self.config.data_compression_enabled {
0.7
} else {
1.0
};
stats.optimization_benefit = 0.5; stats.optimizations_applied = 1;
stats.storage_efficiency = 0.9; stats.conflict_resolution_count = 0;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataConflict {
pub key: String,
pub client_data: Vec<u8>,
pub server_data: Vec<u8>,
pub client_timestamp: std::time::SystemTime,
pub server_timestamp: std::time::SystemTime,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ConflictResolution {
UseClient,
UseServer,
Merge,
Manual,
}
impl Default for OfflineStorageConfig {
fn default() -> Self {
Self {
enable_indexed_db: true,
enable_local_storage: true,
enable_sync_queue: true,
max_storage_size: 100 * 1024 * 1024, sync_batch_size: 10,
conflict_resolution_strategy: ConflictResolutionStrategy::LastWriteWins,
data_compression_enabled: true,
encryption_enabled: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_offline_storage_config() -> OfflineStorageConfig {
OfflineStorageConfig {
enable_indexed_db: true,
enable_local_storage: true,
enable_sync_queue: true,
max_storage_size: 10 * 1024 * 1024, sync_batch_size: 5,
conflict_resolution_strategy: ConflictResolutionStrategy::LastWriteWins,
data_compression_enabled: true,
encryption_enabled: false,
}
}
#[tokio::test]
async fn test_offline_storage_manager_creation() {
let config = create_test_offline_storage_config();
let manager = OfflineStorageManager::new(config);
let stats = manager.get_stats().await;
assert_eq!(stats.total_data_size, 0);
assert_eq!(stats.total_records, 0);
assert_eq!(stats.optimizations_applied, 0);
}
#[tokio::test]
async fn test_offline_storage_initialization() {
let config = create_test_offline_storage_config();
let manager = OfflineStorageManager::new(config);
let result = manager.initialize().await;
assert!(result.is_ok());
let stats = manager.get_stats().await;
assert!(stats.total_data_size > 0);
assert!(stats.total_records > 0);
assert!(stats.optimizations_applied > 0);
}
#[tokio::test]
async fn test_data_storage_and_retrieval() {
let config = create_test_offline_storage_config();
let manager = OfflineStorageManager::new(config);
let _ = manager.initialize().await;
let result = manager
.store_data("test_key".to_string(), b"test_data".to_vec())
.await;
assert!(result.is_ok());
let data = manager.get_data("test_key").await;
assert!(data.is_ok());
assert_eq!(data.unwrap(), b"test_data");
}
#[tokio::test]
async fn test_indexed_db_data_retrieval() {
let config = create_test_offline_storage_config();
let manager = OfflineStorageManager::new(config);
let _ = manager.initialize().await;
let chart_data = manager.get_data("chart_data").await;
assert!(chart_data.is_ok());
assert_eq!(chart_data.unwrap(), b"chart_data_from_indexed_db");
let data = manager.get_data("data_item").await;
assert!(data.is_ok());
assert_eq!(data.unwrap(), b"data_from_indexed_db");
}
#[tokio::test]
async fn test_local_storage_data_retrieval() {
let config = create_test_offline_storage_config();
let manager = OfflineStorageManager::new(config);
let _ = manager.initialize().await;
let preferences = manager.get_data("user_preferences").await;
assert!(preferences.is_ok());
assert_eq!(preferences.unwrap(), b"preferences_data");
let templates = manager.get_data("chart_templates").await;
assert!(templates.is_ok());
assert_eq!(templates.unwrap(), b"template_data");
}
#[tokio::test]
async fn test_sync_operation_management() {
let config = create_test_offline_storage_config();
let manager = OfflineStorageManager::new(config);
let _ = manager.initialize().await;
let operation = SyncOperation {
operation_id: "test_op".to_string(),
operation_type: SyncOperationType::Create,
data: b"sync_data".to_vec(),
timestamp: std::time::SystemTime::now(),
retry_count: 0,
max_retries: 3,
priority: SyncPriority::High,
};
let result = manager.add_sync_operation(operation).await;
assert!(result.is_ok());
let result = manager.process_sync_queue().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_conflict_resolution() {
let config = create_test_offline_storage_config();
let manager = OfflineStorageManager::new(config);
let _ = manager.initialize().await;
let conflicts = vec![DataConflict {
key: "test_key".to_string(),
client_data: b"client_data".to_vec(),
server_data: b"server_data".to_vec(),
client_timestamp: std::time::SystemTime::now(),
server_timestamp: std::time::SystemTime::now() - std::time::Duration::from_secs(3600),
}];
let resolutions = manager.resolve_conflicts(conflicts).await;
assert!(resolutions.is_ok());
let resolutions = resolutions.unwrap();
assert_eq!(resolutions.len(), 1);
assert_eq!(resolutions[0], ConflictResolution::UseClient); }
#[tokio::test]
async fn test_indexed_db_manager() {
let config = create_test_offline_storage_config();
let manager = OfflineStorageManager::new(config);
let _ = manager.initialize().await;
let indexed_db = manager.get_indexed_db_manager().await;
assert_eq!(indexed_db.database_name, "helios_offline_db");
assert!(indexed_db.total_records > 0);
assert!(indexed_db.total_size > 0);
assert!(!indexed_db.object_stores.is_empty());
}
#[tokio::test]
async fn test_local_storage_manager() {
let config = create_test_offline_storage_config();
let manager = OfflineStorageManager::new(config);
let _ = manager.initialize().await;
let local_storage = manager.get_local_storage_manager().await;
assert!(!local_storage.storage_items.is_empty());
assert!(local_storage.total_size > 0);
assert!(local_storage.quota_usage > 0.0);
}
#[tokio::test]
async fn test_sync_queue() {
let config = create_test_offline_storage_config();
let manager = OfflineStorageManager::new(config);
let _ = manager.initialize().await;
let sync_queue = manager.get_sync_queue().await;
assert!(!sync_queue.pending_operations.is_empty());
assert!(sync_queue.last_sync_attempt.is_some());
}
#[tokio::test]
async fn test_config_update() {
let config = create_test_offline_storage_config();
let mut manager = OfflineStorageManager::new(config);
let new_config = create_test_offline_storage_config();
let result = manager.update_config(new_config).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_offline_storage_statistics() {
let config = create_test_offline_storage_config();
let manager = OfflineStorageManager::new(config);
let _ = manager.initialize().await;
let stats = manager.get_stats().await;
assert!(stats.total_data_size > 0);
assert!(stats.total_records > 0);
assert!(stats.optimization_benefit > 0.0);
assert!(stats.storage_efficiency > 0.0);
}
}