Skip to content

Commit 4f88419

Browse files
committed
Implement version cache for ZeuZ Node updates to reduce API calls and improve user experience
1 parent b975401 commit 4f88419

1 file changed

Lines changed: 87 additions & 19 deletions

File tree

Apps/node_runner/main.go

Lines changed: 87 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,61 @@ type zeuzRelease struct {
3838
TagName string `json:"tag_name"`
3939
}
4040

41+
// versionCache persists the last GitHub version check result to disk so the
42+
// network call is skipped on subsequent startups within the cache window.
43+
type versionCache struct {
44+
CheckedAt time.Time `json:"checked_at"`
45+
Latest string `json:"latest"`
46+
}
47+
48+
const versionCheckInterval = 24 * time.Hour
49+
4150
var errRateLimited = errors.New("GitHub API rate limited — try again later")
4251

52+
// versionCachePath returns the path to the version cache file.
53+
func versionCachePath() string {
54+
home, err := os.UserHomeDir()
55+
if err != nil {
56+
return ""
57+
}
58+
return filepath.Join(home, ".zeuz", "version_check_cache.json")
59+
}
60+
61+
// loadVersionCache reads the cache file. Returns nil if missing, unreadable,
62+
// or older than versionCheckInterval.
63+
func loadVersionCache() *versionCache {
64+
path := versionCachePath()
65+
if path == "" {
66+
return nil
67+
}
68+
data, err := os.ReadFile(path)
69+
if err != nil {
70+
return nil
71+
}
72+
var cache versionCache
73+
if err := json.Unmarshal(data, &cache); err != nil {
74+
return nil
75+
}
76+
if time.Since(cache.CheckedAt) > versionCheckInterval {
77+
return nil // expired
78+
}
79+
return &cache
80+
}
81+
82+
// saveVersionCache writes the latest version and current timestamp to disk.
83+
func saveVersionCache(latest string) {
84+
path := versionCachePath()
85+
if path == "" {
86+
return
87+
}
88+
os.MkdirAll(filepath.Dir(path), 0755)
89+
data, err := json.Marshal(versionCache{CheckedAt: time.Now(), Latest: latest})
90+
if err != nil {
91+
return
92+
}
93+
os.WriteFile(path, data, 0644)
94+
}
95+
4396
func effectiveVersion() string {
4497
if targetVersion != "" {
4598
return targetVersion
@@ -123,6 +176,9 @@ func runUpdate() error {
123176
return fmt.Errorf("update failed: %w", err)
124177
}
125178

179+
// Refresh the cache so the next startup doesn't re-check immediately.
180+
saveVersionCache(latest)
181+
126182
fmt.Printf(colorGreen+" Update complete (%s)"+colorReset+"\n", latest)
127183
return nil
128184
}
@@ -401,22 +457,42 @@ func runUVCommands(args []string) error {
401457
return runCmd.Run()
402458
}
403459

460+
// showUpdateBannerIfNeeded reads the version cache and prints the update
461+
// banner if the cached latest version differs from the running version.
462+
// This is called at startup and again after node_cli.py exits so the user
463+
// always sees a reminder. No network call — purely a cache file read.
464+
func showUpdateBannerIfNeeded() {
465+
cache := loadVersionCache()
466+
if cache == nil {
467+
return
468+
}
469+
if cache.Latest != "" && cache.Latest != effectiveVersion() {
470+
printUpdateBanner(effectiveVersion(), cache.Latest)
471+
}
472+
}
473+
404474
func main() {
405475
flag.Parse()
406476

407477
fmt.Printf(colorGreen+colorBold+" ZeuZ Node %s"+colorReset+"\n", version)
408478

409-
// Launch background version check (non-blocking)
410-
updateCh := make(chan string, 1)
479+
// Refresh the version cache in the background (once per 24h).
480+
// Just writes to the cache file — showUpdateBannerIfNeeded() reads it.
411481
go func() {
482+
if loadVersionCache() != nil {
483+
return // cache still fresh, nothing to do
484+
}
412485
latest, err := fetchLatestVersion()
413486
if err != nil {
414-
updateCh <- ""
415487
return
416488
}
417-
updateCh <- latest
489+
saveVersionCache(latest)
418490
}()
419491

492+
// Show update banner at startup using whatever is in the cache.
493+
// On first ever run the cache is empty so nothing shows yet.
494+
showUpdateBannerIfNeeded()
495+
420496
zeuzDir := getZeuZNodeDir()
421497

422498
if *cleanFlag {
@@ -461,24 +537,13 @@ func main() {
461537
os.Exit(1)
462538
}
463539

464-
// Change directory to ZeuZ Node (re-evaluate after potential targetVersion change)
540+
// Re-evaluate after potential targetVersion change by runUpdate.
465541
zeuzDir = getZeuZNodeDir()
466542
if err := os.Chdir(zeuzDir); err != nil {
467543
fmt.Printf(" Error changing to ZeuZ Node directory: %v\n", err)
468544
os.Exit(1)
469545
}
470546

471-
// Drain the background update check (non-blocking)
472-
select {
473-
case latest := <-updateCh:
474-
if latest != "" && latest != effectiveVersion() {
475-
printUpdateBanner(effectiveVersion(), latest)
476-
}
477-
default:
478-
// check still in flight or failed — continue silently
479-
}
480-
481-
// Update PATH before checking if UV is installed
482547
if err := updatePath(); err != nil {
483548
fmt.Printf(" Error updating path: %v\n", err)
484549
}
@@ -496,10 +561,13 @@ func main() {
496561

497562
// Get remaining command line arguments after flag parsing
498563
args := flag.Args()
564+
runErr := runUVCommands(args)
565+
566+
// Show update banner again after node_cli.py exits.
567+
showUpdateBannerIfNeeded()
499568

500-
// Run UV commands with arguments
501-
if err := runUVCommands(args); err != nil {
502-
fmt.Printf(" Error running UV commands: %v\n", err)
569+
if runErr != nil {
570+
fmt.Printf(" Error running UV commands: %v\n", runErr)
503571
os.Exit(1)
504572
}
505573
}

0 commit comments

Comments
 (0)