Skip to content

Commit f0ed2a7

Browse files
MarkShawn2020claude
andcommitted
chore: release v0.30.1
- Fix dev-mode "[TAURI] Couldn't find callback id" warning by deferring get_network_info to next macrotask + adding disk persistence for NETWORK_INFO_CACHE. - Don't record /annual-report-2025 as lastPath resume target. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent e237736 commit f0ed2a7

8 files changed

Lines changed: 156 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Changelog
22

3+
## 0.30.1
4+
5+
### Patch Changes
6+
7+
- 修复 dev 模式下偶发的 `[TAURI] Couldn't find callback id` 警告:
8+
9+
- StatusBar 的 `get_network_info` 调用从挂载即触发改为下一个宏任务,HMR full-reload 期间几乎不会再撞上 5 秒 in-flight invoke。
10+
- `NETWORK_INFO_CACHE` 增加磁盘持久化(`~/.lovstudio/lovcode/cache/network.json`),dev 重启 Rust 进程不再丢缓存。
11+
12+
附带修复:annual-report-2025 路由不再被记录为 lastPath 恢复目标。
13+
314
## 0.30.0
415

516
### Minor Changes

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "lovcode",
33
"private": true,
4-
"version": "0.30.0",
4+
"version": "0.30.1",
55
"type": "module",
66
"packageManager": "pnpm@10.18.1",
77
"scripts": {

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "lovcode"
3-
version = "0.30.0"
3+
version = "0.30.1"
44
description = "A Tauri App"
55
authors = ["you"]
66
edition = "2021"

src-tauri/src/lib.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9900,6 +9900,129 @@ fn is_local_fresh_for_remote(path: &std::path::Path, remote_updated_at: &str) ->
99009900
local_secs >= remote_dt.timestamp()
99019901
}
99029902

9903+
#[derive(Serialize, Deserialize, Clone, Debug)]
9904+
#[serde(rename_all = "camelCase")]
9905+
struct NetworkInfo {
9906+
region: String,
9907+
ip: String,
9908+
is_proxy: bool,
9909+
#[serde(skip_serializing_if = "Option::is_none")]
9910+
proxy_type: Option<String>,
9911+
}
9912+
9913+
static NETWORK_INFO_CACHE: LazyLock<Mutex<Option<(std::time::SystemTime, NetworkInfo)>>> =
9914+
LazyLock::new(|| Mutex::new(None));
9915+
9916+
const NETWORK_INFO_TTL: Duration = Duration::from_secs(3600);
9917+
9918+
fn network_info_cache_path() -> PathBuf {
9919+
dirs::home_dir()
9920+
.unwrap_or_else(|| PathBuf::from("."))
9921+
.join(".lovstudio/lovcode/cache/network.json")
9922+
}
9923+
9924+
#[derive(Serialize, Deserialize)]
9925+
struct NetworkInfoCacheFile {
9926+
fetched_at_unix: u64,
9927+
info: NetworkInfo,
9928+
}
9929+
9930+
fn read_network_info_cache_file() -> Option<(std::time::SystemTime, NetworkInfo)> {
9931+
let raw = std::fs::read(network_info_cache_path()).ok()?;
9932+
let parsed: NetworkInfoCacheFile = serde_json::from_slice(&raw).ok()?;
9933+
let fetched_at = std::time::UNIX_EPOCH + Duration::from_secs(parsed.fetched_at_unix);
9934+
Some((fetched_at, parsed.info))
9935+
}
9936+
9937+
fn write_network_info_cache_file(fetched_at: std::time::SystemTime, info: &NetworkInfo) {
9938+
let path = network_info_cache_path();
9939+
if let Some(parent) = path.parent() {
9940+
let _ = std::fs::create_dir_all(parent);
9941+
}
9942+
let unix = fetched_at
9943+
.duration_since(std::time::UNIX_EPOCH)
9944+
.map(|d| d.as_secs())
9945+
.unwrap_or(0);
9946+
let payload = NetworkInfoCacheFile { fetched_at_unix: unix, info: info.clone() };
9947+
if let Ok(bytes) = serde_json::to_vec(&payload) {
9948+
let _ = std::fs::write(&path, bytes);
9949+
}
9950+
}
9951+
9952+
#[tauri::command]
9953+
async fn get_network_info() -> Result<NetworkInfo, String> {
9954+
if let Ok(mut guard) = NETWORK_INFO_CACHE.lock() {
9955+
if guard.is_none() {
9956+
*guard = read_network_info_cache_file();
9957+
}
9958+
if let Some((fetched_at, info)) = guard.as_ref() {
9959+
if fetched_at.elapsed().map(|e| e < NETWORK_INFO_TTL).unwrap_or(false) {
9960+
return Ok(info.clone());
9961+
}
9962+
}
9963+
}
9964+
9965+
let client = reqwest::Client::builder()
9966+
.timeout(Duration::from_secs(5))
9967+
.build()
9968+
.map_err(|e| format!("client build failed: {e}"))?;
9969+
9970+
let resp = client
9971+
.get("https://ipinfo.io/json")
9972+
.send()
9973+
.await
9974+
.map_err(|e| format!("request failed: {e}"))?;
9975+
9976+
if !resp.status().is_success() {
9977+
let status = resp.status();
9978+
if let Ok(guard) = NETWORK_INFO_CACHE.lock() {
9979+
if let Some((_, info)) = guard.as_ref() {
9980+
return Ok(info.clone());
9981+
}
9982+
}
9983+
return Err(format!("ipinfo returned {status}"));
9984+
}
9985+
9986+
let data: Value = resp
9987+
.json()
9988+
.await
9989+
.map_err(|e| format!("parse failed: {e}"))?;
9990+
9991+
let city = data.get("city").and_then(|v| v.as_str());
9992+
let country = data.get("country").and_then(|v| v.as_str());
9993+
let region = match (city, country) {
9994+
(Some(c), Some(co)) => format!("{c}, {co}"),
9995+
(None, Some(co)) => co.to_string(),
9996+
_ => "Unknown".to_string(),
9997+
};
9998+
let ip = data.get("ip").and_then(|v| v.as_str()).unwrap_or("").to_string();
9999+
let privacy = data.get("privacy");
10000+
let is_vpn = privacy.and_then(|p| p.get("vpn")).and_then(|v| v.as_bool()).unwrap_or(false);
10001+
let is_proxy_flag = privacy.and_then(|p| p.get("proxy")).and_then(|v| v.as_bool()).unwrap_or(false);
10002+
let proxy_type = if is_vpn {
10003+
Some("VPN".to_string())
10004+
} else if is_proxy_flag {
10005+
Some("Proxy".to_string())
10006+
} else {
10007+
None
10008+
};
10009+
10010+
let info = NetworkInfo {
10011+
region,
10012+
ip,
10013+
is_proxy: is_vpn || is_proxy_flag,
10014+
proxy_type,
10015+
};
10016+
10017+
let now = std::time::SystemTime::now();
10018+
if let Ok(mut guard) = NETWORK_INFO_CACHE.lock() {
10019+
*guard = Some((now, info.clone()));
10020+
}
10021+
write_network_info_cache_file(now, &info);
10022+
10023+
Ok(info)
10024+
}
10025+
990310026
#[cfg_attr(mobile, tauri::mobile_entry_point)]
990410027
pub fn run() {
990510028
tauri::Builder::default()
@@ -10101,6 +10224,7 @@ pub fn run() {
1010110224
}
1010210225
})
1010310226
.invoke_handler(tauri::generate_handler![
10227+
get_network_info,
1010410228
make_window_nonactivating_panel,
1010510229
list_projects,
1010610230
list_sessions,

src/components/StatusBar.tsx

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -244,27 +244,23 @@ export function StatusBar({ onOpenSettings }: StatusBarProps) {
244244
};
245245
}, [settings, context]);
246246

