diff --git a/cache/cache.go b/cache/cache.go index 6e06a78..37b5c78 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "strings" + "sync" "time" "github.com/AccursedGalaxy/streakode/scan" @@ -16,15 +17,38 @@ import ( // -> Currently when a major version upgrade happens we need to manually delete cache file and do a refresh. // in-memory cache to hold repo metadata during runtime -var Cache map[string]scan.RepoMetadata +var ( + Cache map[string]scan.RepoMetadata + metadata CacheMetadata + mutex sync.RWMutex +) + +type CacheMetadata struct { + LastRefresh time.Time + Version string // For Future Version Tracking +} // InitCache - Initializes the in memory cache func InitCache() { Cache = make(map[string]scan.RepoMetadata) } +// check if refresh is needed +func ShouldAutoRefresh(refreshInterval time.Duration) bool { + mutex.RLock() + defer mutex.RUnlock() + + if metadata.LastRefresh.IsZero() { + return true + } + return time.Since(metadata.LastRefresh) > refreshInterval +} + // LoadCache - loads repository metadata from a JSON cache file into memory func LoadCache(filePath string) error { + mutex.Lock() + defer mutex.Unlock() + file, err := os.Open(filePath) if os.IsNotExist(err) { InitCache() @@ -41,11 +65,32 @@ func LoadCache(filePath string) error { return nil } + // Load metadata from a separate file + metadataPath := filePath + ".meta" + metaFile, err := os.Open(metadataPath) + if os.IsNotExist(err) { + metadata = CacheMetadata{LastRefresh: time.Time{}} + return nil + } + if err != nil { + return fmt.Errorf("error opening metadata file: %v", err) + } + defer metaFile.Close() + + decoder = json.NewDecoder(metaFile) + if err := decoder.Decode(&metadata); err != nil { + metadata = CacheMetadata{LastRefresh: time.Time{}} + return nil + } + return nil } // SaveCache - saves the in-memory cache to a JSON file func SaveCache(filePath string) error { + mutex.Lock() + defer mutex.Unlock() + // Create directory if it doesn't exist dir := filepath.Dir(filePath) if err := os.MkdirAll(dir, 0755); err != nil { @@ -63,6 +108,20 @@ func SaveCache(filePath string) error { return fmt.Errorf("error encoding cache: %v", err) } + // Save metadata to a separate file + metadataPath := filePath + ".meta" + metaFile, err := os.Create(metadataPath) + if err != nil { + return fmt.Errorf("error creating metadata file: %v", err) + } + defer metaFile.Close() + + metadata.LastRefresh = time.Now() + encoder = json.NewEncoder(metaFile) + if err := encoder.Encode(metadata); err != nil { + return fmt.Errorf("error encoding metadata: %v", err) + } + return nil } @@ -138,3 +197,29 @@ func RefreshCache(dirs []string, author string, cacheFilePath string, excludedPa return SaveCache(cacheFilePath) } + +// AsyncRefreshCache performs a non-blocking cache refresh +func AsyncRefreshCache(dirs []string, author string, cacheFilePath string, excludedPatterns []string, excludedPaths []string) { + go func() { + if err := RefreshCache(dirs, author, cacheFilePath, excludedPatterns, excludedPaths); err != nil { + log.Printf("Background cache refresh failed: %v", err) + } + }() +} + +// QuickNeedsRefresh performs a fast check if refresh is needed without scanning repositories +func QuickNeedsRefresh(refreshInterval time.Duration) bool { + mutex.RLock() + defer mutex.RUnlock() + + if metadata.LastRefresh.IsZero() { + return true + } + + // Check if cache file exists and its modification time + if time.Since(metadata.LastRefresh) > refreshInterval { + return true + } + + return false +} diff --git a/main.go b/main.go index 4957f1d..80fe5d7 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "path/filepath" + "time" "github.com/AccursedGalaxy/streakode/cache" "github.com/AccursedGalaxy/streakode/cmd" @@ -35,6 +36,54 @@ func getCacheFilePath(profile string) string { return filepath.Join(home, fmt.Sprintf(".streakode_%s.cache", profile)) } +func ensureCacheRefresh() error { + // Skip if no refresh interval is configured + if config.AppConfig.RefreshInterval <= 0 { + return nil + } + + interval := time.Duration(config.AppConfig.RefreshInterval) * time.Minute + + // Quick check if refresh is needed + if cache.QuickNeedsRefresh(interval) { + cacheFilePath := getCacheFilePath(config.AppState.ActiveProfile) + + // For commands that need fresh data, use sync refresh + if requiresFreshData() { + return cache.RefreshCache( + config.AppConfig.ScanDirectories, + config.AppConfig.Author, + cacheFilePath, + config.AppConfig.ScanSettings.ExcludedPatterns, + config.AppConfig.ScanSettings.ExcludedPaths, + ) + } + + // For other commands, use async refresh + cache.AsyncRefreshCache( + config.AppConfig.ScanDirectories, + config.AppConfig.Author, + cacheFilePath, + config.AppConfig.ScanSettings.ExcludedPatterns, + config.AppConfig.ScanSettings.ExcludedPaths, + ) + } + return nil +} + +func requiresFreshData() bool { + // Get the command being executed + cmd := os.Args[1] + + // List of commands that need fresh data + freshDataCommands := map[string]bool{ + "stats": true, + "reload": true, + } + + return freshDataCommands[cmd] +} + func main() { var profile string @@ -55,6 +104,10 @@ func main() { if err := cache.LoadCache(cacheFilePath); err != nil { fmt.Printf("Error loading cache: %v\n", err) } + + if err := ensureCacheRefresh(); err != nil { + fmt.Printf("Error refreshing cache: %v\n", err) + } }, }