Cache-first object fetching (#76)

* fix: submillisecond cache hits

* fix: async object loading to hand control back to renderer

* fix: clippy
This commit is contained in:
DecDuck
2025-07-27 12:04:50 +10:00
committed by GitHub
parent 682c6e9c0b
commit 739e6166c5
12 changed files with 110 additions and 101 deletions

View File

@ -212,7 +212,7 @@ pub fn setup() -> (AppStatus, Option<User>) {
let user_result = match fetch_user() {
Ok(data) => data,
Err(RemoteAccessError::FetchError(_)) => {
let user = get_cached_object::<_, User>("user").unwrap();
let user = get_cached_object::<User>("user").unwrap();
return (AppStatus::Offline, Some(user));
}
Err(_) => return (AppStatus::SignedInNeedsReauth, None),

View File

@ -1,6 +1,8 @@
use std::{
fmt::Display,
time::{Duration, SystemTime},
fs::File,
io::{self, Write},
path::{Path, PathBuf},
time::SystemTime,
};
use crate::{
@ -8,8 +10,8 @@ use crate::{
error::remote_access_error::RemoteAccessError,
};
use bitcode::{Decode, DecodeOwned, Encode};
use cacache::Integrity;
use http::{Response, header::CONTENT_TYPE, response::Builder as ResponseBuilder};
use log::debug;
#[macro_export]
macro_rules! offline {
@ -23,47 +25,67 @@ macro_rules! offline {
}
}
pub fn cache_object<K: AsRef<str>, D: Encode>(
key: K,
data: &D,
) -> Result<Integrity, RemoteAccessError> {
fn get_sys_time_in_secs() -> u64 {
match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => panic!("SystemTime before UNIX EPOCH!"),
}
}
fn get_cache_path(base: &Path, key: &str) -> PathBuf {
let key_hash = hex::encode(md5::compute(key.as_bytes()).0);
base.join(key_hash)
}
fn write_sync(base: &Path, key: &str, data: Vec<u8>) -> io::Result<()> {
let cache_path = get_cache_path(base, key);
let mut file = File::create(cache_path)?;
file.write_all(&data)?;
Ok(())
}
fn read_sync(base: &Path, key: &str) -> io::Result<Vec<u8>> {
let cache_path = get_cache_path(base, key);
let file = std::fs::read(cache_path)?;
Ok(file)
}
pub fn cache_object<D: Encode>(key: &str, data: &D) -> Result<(), RemoteAccessError> {
let bytes = bitcode::encode(data);
cacache::write_sync(&borrow_db_checked().cache_dir, key, bytes)
.map_err(RemoteAccessError::Cache)
write_sync(&borrow_db_checked().cache_dir, key, bytes).map_err(RemoteAccessError::Cache)
}
pub fn get_cached_object<K: AsRef<str> + Display, D: Encode + DecodeOwned>(
key: K,
) -> Result<D, RemoteAccessError> {
get_cached_object_db::<K, D>(key, &borrow_db_checked())
pub fn get_cached_object<D: Encode + DecodeOwned>(key: &str) -> Result<D, RemoteAccessError> {
get_cached_object_db::<D>(key, &borrow_db_checked())
}
pub fn get_cached_object_db<K: AsRef<str> + Display, D: DecodeOwned>(
key: K,
pub fn get_cached_object_db<D: DecodeOwned>(
key: &str,
db: &Database,
) -> Result<D, RemoteAccessError> {
let bytes = cacache::read_sync(&db.cache_dir, &key).map_err(RemoteAccessError::Cache)?;
let data = bitcode::decode::<D>(&bytes).map_err(|_| {
RemoteAccessError::Cache(cacache::Error::EntryNotFound(
db.cache_dir.clone(),
key.to_string(),
))
})?;
let start = SystemTime::now();
let bytes = read_sync(&db.cache_dir, key).map_err(RemoteAccessError::Cache)?;
let read = start.elapsed().unwrap();
let data =
bitcode::decode::<D>(&bytes).map_err(|e| RemoteAccessError::Cache(io::Error::other(e)))?;
let decode = start.elapsed().unwrap();
debug!(
"cache object took: r:{}, d:{}, b:{}",
read.as_millis(),
read.abs_diff(decode).as_millis(),
bytes.len()
);
Ok(data)
}
#[derive(Encode, Decode)]
pub struct ObjectCache {
content_type: String,
body: Vec<u8>,
expiry: u128,
expiry: u64,
}
impl ObjectCache {
pub fn has_expired(&self) -> bool {
let duration = Duration::from_millis(self.expiry.try_into().unwrap());
SystemTime::UNIX_EPOCH
.checked_add(duration)
.unwrap()
.elapsed()
.is_err()
let current = get_sys_time_in_secs();
self.expiry < current
}
}
@ -78,12 +100,7 @@ impl From<Response<Vec<u8>>> for ObjectCache {
.unwrap()
.to_owned(),
body: value.body().clone(),
expiry: SystemTime::now()
.checked_add(Duration::from_days(1))
.unwrap()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_millis(),
expiry: get_sys_time_in_secs() + 60 * 60 * 24,
}
}
}

View File

@ -55,7 +55,7 @@ pub fn fetch_drop_object(path: String) -> Result<Vec<u8>, RemoteAccessError> {
}
Err(e) => {
debug!("{e}");
get_cached_object::<&str, Vec<u8>>(&path)
get_cached_object::<Vec<u8>>(&path)
}
}
}

View File

@ -2,17 +2,18 @@ use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder};
use log::warn;
use tauri::UriSchemeResponder;
use crate::{DB, database::db::DatabaseImpls};
use super::{
auth::generate_authorization_header,
cache::{cache_object, get_cached_object, ObjectCache},
requests::make_request,
cache::{ObjectCache, cache_object, get_cached_object},
};
pub fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeResponder) {
pub async fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeResponder) {
// Drop leading /
let object_id = &request.uri().path()[1..];
let cache_result = get_cached_object::<&str, ObjectCache>(object_id);
let cache_result = get_cached_object::<ObjectCache>(object_id);
if let Ok(cache_result) = &cache_result
&& !cache_result.has_expired()
{
@ -21,12 +22,10 @@ pub fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeRespond
}
let header = generate_authorization_header();
let client: reqwest::blocking::Client = reqwest::blocking::Client::new();
let response = make_request(&client, &["/api/v1/client/object/", object_id], &[], |f| {
f.header("Authorization", header)
})
.unwrap()
.send();
let client = reqwest::Client::new();
let url = format!("{}api/v1/client/object/{object_id}", DB.fetch_base_url());
let response = client.get(url).header("Authorization", header).send().await;
if response.is_err() {
match cache_result {
Ok(cache_result) => responder.respond(cache_result.into()),
@ -42,20 +41,11 @@ pub fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeRespond
CONTENT_TYPE,
response.headers().get("Content-Type").unwrap(),
);
let data = Vec::from(response.bytes().unwrap());
let data = Vec::from(response.bytes().await.unwrap());
let resp = resp_builder.body(data).unwrap();
if cache_result.is_err() || cache_result.unwrap().has_expired() {
cache_object::<&str, ObjectCache>(object_id, &resp.clone().into()).unwrap();
cache_object::<ObjectCache>(object_id, &resp.clone().into()).unwrap();
}
responder.respond(resp);
}
pub fn fetch_object_offline(request: http::Request<Vec<u8>>, responder: UriSchemeResponder) {
let object_id = &request.uri().path()[1..];
let data = get_cached_object::<&str, ObjectCache>(object_id);
match data {
Ok(data) => responder.respond(data.into()),
Err(e) => warn!("{e}"),
}
}