From f0bc768e601585a28accf06d267e3a86b6410d0d Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Tue, 2 Jun 2026 16:08:22 -0400 Subject: [PATCH 1/2] Rate-limit CLI update notices --- crates/update/src/update_notice.rs | 101 +++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/crates/update/src/update_notice.rs b/crates/update/src/update_notice.rs index 3148061a64e..a25e08f2870 100644 --- a/crates/update/src/update_notice.rs +++ b/crates/update/src/update_notice.rs @@ -13,6 +13,8 @@ use crate::cli::install::fetch_latest_release_version; /// How long to cache the update check result. const CHECK_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60); +/// How often to show the user the same update notice. +const NOTICE_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60); const UPDATE_CHECK_TIMEOUT: Duration = Duration::from_secs(2); /// Cache file name. @@ -24,6 +26,15 @@ struct Cache { last_check_secs: u64, /// The latest version string (without "v" prefix). latest_version: String, + /// Unix timestamp of the last printed update notice. + #[serde(default)] + last_notice_secs: u64, + /// The current version from the last printed update notice. + #[serde(default)] + notice_current_version: String, + /// The latest version from the last printed update notice. + #[serde(default)] + notice_latest_version: String, } impl Cache { @@ -41,6 +52,18 @@ impl Cache { fn path(config_dir: &Path) -> PathBuf { config_dir.join(CACHE_FILENAME) } + + fn should_print_notice(&self, current: &semver::Version, latest: &semver::Version, now: u64) -> bool { + self.notice_current_version != current.to_string() + || self.notice_latest_version != latest.to_string() + || now.saturating_sub(self.last_notice_secs) >= NOTICE_INTERVAL.as_secs() + } + + fn mark_notice_printed(&mut self, current: &semver::Version, latest: &semver::Version, now: u64) { + self.last_notice_secs = now; + self.notice_current_version = current.to_string(); + self.notice_latest_version = latest.to_string(); + } } fn now_secs() -> u64 { @@ -78,6 +101,7 @@ fn latest_version_or_cached(config_dir: &Path) -> Option { Cache { last_check_secs: now, latest_version: version.to_string(), + ..Default::default() } .write(config_dir); Some(version) @@ -115,11 +139,88 @@ pub(crate) fn maybe_print_update_notice(config_dir: &Path) { }; if latest > current { + let now = now_secs(); + let mut cache = Cache::read(config_dir).unwrap_or_default(); + if !cache.should_print_notice(¤t, &latest, now) { + return; + } + eprintln!( "{}", format!("A new version of SpacetimeDB is available: v{latest} (current: v{current})").yellow() ); eprintln!("Run `spacetime version upgrade` to update."); eprintln!(); + + cache.mark_notice_printed(¤t, &latest, now); + if cache.latest_version.is_empty() { + cache.latest_version = latest.to_string(); + } + cache.write(config_dir); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn version(version: &str) -> semver::Version { + semver::Version::parse(version).unwrap() + } + + #[test] + fn update_notice_prints_when_never_shown() { + let cache = Cache { + latest_version: "2.0.0".to_string(), + ..Default::default() + }; + + assert!(cache.should_print_notice(&version("1.0.0"), &version("2.0.0"), 100)); + } + + #[test] + fn update_notice_is_suppressed_within_interval_for_same_versions() { + let mut cache = Cache::default(); + let current = version("1.0.0"); + let latest = version("2.0.0"); + cache.mark_notice_printed(¤t, &latest, 100); + + assert!(!cache.should_print_notice(¤t, &latest, 100 + NOTICE_INTERVAL.as_secs() - 1)); + } + + #[test] + fn update_notice_reprints_after_interval_for_same_versions() { + let mut cache = Cache::default(); + let current = version("1.0.0"); + let latest = version("2.0.0"); + cache.mark_notice_printed(¤t, &latest, 100); + + assert!(cache.should_print_notice(¤t, &latest, 100 + NOTICE_INTERVAL.as_secs())); + } + + #[test] + fn update_notice_reprints_when_latest_version_changes() { + let mut cache = Cache::default(); + let current = version("1.0.0"); + cache.mark_notice_printed(¤t, &version("2.0.0"), 100); + + assert!(cache.should_print_notice(¤t, &version("2.1.0"), 101)); + } + + #[test] + fn update_notice_cache_reads_old_format() { + let tempdir = tempfile::tempdir().unwrap(); + std::fs::write( + Cache::path(tempdir.path()), + r#"{"last_check_secs":123,"latest_version":"2.0.0"}"#, + ) + .unwrap(); + + let cache = Cache::read(tempdir.path()).unwrap(); + assert_eq!(cache.last_check_secs, 123); + assert_eq!(cache.latest_version, "2.0.0"); + assert_eq!(cache.last_notice_secs, 0); + assert!(cache.notice_current_version.is_empty()); + assert!(cache.notice_latest_version.is_empty()); } } From f0ab9ecdc9d2cf8f2040cf6927bc1f6fa2b0f7b6 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Tue, 2 Jun 2026 16:13:12 -0400 Subject: [PATCH 2/2] Address update notice review --- crates/update/src/update_notice.rs | 33 +++++++++++------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/crates/update/src/update_notice.rs b/crates/update/src/update_notice.rs index a25e08f2870..b3c3c05792f 100644 --- a/crates/update/src/update_notice.rs +++ b/crates/update/src/update_notice.rs @@ -29,9 +29,6 @@ struct Cache { /// Unix timestamp of the last printed update notice. #[serde(default)] last_notice_secs: u64, - /// The current version from the last printed update notice. - #[serde(default)] - notice_current_version: String, /// The latest version from the last printed update notice. #[serde(default)] notice_latest_version: String, @@ -53,15 +50,13 @@ impl Cache { config_dir.join(CACHE_FILENAME) } - fn should_print_notice(&self, current: &semver::Version, latest: &semver::Version, now: u64) -> bool { - self.notice_current_version != current.to_string() - || self.notice_latest_version != latest.to_string() + fn should_print_notice(&self, latest: &semver::Version, now: u64) -> bool { + self.notice_latest_version != latest.to_string() || now.saturating_sub(self.last_notice_secs) >= NOTICE_INTERVAL.as_secs() } - fn mark_notice_printed(&mut self, current: &semver::Version, latest: &semver::Version, now: u64) { + fn mark_notice_printed(&mut self, latest: &semver::Version, now: u64) { self.last_notice_secs = now; - self.notice_current_version = current.to_string(); self.notice_latest_version = latest.to_string(); } } @@ -141,7 +136,7 @@ pub(crate) fn maybe_print_update_notice(config_dir: &Path) { if latest > current { let now = now_secs(); let mut cache = Cache::read(config_dir).unwrap_or_default(); - if !cache.should_print_notice(¤t, &latest, now) { + if !cache.should_print_notice(&latest, now) { return; } @@ -152,7 +147,7 @@ pub(crate) fn maybe_print_update_notice(config_dir: &Path) { eprintln!("Run `spacetime version upgrade` to update."); eprintln!(); - cache.mark_notice_printed(¤t, &latest, now); + cache.mark_notice_printed(&latest, now); if cache.latest_version.is_empty() { cache.latest_version = latest.to_string(); } @@ -175,36 +170,33 @@ mod tests { ..Default::default() }; - assert!(cache.should_print_notice(&version("1.0.0"), &version("2.0.0"), 100)); + assert!(cache.should_print_notice(&version("2.0.0"), 100)); } #[test] fn update_notice_is_suppressed_within_interval_for_same_versions() { let mut cache = Cache::default(); - let current = version("1.0.0"); let latest = version("2.0.0"); - cache.mark_notice_printed(¤t, &latest, 100); + cache.mark_notice_printed(&latest, 100); - assert!(!cache.should_print_notice(¤t, &latest, 100 + NOTICE_INTERVAL.as_secs() - 1)); + assert!(!cache.should_print_notice(&latest, 100 + NOTICE_INTERVAL.as_secs() - 1)); } #[test] fn update_notice_reprints_after_interval_for_same_versions() { let mut cache = Cache::default(); - let current = version("1.0.0"); let latest = version("2.0.0"); - cache.mark_notice_printed(¤t, &latest, 100); + cache.mark_notice_printed(&latest, 100); - assert!(cache.should_print_notice(¤t, &latest, 100 + NOTICE_INTERVAL.as_secs())); + assert!(cache.should_print_notice(&latest, 100 + NOTICE_INTERVAL.as_secs())); } #[test] fn update_notice_reprints_when_latest_version_changes() { let mut cache = Cache::default(); - let current = version("1.0.0"); - cache.mark_notice_printed(¤t, &version("2.0.0"), 100); + cache.mark_notice_printed(&version("2.0.0"), 100); - assert!(cache.should_print_notice(¤t, &version("2.1.0"), 101)); + assert!(cache.should_print_notice(&version("2.1.0"), 101)); } #[test] @@ -220,7 +212,6 @@ mod tests { assert_eq!(cache.last_check_secs, 123); assert_eq!(cache.latest_version, "2.0.0"); assert_eq!(cache.last_notice_secs, 0); - assert!(cache.notice_current_version.is_empty()); assert!(cache.notice_latest_version.is_empty()); } }