Refactor: Move DataServer to its own module

Moves the `DataServer` struct and its implementation into a new
`dataserver` module. This improves code organization and separates data
loading logic from the main application logic.
This commit is contained in:
Michael Schimmel
2026-02-16 18:12:05 +01:00
parent 6974bbaf7f
commit c8f561749d
2 changed files with 172 additions and 161 deletions
+166
View File
@@ -0,0 +1,166 @@
use bytemuck::cast_slice;
use std::collections::HashMap;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use std::time::Instant;
use zip::ZipArchive;
#[derive(Debug, Clone)]
pub struct DataFile {
pub path: PathBuf,
pub symbol: String,
pub year: i32,
pub month: i32,
}
#[derive(Debug)]
pub struct CacheEntry<T> {
pub data: Arc<Vec<T>>,
pub last_accessed: Instant,
}
pub struct DataServer<T: bytemuck::Pod + Send + Sync + 'static> {
base_path: PathBuf,
pub symbols: RwLock<HashMap<String, Vec<DataFile>>>,
pub cache: RwLock<HashMap<PathBuf, CacheEntry<T>>>,
}
impl<T: bytemuck::Pod + Send + Sync + 'static> DataServer<T> {
pub fn new(path: impl AsRef<Path>) -> Self {
Self {
base_path: path.as_ref().to_path_buf(),
symbols: RwLock::new(HashMap::new()),
cache: RwLock::new(HashMap::new()),
}
}
pub async fn update_symbols(&self) -> tokio::io::Result<()> {
let mut entries = tokio::fs::read_dir(&self.base_path).await?;
let mut temp_map: HashMap<String, Vec<DataFile>> = HashMap::new();
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
if file_name.ends_with(".m1") || file_name.ends_with(".tick") {
if let Some(data_file) = self.parse_file_name(&path, file_name) {
temp_map
.entry(data_file.symbol.clone())
.or_default()
.push(data_file);
}
}
}
}
for files in temp_map.values_mut() {
files.sort_by(|a, b| a.year.cmp(&b.year).then(a.month.cmp(&b.month)));
}
let mut symbols = self.symbols.write().unwrap();
*symbols = temp_map;
Ok(())
}
pub fn enumerate_symbols(&self) -> Vec<String> {
let symbols = self.symbols.read().unwrap();
let mut keys: Vec<String> = symbols.keys().cloned().collect();
keys.sort();
keys
}
fn parse_file_name(&self, path: &Path, file_name: &str) -> Option<DataFile> {
let stem = Path::new(file_name).file_stem()?.to_str()?;
let ext = Path::new(file_name).extension()?.to_str()?;
let parts: Vec<&str> = stem.split('_').collect();
if parts.len() < 3 {
return None;
}
let month = parts.last()?.parse::<i32>().ok()?;
let year = parts[parts.len() - 2].parse::<i32>().ok()?;
let symbol = format!("{}.{}", parts[..parts.len() - 2].join("_"), ext);
Some(DataFile {
path: path.to_path_buf(),
symbol,
year,
month,
})
}
pub async fn load_data<R>(&self, data_file: &DataFile) -> anyhow::Result<Arc<Vec<T>>>
where
R: crate::records::SourceRecord,
crate::records::DataPoint<R::Mapped>: Into<T>,
{
{
let mut cache = self.cache.write().unwrap();
if let Some(entry) = cache.get_mut(&data_file.path) {
entry.last_accessed = Instant::now();
return Ok(Arc::clone(&entry.data));
}
}
let path = data_file.path.clone();
let shared_data = tokio::task::spawn_blocking(move || {
let file = std::fs::File::open(&path)?;
let mut archive = ZipArchive::new(file)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
if file.name().ends_with(".bin") {
let mut bin_content = vec![0u8; file.size() as usize];
file.read_exact(&mut bin_content)?;
let raw_data: &[R] = cast_slice(&bin_content);
// Perform mapping inside blocking task
let mapped_data: Vec<T> =
raw_data.iter().map(|&r| r.to_data_point().into()).collect();
return Ok(Arc::new(mapped_data));
}
}
Err(anyhow::anyhow!("No .bin file found"))
})
.await??;
let mut cache = self.cache.write().unwrap();
cache.insert(
data_file.path.clone(),
CacheEntry {
data: Arc::clone(&shared_data),
last_accessed: Instant::now(),
},
);
Ok(shared_data)
}
// async fn read_from_zip - removed as it was unused in main.rs context or seemingly redundant/internal helper not used in extracted public interface?
// Checking main.rs: It was NOT used in main.rs, it was a private helper method inside impl DataServer.
// However, it wasn't used by load_data either. It seems dead code or future code.
// I will include it to be safe, but make it private or allow dead code if it warns.
// Actually, looking at main.rs again, read_from_zip was defined but NOT used in the provided snippet of DataServer.
// Wait, load_data implements similar logic inline.
// I will keep it to strictly follow "never change code unless..." (preserving the code structure).
#[allow(dead_code)]
async fn read_from_zip<R: bytemuck::Pod + Send + 'static>(
&self,
path: &Path,
) -> anyhow::Result<Vec<R>> {
let path = path.to_path_buf();
tokio::task::spawn_blocking(move || {
let file = std::fs::File::open(&path)?;
let mut archive = ZipArchive::new(file)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
if file.name().ends_with(".bin") {
let mut bin_content = vec![0u8; file.size() as usize];
file.read_exact(&mut bin_content)?;
let records: &[R] = cast_slice(&bin_content);
return Ok(records.to_vec());
}
}
Err(anyhow::anyhow!("No .bin file found"))
})
.await?
}
}
+6 -161
View File
@@ -1,7 +1,12 @@
mod dataserver;
mod records;
use crate::dataserver::DataServer;
use crate::records::{DataPoint, M1Record, OhlcItem, TickRecord};
use chrono::{DateTime, Utc};
use chrono::DateTime;
use eframe::egui;
use std::sync::Arc;
use tokio::runtime::Runtime;
fn format_ts(ts: f64) -> String {
// Delphi TDateTime: Days since 1899-12-30.
@@ -16,166 +21,6 @@ fn format_ts(ts: f64) -> String {
format!("{:.6}", ts)
}
}
use bytemuck::cast_slice;
use eframe::egui;
use std::collections::HashMap;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use std::time::Instant;
use tokio::runtime::Runtime;
use zip::ZipArchive;
// --- Data Structures ---
#[derive(Debug, Clone)]
pub struct DataFile {
pub path: PathBuf,
pub symbol: String,
pub year: i32,
pub month: i32,
}
struct CacheEntry<T> {
data: Arc<Vec<T>>,
last_accessed: Instant,
}
pub struct DataServer<T: bytemuck::Pod + Send + Sync + 'static> {
base_path: PathBuf,
symbols: RwLock<HashMap<String, Vec<DataFile>>>,
cache: RwLock<HashMap<PathBuf, CacheEntry<T>>>,
}
impl<T: bytemuck::Pod + Send + Sync + 'static> DataServer<T> {
pub fn new(path: impl AsRef<Path>) -> Self {
Self {
base_path: path.as_ref().to_path_buf(),
symbols: RwLock::new(HashMap::new()),
cache: RwLock::new(HashMap::new()),
}
}
pub async fn update_symbols(&self) -> tokio::io::Result<()> {
let mut entries = tokio::fs::read_dir(&self.base_path).await?;
let mut temp_map: HashMap<String, Vec<DataFile>> = HashMap::new();
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
if file_name.ends_with(".m1") || file_name.ends_with(".tick") {
if let Some(data_file) = self.parse_file_name(&path, file_name) {
temp_map
.entry(data_file.symbol.clone())
.or_default()
.push(data_file);
}
}
}
}
for files in temp_map.values_mut() {
files.sort_by(|a, b| a.year.cmp(&b.year).then(a.month.cmp(&b.month)));
}
let mut symbols = self.symbols.write().unwrap();
*symbols = temp_map;
Ok(())
}
pub fn enumerate_symbols(&self) -> Vec<String> {
let symbols = self.symbols.read().unwrap();
let mut keys: Vec<String> = symbols.keys().cloned().collect();
keys.sort();
keys
}
fn parse_file_name(&self, path: &Path, file_name: &str) -> Option<DataFile> {
let stem = Path::new(file_name).file_stem()?.to_str()?;
let ext = Path::new(file_name).extension()?.to_str()?;
let parts: Vec<&str> = stem.split('_').collect();
if parts.len() < 3 {
return None;
}
let month = parts.last()?.parse::<i32>().ok()?;
let year = parts[parts.len() - 2].parse::<i32>().ok()?;
let symbol = format!("{}.{}", parts[..parts.len() - 2].join("_"), ext);
Some(DataFile {
path: path.to_path_buf(),
symbol,
year,
month,
})
}
pub async fn load_data<R>(&self, data_file: &DataFile) -> anyhow::Result<Arc<Vec<T>>>
where
R: crate::records::SourceRecord,
crate::records::DataPoint<R::Mapped>: Into<T>,
{
{
let mut cache = self.cache.write().unwrap();
if let Some(entry) = cache.get_mut(&data_file.path) {
entry.last_accessed = Instant::now();
return Ok(Arc::clone(&entry.data));
}
}
let path = data_file.path.clone();
let shared_data = tokio::task::spawn_blocking(move || {
let file = std::fs::File::open(&path)?;
let mut archive = ZipArchive::new(file)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
if file.name().ends_with(".bin") {
let mut bin_content = vec![0u8; file.size() as usize];
file.read_exact(&mut bin_content)?;
let raw_data: &[R] = cast_slice(&bin_content);
// Perform mapping inside blocking task
let mapped_data: Vec<T> =
raw_data.iter().map(|&r| r.to_data_point().into()).collect();
return Ok(Arc::new(mapped_data));
}
}
Err(anyhow::anyhow!("No .bin file found"))
})
.await??;
let mut cache = self.cache.write().unwrap();
cache.insert(
data_file.path.clone(),
CacheEntry {
data: Arc::clone(&shared_data),
last_accessed: Instant::now(),
},
);
Ok(shared_data)
}
async fn read_from_zip<R: bytemuck::Pod + Send + 'static>(
&self,
path: &Path,
) -> anyhow::Result<Vec<R>> {
let path = path.to_path_buf();
tokio::task::spawn_blocking(move || {
let file = std::fs::File::open(&path)?;
let mut archive = ZipArchive::new(file)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
if file.name().ends_with(".bin") {
let mut bin_content = vec![0u8; file.size() as usize];
file.read_exact(&mut bin_content)?;
let records: &[R] = cast_slice(&bin_content);
return Ok(records.to_vec());
}
}
Err(anyhow::anyhow!("No .bin file found"))
})
.await?
}
}
// --- EGui App ---