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

185
Cargo.lock generated
View File

@@ -2147,7 +2147,7 @@ dependencies = [
"futures",
"indexmap 2.11.4",
"itertools 0.14.0",
"reqwest",
"reqwest 0.12.24",
"rust-s3",
"serde",
"serde-xml-rs",
@@ -4189,9 +4189,9 @@ dependencies = [
[[package]]
name = "ico"
version = "0.4.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98"
checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371"
dependencies = [
"byteorder",
"png 0.17.16",
@@ -4682,9 +4682,9 @@ dependencies = [
[[package]]
name = "js-sys"
version = "0.3.81"
version = "0.3.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -4850,7 +4850,7 @@ dependencies = [
"rand_chacha 0.3.1",
"redis",
"regex",
"reqwest",
"reqwest 0.12.24",
"rust-s3",
"rust_decimal",
"rust_iso3166",
@@ -5305,7 +5305,7 @@ dependencies = [
"log",
"meilisearch-index-setting-macro",
"pin-project-lite",
"reqwest",
"reqwest 0.12.24",
"serde",
"serde_json",
"thiserror 2.0.17",
@@ -5414,7 +5414,7 @@ dependencies = [
"flate2",
"maxminddb",
"modrinth-util",
"reqwest",
"reqwest 0.12.24",
"tar",
"tokio",
"tracing",
@@ -5493,7 +5493,7 @@ dependencies = [
"bytes",
"chrono",
"derive_more 2.0.1",
"reqwest",
"reqwest 0.12.24",
"rust_decimal",
"rust_iso3166",
"secrecy",
@@ -6247,7 +6247,7 @@ dependencies = [
"bytes",
"http 1.3.1",
"opentelemetry",
"reqwest",
"reqwest 0.12.24",
]
[[package]]
@@ -6262,7 +6262,7 @@ dependencies = [
"opentelemetry-proto",
"opentelemetry_sdk",
"prost 0.13.5",
"reqwest",
"reqwest 0.12.24",
"thiserror 2.0.17",
"tokio",
"tonic 0.13.1",
@@ -7703,11 +7703,45 @@ dependencies = [
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams",
"wasm-streams 0.4.2",
"web-sys",
"webpki-roots 1.0.3",
]
[[package]]
name = "reqwest"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-core",
"futures-util",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"hyper 1.7.0",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"serde",
"serde_json",
"sync_wrapper",
"tokio",
"tokio-util",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams 0.5.0",
"web-sys",
]
[[package]]
name = "resolv-conf"
version = "0.7.5"
@@ -7893,7 +7927,7 @@ dependencies = [
"minidom",
"percent-encoding",
"quick-xml 0.38.3",
"reqwest",
"reqwest 0.12.24",
"serde",
"serde_derive",
"serde_json",
@@ -8361,7 +8395,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48b85e25e8a1fc13928885e8bf13abe8a09e15c46993aed05d6405f7755d6e20"
dependencies = [
"httpdate",
"reqwest",
"reqwest 0.12.24",
"rustls 0.23.32",
"sentry-backtrace",
"sentry-contexts",
@@ -9464,9 +9498,9 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]]
name = "tao"
version = "0.34.3"
version = "0.34.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "959469667dbcea91e5485fc48ba7dd6023face91bb0f1a14681a70f99847c3f7"
checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7"
dependencies = [
"bitflags 2.9.4",
"block2 0.6.2",
@@ -9538,9 +9572,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tauri"
version = "2.8.5"
version = "2.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d1d3b3dc4c101ac989fd7db77e045cc6d91a25349cd410455cb5c57d510c1c"
checksum = "463ae8677aa6d0f063a900b9c41ecd4ac2b7ca82f0b058cc4491540e55b20129"
dependencies = [
"anyhow",
"bytes",
@@ -9567,7 +9601,7 @@ dependencies = [
"percent-encoding",
"plist",
"raw-window-handle",
"reqwest",
"reqwest 0.13.2",
"serde",
"serde_json",
"serde_repr",
@@ -9582,7 +9616,6 @@ dependencies = [
"tokio",
"tray-icon",
"url",
"urlpattern",
"webkit2gtk",
"webview2-com",
"window-vibrancy",
@@ -9591,9 +9624,9 @@ dependencies = [
[[package]]
name = "tauri-build"
version = "2.4.1"
version = "2.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c432ccc9ff661803dab74c6cd78de11026a578a9307610bbc39d3c55be7943f"
checksum = "ca7bd893329425df750813e95bd2b643d5369d929438da96d5bbb7cc2c918f74"
dependencies = [
"anyhow",
"cargo_toml",
@@ -9615,9 +9648,9 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "2.4.0"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ab3a62cf2e6253936a8b267c2e95839674e7439f104fa96ad0025e149d54d8a"
checksum = "aac423e5859d9f9ccdd32e3cf6a5866a15bedbf25aa6630bcb2acde9468f6ae3"
dependencies = [
"base64 0.22.1",
"brotli",
@@ -9642,9 +9675,9 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "2.4.0"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4368ea8094e7045217edb690f493b55b30caf9f3e61f79b4c24b6db91f07995e"
checksum = "1b6a1bd2861ff0c8766b1d38b32a6a410f6dc6532d4ef534c47cfb2236092f59"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -9656,9 +9689,9 @@ dependencies = [
[[package]]
name = "tauri-plugin"
version = "2.4.0"
version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9946a3cede302eac0c6eb6c6070ac47b1768e326092d32efbb91f21ed58d978f"
checksum = "692a77abd8b8773e107a42ec0e05b767b8d2b7ece76ab36c6c3947e34df9f53f"
dependencies = [
"anyhow",
"glob",
@@ -9712,9 +9745,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-fs"
version = "2.4.2"
version = "2.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "315784ec4be45e90a987687bae7235e6be3d6e9e350d2b75c16b8a4bf22c1db7"
checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804"
dependencies = [
"anyhow",
"dunce",
@@ -9734,16 +9767,16 @@ dependencies = [
[[package]]
name = "tauri-plugin-http"
version = "2.5.2"
version = "2.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "938a3d7051c9a82b431e3a0f3468f85715b3442b3c3a3913095e9fa509e2652c"
checksum = "d8f069451c4e87e7e2636b7f065a4c52866c4ce5e60e2d53fa1038edb6d184dc"
dependencies = [
"bytes",
"cookie_store",
"data-url",
"http 1.3.1",
"regex",
"reqwest",
"reqwest 0.12.24",
"schemars 0.8.22",
"serde",
"serde_json",
@@ -9827,7 +9860,7 @@ dependencies = [
"minisign-verify",
"osakit",
"percent-encoding",
"reqwest",
"reqwest 0.12.24",
"semver",
"serde",
"serde_json",
@@ -9860,9 +9893,9 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "2.8.0"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4cfc9ad45b487d3fded5a4731a567872a4812e9552e3964161b08edabf93846"
checksum = "b885ffeac82b00f1f6fd292b6e5aabfa7435d537cef57d11e38a489956535651"
dependencies = [
"cookie 0.18.1",
"dpi",
@@ -9885,9 +9918,9 @@ dependencies = [
[[package]]
name = "tauri-runtime-wry"
version = "2.8.1"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1fe9d48bd122ff002064e88cfcd7027090d789c4302714e68fcccba0f4b7807"
checksum = "5204682391625e867d16584fedc83fc292fb998814c9f7918605c789cd876314"
dependencies = [
"gtk",
"http 1.3.1",
@@ -9912,9 +9945,9 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "2.7.0"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41a3852fdf9a4f8fbeaa63dc3e9a85284dd6ef7200751f0bd66ceee30c93f212"
checksum = "fcd169fccdff05eff2c1033210b9b94acd07a47e6fa9a3431cf09cfd4f01c87e"
dependencies = [
"anyhow",
"brotli",
@@ -10068,7 +10101,7 @@ dependencies = [
"quick-xml 0.38.3",
"rand 0.8.5",
"regex",
"reqwest",
"reqwest 0.12.24",
"rgb",
"serde",
"serde_ini",
@@ -10589,9 +10622,9 @@ dependencies = [
[[package]]
name = "tower-http"
version = "0.6.6"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
"bitflags 2.9.4",
"bytes",
@@ -11319,9 +11352,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
version = "0.2.104"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
dependencies = [
"cfg-if",
"once_cell",
@@ -11330,27 +11363,14 @@ dependencies = [
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn 2.0.106",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.54"
version = "0.4.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c"
checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8"
dependencies = [
"cfg-if",
"futures-util",
"js-sys",
"once_cell",
"wasm-bindgen",
@@ -11359,9 +11379,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.104"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -11369,22 +11389,22 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.104"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 2.0.106",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.104"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
dependencies = [
"unicode-ident",
]
@@ -11402,6 +11422,19 @@ dependencies = [
"web-sys",
]
[[package]]
name = "wasm-streams"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
dependencies = [
"futures-util",
"js-sys",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "wayland-backend"
version = "0.3.11"
@@ -11464,9 +11497,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.81"
version = "0.3.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120"
checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -11484,9 +11517,9 @@ dependencies = [
[[package]]
name = "webkit2gtk"
version = "2.0.1"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a"
checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793"
dependencies = [
"bitflags 1.3.2",
"cairo-rs",
@@ -11508,9 +11541,9 @@ dependencies = [
[[package]]
name = "webkit2gtk-sys"
version = "2.0.1"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c"
checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5"
dependencies = [
"bitflags 1.3.2",
"cairo-sys-rs",
@@ -12213,9 +12246,9 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
[[package]]
name = "wry"
version = "0.53.4"
version = "0.54.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d78ec082b80fa088569a970d043bb3050abaabf4454101d44514ee8d9a8c9f6"
checksum = "bb26159b420aa77684589a744ae9a9461a95395b848764ad12290a14d960a11a"
dependencies = [
"base64 0.22.1",
"block2 0.6.2",

View File

@@ -179,7 +179,7 @@ tauri = "2.8.5"
tauri-build = "2.4.1"
tauri-plugin-deep-link = "2.4.3"
tauri-plugin-dialog = "2.4.0"
tauri-plugin-http = "2.5.2"
tauri-plugin-http = "2.5.7"
tauri-plugin-opener = "2.5.0"
tauri-plugin-os = "2.3.1"
tauri-plugin-single-instance = "2.3.4"

View File

@@ -85,7 +85,6 @@ import { debugAnalytics, initAnalytics, trackEvent } from '@/helpers/analytics'
import { check_reachable } from '@/helpers/auth.js'
import { get_user } from '@/helpers/cache.js'
import { command_listener, warning_listener } from '@/helpers/events.js'
import { useFetch } from '@/helpers/fetch.js'
import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.ts'
import { list } from '@/helpers/profile.js'
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
@@ -303,11 +302,7 @@ async function setupApp() {
}),
)
useFetch(
`https://api.modrinth.com/appCriticalAnnouncement.json?version=${version}`,
'criticalAnnouncements',
true,
)
fetch(`https://api.modrinth.com/appCriticalAnnouncement.json?version=${version}`)
.then((response) => response.json())
.then((res) => {
if (res && res.header && res.body) {
@@ -320,23 +315,21 @@ async function setupApp() {
)
})
useFetch(`https://modrinth.com/news/feed/articles.json`, 'news', true)
fetch(`https://modrinth.com/news/feed/articles.json`)
.then((response) => response.json())
.then((res) => {
if (res && res.articles) {
// Format expected by NewsArticleCard component.
news.value = res.articles
.map((article) => ({
...article,
path: article.link,
thumbnail: article.thumbnail,
title: article.title,
summary: article.summary,
date: article.date,
}))
.slice(0, 4)
}
})
.catch((error) => {
console.error('Failed to fetch news articles', error)
})
get_opening_command().then(handleCommand)
fetchCredentials()

View File

@@ -33,6 +33,7 @@ async function purgeCache() {
'user',
'team',
'organization',
'file',
'loader_manifest',
'minecraft_manifest',
'categories',
@@ -40,8 +41,10 @@ async function purgeCache() {
'loaders',
'game_versions',
'donation_platforms',
'file_hash',
'file_update',
'search_results',
'search_results_v3',
]).catch(handleError)
}

View File

@@ -1,18 +0,0 @@
import { getVersion } from '@tauri-apps/api/app'
import { fetch } from '@tauri-apps/plugin-http'
export const useFetch = async (url, item, isSilent) => {
try {
const version = await getVersion()
return await fetch(url, {
method: 'GET',
headers: { 'User-Agent': `modrinth/theseus/${version} (support@modrinth.com)` },
})
} catch (err) {
if (!isSilent) {
throw err
} else {
console.error(err)
}
}
}

View File

@@ -231,6 +231,25 @@ export function isLinkedWorld(world: World): boolean {
return world.type === 'server' && !!world.linked_project_id
}
export async function getServerLatency(
address: string,
protocolVersion: ProtocolVersion | null = null,
): Promise<number | undefined> {
const pings: number[] = []
for (let i = 0; i < 3; i++) {
try {
const status = await get_server_status(address, protocolVersion)
if (status.ping != null) {
pings.push(status.ping)
}
} catch {
// Ignore individual ping failures
}
}
if (pings.length === 0) return undefined
return Math.round(pings.reduce((sum, p) => sum + p, 0) / pings.length)
}
export async function refreshServerData(
serverData: ServerData,
protocolVersion: ProtocolVersion | null,

View File

@@ -49,7 +49,7 @@ import {
} from '@/helpers/profile.js'
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
import type { GameInstance } from '@/helpers/types'
import { get_server_status } from '@/helpers/worlds'
import { getServerLatency } from '@/helpers/worlds'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { getServerAddress, playServerProject, useInstall } from '@/store/install.js'
@@ -287,17 +287,18 @@ const {
} = useServerSearch({ tags, query, maxResults, currentPage })
async function pingServerHits(hits: Labrinth.Search.v3.ResultSearchProject[]) {
for (const hit of hits) {
const address = hit.minecraft_java_server?.address
if (!address) continue
get_server_status(address)
.then((status) => {
serverPings.value = { ...serverPings.value, [hit.project_id]: status.ping }
})
.catch((err) => {
const pingsToFetch = hits.filter((hit) => hit.minecraft_java_server?.address)
await Promise.all(
pingsToFetch.map(async (hit) => {
const address = hit.minecraft_java_server!.address!
try {
const latency = await getServerLatency(address)
serverPings.value = { ...serverPings.value, [hit.project_id]: latency }
} catch (err) {
console.error(`Failed to ping server ${address}:`, err)
})
}
}
}),
)
}
const previousFilterState = ref('')

View File

@@ -67,7 +67,7 @@
<ServerPing v-if="ping" :ping="ping" />
<div
v-if="modpackContentProjectV3 && (minecraftServer?.country || ping)"
v-if="minecraftServer?.country || ping"
class="w-1.5 h-1.5 rounded-full bg-surface-5"
></div>
@@ -312,13 +312,13 @@ import InstanceSettingsModal from '@/components/ui/modal/InstanceSettingsModal.v
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
import NavTabs from '@/components/ui/NavTabs.vue'
import { trackEvent } from '@/helpers/analytics'
import { get_project_v3, get_version, get_version_many } from '@/helpers/cache.js'
import { get_project_v3, get_version_many } from '@/helpers/cache.js'
import { process_listener, profile_listener } from '@/helpers/events'
import { get_by_profile_path } from '@/helpers/process'
import { finish_install, get, get_full_path, get_projects, kill, run } from '@/helpers/profile'
import { finish_install, get, get_full_path, kill, run } from '@/helpers/profile'
import type { GameInstance } from '@/helpers/types'
import { showProfileInFolder } from '@/helpers/utils.js'
import { get_server_status } from '@/helpers/worlds'
import { get_server_status, getServerLatency } from '@/helpers/worlds'
import { handleSevereError } from '@/store/error.js'
import { playServerProject } from '@/store/install.js'
import { useBreadcrumbs, useLoading } from '@/store/state'
@@ -348,9 +348,7 @@ const exportModal = ref<InstanceType<typeof ExportModal>>()
const updateToPlayModal = ref<InstanceType<typeof UpdateToPlayModal>>()
const isServerInstance = ref(false)
const hasContent = ref(true)
const linkedProjectV3 = ref<Labrinth.Projects.v3.Project>()
const modpackContentProjectV3 = ref<Labrinth.Projects.v3.Project | null>(null)
const selected = ref<unknown[]>([])
const minecraftServer = computed(() => linkedProjectV3.value?.minecraft_server)
@@ -362,9 +360,7 @@ const ping = ref<number | undefined>(undefined)
async function fetchInstance() {
isServerInstance.value = false
linkedProjectV3.value = undefined
modpackContentProjectV3.value = null
modrinthVersions.value = []
hasContent.value = true
ping.value = undefined
instance.value = await get(route.params.id as string).catch(handleError)
@@ -382,48 +378,31 @@ async function fetchInstance() {
(a: Labrinth.Versions.v2.Version, b: Labrinth.Versions.v2.Version) =>
dayjs(b.date_published).valueOf() - dayjs(a.date_published).valueOf(),
)
if (linkedProjectV3.value?.minecraft_server != null) {
isServerInstance.value = true
}
const serverAddress = linkedProjectV3.value?.minecraft_java_server?.address
if (serverAddress) {
get_server_status(serverAddress)
.then((status) => {
if (status.ping != null) {
ping.value = status.ping
playersOnline.value = status.players?.online
}
})
.catch((err) => {
console.error(`Failed to ping server ${serverAddress}:`, err)
})
if (linkedProjectV3.value?.minecraft_server != null) {
isServerInstance.value = true
const serverAddress = linkedProjectV3.value?.minecraft_java_server?.address
if (serverAddress) {
try {
const status = await get_server_status(serverAddress)
const latency = await getServerLatency(serverAddress)
ping.value = latency
playersOnline.value = status.players?.online
} catch (err) {
console.error(`Failed to ping server ${serverAddress}:`, err)
}
await fetchModpackContent()
const projects = await get_projects(instance.value!.path).catch(() => ({}))
hasContent.value = Object.keys(projects).length > 0
}
}
} catch (error: Error) {
handleError(error)
} catch (error) {
handleError(error as Error)
}
}
await updatePlayState()
}
async function fetchModpackContent() {
modpackContentProjectV3.value = null
const versionId = instance.value?.linked_data?.version_id
if (!versionId) return
const contentVersion = await get_version(versionId, 'must_revalidate')
const projectId = contentVersion?.project_id
if (projectId) {
modpackContentProjectV3.value = await get_project_v3(projectId, 'must_revalidate')
}
}
async function updatePlayState() {
const runningProcesses = await get_by_profile_path(route.params.id as string).catch(handleError)

View File

@@ -265,7 +265,7 @@ import {
list as listInstances,
} from '@/helpers/profile'
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
import { get_server_status } from '@/helpers/worlds'
import { getServerLatency } from '@/helpers/worlds'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import {
getServerAddress,
@@ -398,15 +398,11 @@ async function fetchProjectData() {
serverStatusOnline.value = !!projectV3.value?.minecraft_java_server?.ping?.data
if (serverAddress) {
serverPing.value = undefined
get_server_status(serverAddress)
.then((status) => {
if (status.ping != null) {
serverPing.value = status.ping
}
})
.catch((err) => {
console.error(`Failed to ping server ${serverAddress}:`, err)
})
try {
serverPing.value = await getServerLatency(serverAddress)
} catch (error) {
console.error(`Failed to ping server ${serverAddress}:`, error)
}
}
// Fetch server sidebar data (modpack version + project)

View File

@@ -328,7 +328,7 @@ export const installServerProject = async (serverProjectId) => {
})
await edit_icon(profilePath, originalIconPath)
await syncServerAsWorld(profilePath, project.title, serverAddress, serverProjectId)
await syncServerProjectAsWorld(profilePath, project.title, serverAddress, serverProjectId)
}
export const getServerAddress = (javaServer) => {
@@ -337,7 +337,7 @@ export const getServerAddress = (javaServer) => {
return port !== 25565 ? `${address}:${port}` : address
}
const syncServerAsWorld = async (
const syncServerProjectAsWorld = async (
profilePath,
serverName,
serverAddress,
@@ -405,7 +405,7 @@ const findInstalledInstance = async (projectId) => {
return packs.find((pack) => pack.linked_data?.project_id === projectId) ?? null
}
const createVanillaInstance = async (project, gameVersion, serverAddress) => {
const createVanillaServerInstance = async (project, gameVersion, serverAddress) => {
const profilePath = await create(
project.title,
gameVersion,
@@ -420,7 +420,8 @@ const createVanillaInstance = async (project, gameVersion, serverAddress) => {
},
)
await syncServerAsWorld(profilePath, project.title, serverAddress, project.id)
//
await syncServerProjectAsWorld(profilePath, project.title, serverAddress, project.id)
return profilePath
}
@@ -514,6 +515,7 @@ export const playServerProject = async (projectId) => {
if (projectV3?.minecraft_server == null) {
console.warn('playServerProject failed: project is not a server project')
return
}
const content = projectV3?.minecraft_java_server?.content
@@ -529,7 +531,7 @@ export const playServerProject = async (projectId) => {
if (installStore.installingServerProjects.includes(projectId)) return
installStore.startInstallingServer(projectId)
try {
const path = await createVanillaInstance(project, recommendedGameVersion, serverAddress)
const path = await createVanillaServerInstance(project, recommendedGameVersion, serverAddress)
if (path) {
instance = await get(path)
showModpackInstallSuccess(installStore, instance, serverAddress)
@@ -543,8 +545,6 @@ export const playServerProject = async (projectId) => {
installStore.showInstallToPlayModal(projectV3, modpackVersionId, async () => {
const newInstance = await findInstalledInstance(project.id)
if (!newInstance) return
// Ensure the server is in the worlds list after modpack install
await syncServerAsWorld(newInstance.path, project.title, serverAddress, project.id)
showModpackInstallSuccess(installStore, newInstance, serverAddress)
})
return
@@ -552,7 +552,7 @@ export const playServerProject = async (projectId) => {
if (!instance) return
await syncServerAsWorld(instance.path, project.title, serverAddress, project.id)
await syncServerProjectAsWorld(instance.path, project.title, serverAddress, project.id)
// Update existing instance if needed
if (isModpack && instance.linked_data?.version_id !== modpackVersionId) {

View File

@@ -2523,6 +2523,13 @@ const navLinks = computed(() => {
const routeType = route.params.type || project.value.project_type
const projectUrl = `/${routeType}/${project.value.slug ? project.value.slug : project.value.id}`
const galleryCount =
routeType === 'server'
? project.value.gallery.filter((item) => item.name === '__mc_server_banner__').length
: project.value.gallery.length
console.log('galleryCount', galleryCount, !!currentMember.value)
return [
{
label: formatMessage(messages.descriptionTab),
@@ -2531,7 +2538,7 @@ const navLinks = computed(() => {
{
label: formatMessage(messages.galleryTab),
href: `${projectUrl}/gallery`,
shown: project.value.gallery.length > 0 || !!currentMember.value,
shown: galleryCount > 0 || !!currentMember.value,
},
{
label: formatMessage(messages.changelogTab),

View File

@@ -281,35 +281,46 @@ if (projectV3.value) {
const countryOptions = [
{ value: 'US', label: 'United States' },
{ value: 'CA', label: 'Canada' },
{ value: 'GB', label: 'United Kingdom' },
{ value: 'DE', label: 'Germany' },
{ value: 'FR', label: 'France' },
{ value: 'NL', label: 'Netherlands' },
{ value: 'FI', label: 'Finland' },
{ value: 'SE', label: 'Sweden' },
{
value: 'EU',
label: 'Europe',
searchTerms: [
'Germany',
'France',
'Netherlands',
'Finland',
'Sweden',
'Denmark',
'Poland',
'Czech Republic',
'Romania',
'Austria',
'Belgium',
'Ireland',
'Spain',
'Italy',
'Portugal',
'Lithuania',
'Latvia',
'Estonia',
'Bulgaria',
'Croatia',
'Hungary',
'Slovakia',
'Greece',
'Luxembourg',
'Malta',
'Cyprus',
'Slovenia',
'Great Britain',
'United Kingdom',
],
},
{ value: 'NO', label: 'Norway' },
{ value: 'DK', label: 'Denmark' },
{ value: 'PL', label: 'Poland' },
{ value: 'CZ', label: 'Czech Republic' },
{ value: 'RO', label: 'Romania' },
{ value: 'CH', label: 'Switzerland' },
{ value: 'AT', label: 'Austria' },
{ value: 'BE', label: 'Belgium' },
{ value: 'IE', label: 'Ireland' },
{ value: 'ES', label: 'Spain' },
{ value: 'IT', label: 'Italy' },
{ value: 'PT', label: 'Portugal' },
{ value: 'RU', label: 'Russia' },
{ value: 'UA', label: 'Ukraine' },
{ value: 'LT', label: 'Lithuania' },
{ value: 'LV', label: 'Latvia' },
{ value: 'EE', label: 'Estonia' },
{ value: 'BG', label: 'Bulgaria' },
{ value: 'HR', label: 'Croatia' },
{ value: 'HU', label: 'Hungary' },
{ value: 'SK', label: 'Slovakia' },
{ value: 'RS', label: 'Serbia' },
{ value: 'GR', label: 'Greece' },
{ value: 'TR', label: 'Turkey' },
{ value: 'IL', label: 'Israel' },
{ value: 'AE', label: 'United Arab Emirates' },

View File

@@ -54,7 +54,7 @@ impl ServerPingQueue {
let mut retries = ENV.SERVER_PING_RETRIES;
let result = loop {
match ping_server(&address, port).await {
match ping_server(&address, port, None).await {
Ok(ping) => {
info!(?ping, "Received successful ping");
break Ok(ping);
@@ -252,39 +252,15 @@ impl ServerPingQueue {
pub async fn ping_server(
address: &str,
port: u16,
timeout: Option<Duration>,
) -> eyre::Result<exp::minecraft::JavaServerPingData> {
let start = Instant::now();
let timeout = Duration::from_millis(ENV.SERVER_PING_TIMEOUT_MS);
let default_duration = Duration::from_millis(ENV.SERVER_PING_TIMEOUT_MS);
let timeout = timeout
.map(|duration| duration.min(default_duration))
.unwrap_or(default_duration);
let task1 = async move {
let conn = async_minecraft_ping::ConnectionConfig::build(address)
.with_port(port)
.connect()
.await
.wrap_err("failed to connect to server")?;
let status = conn
.status()
.await
.wrap_err("failed to get server status")?
.status;
debug!("Successful ping with `async_minecraft_ping`");
eyre::Ok(exp::minecraft::JavaServerPingData {
latency: start.elapsed(),
version_name: status.version.name,
version_protocol: status.version.protocol,
description: match status.description {
ServerDescription::Plain(text)
| ServerDescription::Object { text } => text,
},
players_online: status.players.online,
players_max: status.players.max,
})
};
let task1 = tokio::time::timeout(timeout, task1);
let task2 = async move {
let task_ep = async move {
fn map_component(c: elytra_ping::parse::TextComponent) -> String {
match c {
elytra_ping::parse::TextComponent::Plain(t) => t,
@@ -303,7 +279,6 @@ pub async fn ping_server(
elytra_ping::ping_or_timeout((address.to_string(), port), timeout)
.await?;
debug!("Successful ping with `elytra_ping`");
eyre::Ok(exp::minecraft::JavaServerPingData {
latency,
version_name: result
@@ -326,15 +301,54 @@ pub async fn ping_server(
})
};
async move {
if let Ok(t) = task1
.await
.wrap_err("failed to ping with `async_minecraft_ping`")?
{
return Ok(t);
}
let task_amp = async move {
let task = async move {
let conn = async_minecraft_ping::ConnectionConfig::build(address)
.with_port(port)
.connect()
.await
.wrap_err("failed to connect to server")?;
task2.await.wrap_err("failed to ping with `elytra_ping`")
let status = conn
.status()
.await
.wrap_err("failed to get server status")?
.status;
eyre::Ok(exp::minecraft::JavaServerPingData {
latency: start.elapsed(),
version_name: status.version.name,
version_protocol: status.version.protocol,
description: match status.description {
ServerDescription::Plain(text)
| ServerDescription::Object { text } => text,
},
players_online: status.players.online,
players_max: status.players.max,
})
};
tokio::time::timeout(timeout, task)
.await
.map_err(eyre::Error::new)
.flatten()
};
async move {
let (result_ep, result_amp) = (task_ep.await, task_amp.await);
let result_ep = result_ep
.inspect(|_| debug!("Successful ping with `elytra_ping`"))
.inspect_err(|err| {
debug!("Failed to ping with `elytra_ping`: {err:#}")
});
let result_amp = result_amp
.inspect(|_| debug!("Successful ping with `async_minecraft_ping`"))
.inspect_err(|err| {
debug!("Failed to ping with `async_minecraft_ping`: {err:#}")
});
result_ep.or(result_amp)
}
.await
}
@@ -359,11 +373,20 @@ mod tests {
#[actix_rt::test]
async fn test_ping_server_success() {
let _status = ping_server("mc.hypixel.net", 25565).await.unwrap();
let _status = ping_server("mc.hypixel.net", 25565, None).await.unwrap();
}
#[actix_rt::test]
async fn test_ping_server_invalid_address() {
_ = ping_server("invalid.invalid", 25565).await.unwrap_err();
_ = ping_server("invalid.invalid", 25565, None)
.await
.unwrap_err();
}
#[actix_rt::test]
async fn test_ping_zero_timeout() {
_ = ping_server("hypixel.net", 25565, Some(Duration::ZERO))
.await
.unwrap_err();
}
}

View File

@@ -1,3 +1,5 @@
use std::time::Duration;
use actix_web::{HttpRequest, post, web};
use serde::{Deserialize, Serialize};
@@ -18,6 +20,7 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
pub struct PingRequest {
pub address: String,
pub port: u16,
pub timeout_ms: Option<u64>,
}
#[utoipa::path]
@@ -38,7 +41,8 @@ pub async fn ping_minecraft_java(
)
.await?;
server_ping::ping_server(&request.address, request.port)
let timeout = request.timeout_ms.map(Duration::from_millis);
server_ping::ping_server(&request.address, request.port, timeout)
.await
.wrap_request_err("failed to ping server")?;

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!(
"

View File

@@ -69,7 +69,7 @@ pub async fn get_server_status(
mod modern {
use super::ServerStatus;
use crate::ErrorKind;
use chrono::Utc;
use std::time::Instant;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpStream, ToSocketAddrs};
@@ -79,6 +79,7 @@ mod modern {
protocol_version: Option<u32>,
) -> crate::Result<ServerStatus> {
let mut stream = TcpStream::connect(address).await?;
stream.set_nodelay(true)?;
handshake(&mut stream, original_address, protocol_version).await?;
let mut result = status_body(&mut stream).await?;
result.ping = ping(&mut stream).await.ok();
@@ -155,9 +156,9 @@ mod modern {
}
async fn ping(stream: &mut TcpStream) -> crate::Result<i64> {
let start_time = Utc::now();
let ping_magic = start_time.timestamp_millis();
let ping_magic = chrono::Utc::now().timestamp_millis();
let start_time = Instant::now();
stream.write_all(&[0x09, 0x01]).await?;
stream.write_i64(ping_magic).await?;
stream.flush().await?;
@@ -172,8 +173,7 @@ mod modern {
.into());
}
let response_time = Utc::now();
Ok((response_time - start_time).num_milliseconds())
Ok(start_time.elapsed().as_millis() as i64)
}
mod varint {

View File

@@ -156,6 +156,7 @@ export interface ComboboxOption<T> {
href?: string
target?: string
action?: () => void
searchTerms?: string[]
}
const DROPDOWN_VIEWPORT_MARGIN = 8
@@ -272,7 +273,9 @@ const filteredOptions = computed(() => {
const query = searchQuery.value.toLowerCase()
return optionsWithKeys.value.filter((opt) => {
if (isDivider(opt)) return false
return opt.label.toLowerCase().includes(query)
if (opt.label.toLowerCase().includes(query)) return true
if (opt.searchTerms?.some((term) => term.toLowerCase().includes(query))) return true
return false
})
})