fix: app cache and other issues (#5460)

* fixes

* #[serde(untagged)] my BEHATED (still kinda broken)

* remove unused hasContent ref

* clean up code in fetch instance

* ping 3 times for average latency

* fix: pinging to be more accurate

TCP_NODELAY — Set on the TCP stream right after connect, preventing Nagle's algorithm from buffering the small ping packet (could save up to ~40ms)

Instant over Utc::now() — Switched to monotonic std::time::Instant for timing, which is more precise and designed for measuring elapsed time (still using chrono just for the ping magic value)

* delete useFetch util and just use native fetch

* rename worlds until functions for more clarity

* fix lint

* fix cache.rs logic

* make backend ping use both impls

* Add optional timeout to server ping

* fix gallery appearing in nav with no items

* remove EU countries and add EU option for server country

* add uk to europe

---------

Co-authored-by: aecsocket <aecsocket@tutanota.com>
This commit is contained in:
Truman Gao
2026-03-03 10:41:12 -08:00
committed by GitHub
parent 211ec20970
commit 0029a22569
17 changed files with 468 additions and 272 deletions

View File

@@ -15,7 +15,7 @@ use std::path::{Path, PathBuf};
// 1 day
const DEFAULT_ID: &str = "0";
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CacheValueType {
Project,
@@ -131,37 +131,41 @@ impl CacheValueType {
}
}
// De/serialization strategy:
// - on serialize:
// - in the `cache` table, save the `data_type` (variant of this value) alongside
// the data
// - data column contains the serialized form of the INNER value (i.e. for a
// `CacheValue::Project`, we serialize it as a `Project,` NOT as a `CacheValue`)
// - this way, we do not tag the data using serde in any way
// - on deserialize:
// - use the `data_type` to figure out what type of value to deser as
// - then wrap that in a `CacheValue`
//
// do NOT use `#[serde(untagged)]` here, since then a value of one variant can be
// deser'd as a value of another variant, if it comes before it in the enum
// definition list.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum CacheValue {
Project(Project),
ProjectV3(ProjectV3),
Version(Version),
User(User),
Team(Vec<TeamMember>),
Organization(Organization),
File(CachedFile),
LoaderManifest(CachedLoaderManifest),
MinecraftManifest(daedalus::minecraft::VersionManifest),
Categories(Vec<Category>),
ReportTypes(Vec<String>),
Loaders(Vec<Loader>),
GameVersions(Vec<GameVersion>),
DonationPlatforms(Vec<DonationPlatform>),
FileHash(CachedFileHash),
FileUpdate(CachedFileUpdate),
SearchResults(SearchResults),
SearchResultsV3(SearchResultsV3),
ProjectV3(ProjectV3),
}
#[derive(Serialize, Deserialize, Clone, Debug)]
@@ -566,6 +570,47 @@ impl CacheValue {
| CacheValue::SearchResultsV3(_) => None,
}
}
fn to_json_value(&self) -> crate::Result<serde_json::Value> {
let value = match self {
CacheValue::Project(project) => serde_json::to_value(project),
CacheValue::ProjectV3(project) => serde_json::to_value(project),
CacheValue::Version(version) => serde_json::to_value(version),
CacheValue::User(user) => serde_json::to_value(user),
CacheValue::Team(members) => serde_json::to_value(members),
CacheValue::Organization(org) => serde_json::to_value(org),
CacheValue::File(file) => serde_json::to_value(file),
CacheValue::LoaderManifest(loader) => serde_json::to_value(loader),
CacheValue::MinecraftManifest(manifest) => {
serde_json::to_value(manifest)
}
CacheValue::Categories(categories) => {
serde_json::to_value(categories)
}
CacheValue::ReportTypes(report_types) => {
serde_json::to_value(report_types)
}
CacheValue::Loaders(loaders) => serde_json::to_value(loaders),
CacheValue::GameVersions(versions) => {
serde_json::to_value(versions)
}
CacheValue::DonationPlatforms(platforms) => {
serde_json::to_value(platforms)
}
CacheValue::FileHash(hash) => serde_json::to_value(hash),
CacheValue::FileUpdate(update) => serde_json::to_value(update),
CacheValue::SearchResults(search) => serde_json::to_value(search),
CacheValue::SearchResultsV3(search) => serde_json::to_value(search),
}
.map_err(|err| {
crate::ErrorKind::OtherError(format!(
"Failed to serialize cache value: {err}"
))
.as_error()
})?;
Ok(value)
}
}
#[derive(
@@ -759,15 +804,11 @@ impl CachedEntry {
.await?;
for row in query {
let row_exists = row.data.is_some();
let parsed_data = row
.data
.and_then(|x| serde_json::from_value::<CacheValue>(x).ok());
// If data is corrupted/failed to parse ignore it
if row_exists && parsed_data.is_none() {
continue;
}
let parsed_data = if let Some(data) = row.data.clone() {
Some(Self::deserialize_cache_value(type_, data, &row.id)?)
} else {
None
};
if row.expires <= Utc::now().timestamp() {
if cache_behaviour == CacheBehaviour::MustRevalidate {
@@ -789,6 +830,16 @@ impl CachedEntry {
});
if let Some(data) = parsed_data {
if data.get_type() != type_ {
return Err(crate::ErrorKind::OtherError(format!(
"Cache type mismatch for id {}: expected {:?}, got {:?}",
row.id,
type_,
data.get_type()
))
.as_error());
}
return_vals.push(Self {
id: row.id,
alias: row.alias,
@@ -1509,11 +1560,102 @@ impl CachedEntry {
})
}
fn deserialize_cache_value(
type_: CacheValueType,
data: serde_json::Value,
id: &str,
) -> crate::Result<CacheValue> {
fn parse<T: DeserializeOwned>(
data: serde_json::Value,
id: &str,
label: &str,
) -> crate::Result<T> {
serde_json::from_value::<T>(data.clone()).map_err(|err| {
crate::ErrorKind::OtherError(format!(
"Failed to deserialize cache {label} for id {id}: {err}\n\ndata:\n{}",
serde_json::to_string_pretty(&data).unwrap(),
))
.as_error()
})
}
let value = match type_ {
CacheValueType::Project => {
CacheValue::Project(parse(data, id, "project")?)
}
CacheValueType::ProjectV3 => {
CacheValue::ProjectV3(parse(data, id, "project_v3")?)
}
CacheValueType::Version => {
CacheValue::Version(parse(data, id, "version")?)
}
CacheValueType::User => CacheValue::User(parse(data, id, "user")?),
CacheValueType::Team => CacheValue::Team(parse(data, id, "team")?),
CacheValueType::Organization => {
CacheValue::Organization(parse(data, id, "organization")?)
}
CacheValueType::File => CacheValue::File(parse(data, id, "file")?),
CacheValueType::LoaderManifest => {
CacheValue::LoaderManifest(parse(data, id, "loader_manifest")?)
}
CacheValueType::MinecraftManifest => CacheValue::MinecraftManifest(
parse(data, id, "minecraft_manifest")?,
),
CacheValueType::Categories => {
CacheValue::Categories(parse(data, id, "categories")?)
}
CacheValueType::ReportTypes => {
CacheValue::ReportTypes(parse(data, id, "report_types")?)
}
CacheValueType::Loaders => {
CacheValue::Loaders(parse(data, id, "loaders")?)
}
CacheValueType::GameVersions => {
CacheValue::GameVersions(parse(data, id, "game_versions")?)
}
CacheValueType::DonationPlatforms => CacheValue::DonationPlatforms(
parse(data, id, "donation_platforms")?,
),
CacheValueType::FileHash => {
CacheValue::FileHash(parse(data, id, "file_hash")?)
}
CacheValueType::FileUpdate => {
CacheValue::FileUpdate(parse(data, id, "file_update")?)
}
CacheValueType::SearchResults => {
CacheValue::SearchResults(parse(data, id, "search_results")?)
}
CacheValueType::SearchResultsV3 => CacheValue::SearchResultsV3(
parse(data, id, "search_results_v3")?,
),
};
Ok(value)
}
pub(crate) async fn upsert_many(
items: &[Self],
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
) -> crate::Result<()> {
let items = serde_json::to_string(items)?;
let items = items
.iter()
.map(|item| {
let data = item
.data
.as_ref()
.map(|value| value.to_json_value())
.transpose()?;
Ok(serde_json::json!({
"id": item.id,
"data_type": item.type_.as_str(),
"alias": item.alias,
"data": data,
"expires": item.expires,
}))
})
.collect::<crate::Result<Vec<_>>>()?;
let items = serde_json::to_string(&items)?;
sqlx::query!(
"