247-
// Fetch network info (only for default mode)
247+
// Fetch network info (only for default mode).
248+
// Defer to next macrotask so an HMR full-reload arriving mid-startup
249+
// doesn't leave a 5s in-flight invoke whose callback ID is gone after reload
250+
// (source of "[TAURI] Couldn't find callback id ..." dev warning).
248251
useEffect(() => {
249-
if (settings?.enabled) return; // Skip if script mode
252+
if (settings?.enabled) return;
250253

251-
async function fetchNetworkInfo() {
252-
try {
253-
const res = await fetch("https://ipinfo.io/json");
254-
if (res.ok) {
255-
const data = await res.json();
256-
setNetworkInfo({
257-
region: data.city ? `${data.city}, ${data.country}` : data.country || "Unknown",
258-
ip: data.ip || "",
259-
isProxy: data.privacy?.proxy || data.privacy?.vpn || false,
260-
proxyType: data.privacy?.vpn ? "VPN" : data.privacy?.proxy ? "Proxy" : undefined,
261-
});
262-
}
263-
} catch {
264-
// Silently fail
265-
}
266-
}
267-
fetchNetworkInfo();
254+
let cancelled = false;
255+
const handle = window.setTimeout(() => {
256+
invoke<NetworkInfo>("get_network_info")
257+
.then((info) => { if (!cancelled) setNetworkInfo(info); })
258+
.catch(() => {});
259+
}, 0);
260+
return () => {
261+
cancelled = true;
262+
window.clearTimeout(handle);
263+
};
268264
}, [settings?.enabled]);
269265

270266
const formatTime = useCallback((d: Date) => {

src/pages/_layout.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ export default function RootLayout() {
101101

102102
useEffect(() => {
103103
const path = location.pathname + location.search;
104-
if (path && path !== "/") {
104+
// Skip transient overlay routes — they shouldn't be the "resume" target
105+
if (path && path !== "/" && location.pathname !== "/annual-report-2025") {
105106
try { localStorage.setItem("lovcode:lastPath", path); } catch {}
106107
}
107108
}, [location.pathname, location.search]);

src/pages/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const LAST_PATH_KEY = "lovcode:lastPath";
55
function getLastPath(): string {
66
try {
77
const saved = localStorage.getItem(LAST_PATH_KEY);
8-
if (saved && saved !== "/" && saved.startsWith("/")) return saved;
8+
if (saved && saved !== "/" && saved.startsWith("/") && saved !== "/annual-report-2025") return saved;
99
} catch {}
1010
return "/workspace";
1111
}

0 commit comments

Comments
 (0)