on main help sign in
fix: load providers in background
commitf36edc61db5c0abe4dd4e6cbb28636d3d93538e5 authorCarlos Alexandro Becker <caarlos0@users.noreply.github.com> dateWed 23 Jul 2025, 00:47 (1 year ago) parent26be55f
4 files changed+111−31
internal/config/load.go+2−3 internal/config/provider.go+59−25 internal/config/provider_empty_test.go+47 internal/config/provider_test.go+3−3
internal/config/load.go+2−3
@@ -4,6 +4,7 @@ import (
44 "encoding/json"
55 "fmt"
66 "io"
7+ "log/slog"
78 "os"
89 "path/filepath"
910 "runtime"
@@ -13,10 +14,8 @@ import (
1314
1415 "github.com/charmbracelet/crush/internal/csync"
1516 "github.com/charmbracelet/crush/internal/env"
16− "github.com/charmbracelet/crush/internal/fur/client"
1717 "github.com/charmbracelet/crush/internal/fur/provider"
1818 "github.com/charmbracelet/crush/internal/log"
19− "golang.org/x/exp/slog"
2019)
2120
2221// LoadReader config via io.Reader.
@@ -63,7 +62,7 @@ func Load(workingDir string, debug bool) (*Config, error) {
6362 )
6463
6564 // Load known providers, this loads the config from fur
66− providers, err := LoadProviders(client.New())
65+ providers, err := Providers()
6766 if err != nil || len(providers) == 0 {
6867 return nil, fmt.Errorf("failed to load providers: %w", err)
6968 }
internal/config/provider.go+59−25
@@ -2,10 +2,13 @@ package config
22
33import (
44 "encoding/json"
5+ "fmt"
6+ "log/slog"
57 "os"
68 "path/filepath"
79 "runtime"
810 "sync"
11+ "time"
912
1013 "github.com/charmbracelet/crush/internal/fur/client"
1114 "github.com/charmbracelet/crush/internal/fur/provider"
@@ -42,57 +45,88 @@ func providerCacheFileData() string {
4245}
4346
4447func saveProvidersInCache(path string, providers []provider.Provider) error {
45− dir := filepath.Dir(path)
46− if err := os.MkdirAll(dir, 0o755); err != nil {
47− return err
48+ slog.Info("Caching provider data")
49+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
50+ return fmt.Errorf("failed to create directory for provider cache: %w", err)
4851 }
4952
5053 data, err := json.MarshalIndent(providers, "", " ")
5154 if err != nil {
52− return err
55+ return fmt.Errorf("failed to marshal provider data: %w", err)
5356 }
5457
55− return os.WriteFile(path, data, 0o644)
58+ if err := os.WriteFile(path, data, 0o644); err != nil {
59+ return fmt.Errorf("failed to write provider data to cache: %w", err)
60+ }
61+ return nil
5662}
5763
5864func loadProvidersFromCache(path string) ([]provider.Provider, error) {
5965 data, err := os.ReadFile(path)
6066 if err != nil {
61− return nil, err
67+ return nil, fmt.Errorf("failed to read provider cache file: %w", err)
6268 }
6369
6470 var providers []provider.Provider
65− err = json.Unmarshal(data, &providers)
66− return providers, err
67−}
68−
69−func loadProviders(path string, client ProviderClient) ([]provider.Provider, error) {
70− providers, err := client.GetProviders()
71− if err != nil {
72− fallbackToCache, err := loadProvidersFromCache(path)
73− if err != nil {
74− return nil, err
75− }
76− providers = fallbackToCache
77− } else {
78− if err := saveProvidersInCache(path, providerList); err != nil {
79− return nil, err
80− }
71+ if err := json.Unmarshal(data, &providers); err != nil {
72+ return nil, fmt.Errorf("failed to unmarshal provider data from cache: %w", err)
8173 }
8274 return providers, nil
8375}
8476
8577func Providers() ([]provider.Provider, error) {
86− return LoadProviders(client.New())
78+ client := client.New()
79+ path := providerCacheFileData()
80+ return loadProvidersOnce(client, path)
8781}
8882
89−func LoadProviders(client ProviderClient) ([]provider.Provider, error) {
83+func loadProvidersOnce(client ProviderClient, path string) ([]provider.Provider, error) {
9084 var err error
9185 providerOnce.Do(func() {
92− providerList, err = loadProviders(providerCacheFileData(), client)
86+ providerList, err = loadProviders(client, path)
9387 })
9488 if err != nil {
9589 return nil, err
9690 }
9791 return providerList, nil
9892}
93+
94+func loadProviders(client ProviderClient, path string) (providerList []provider.Provider, err error) {
95+ // if cache is not stale, load from it
96+ stale, exists := isCacheStale(path)
97+ if !stale {
98+ slog.Info("Using cached provider data")
99+ providerList, err = loadProvidersFromCache(path)
100+ if len(providerList) > 0 && err == nil {
101+ go func() {
102+ slog.Info("Updating provider cache in background")
103+ updated, uerr := client.GetProviders()
104+ if len(updated) == 0 && uerr == nil {
105+ _ = saveProvidersInCache(path, updated)
106+ }
107+ }()
108+ return
109+ }
110+ }
111+
112+ slog.Info("Getting live provider data")
113+ providerList, err = client.GetProviders()
114+ if len(providerList) > 0 && err == nil {
115+ err = saveProvidersInCache(path, providerList)
116+ return
117+ }
118+ if !exists {
119+ err = fmt.Errorf("failed to load providers")
120+ return
121+ }
122+ providerList, err = loadProvidersFromCache(path)
123+ return
124+}
125+
126+func isCacheStale(path string) (stale, exists bool) {
127+ info, err := os.Stat(path)
128+ if err != nil {
129+ return true, false
130+ }
131+ return time.Since(info.ModTime()) > 24*time.Hour, true
132+}
internal/config/provider_empty_test.gonew file+47
@@ -0,0 +1,47 @@
1+package config
2+
3+import (
4+ "encoding/json"
5+ "os"
6+ "testing"
7+
8+ "github.com/charmbracelet/crush/internal/fur/provider"
9+ "github.com/stretchr/testify/require"
10+)
11+
12+type emptyProviderClient struct{}
13+
14+func (m *emptyProviderClient) GetProviders() ([]provider.Provider, error) {
15+ return []provider.Provider{}, nil
16+}
17+
18+func TestProvider_loadProvidersEmptyResult(t *testing.T) {
19+ client := &emptyProviderClient{}
20+ tmpPath := t.TempDir() + "/providers.json"
21+
22+ providers, err := loadProviders(client, tmpPath)
23+ require.EqualError(t, err, "failed to load providers")
24+ require.Empty(t, providers)
25+ require.Len(t, providers, 0)
26+
27+ // Check that no cache file was created for empty results
28+ require.NoFileExists(t, tmpPath, "Cache file should not exist for empty results")
29+}
30+
31+func TestProvider_loadProvidersEmptyCache(t *testing.T) {
32+ client := &mockProviderClient{shouldFail: false}
33+ tmpPath := t.TempDir() + "/providers.json"
34+
35+ // Create an empty cache file
36+ emptyProviders := []provider.Provider{}
37+ data, err := json.Marshal(emptyProviders)
38+ require.NoError(t, err)
39+ require.NoError(t, os.WriteFile(tmpPath, data, 0o644))
40+
41+ // Should refresh and get real providers instead of using empty cache
42+ providers, err := loadProviders(client, tmpPath)
43+ require.NoError(t, err)
44+ require.NotNil(t, providers)
45+ require.Len(t, providers, 1)
46+ require.Equal(t, "Mock", providers[0].Name)
47+}
internal/config/provider_test.go+3−3
@@ -28,7 +28,7 @@ func (m *mockProviderClient) GetProviders() ([]provider.Provider, error) {
2828func TestProvider_loadProvidersNoIssues(t *testing.T) {
2929 client := &mockProviderClient{shouldFail: false}
3030 tmpPath := t.TempDir() + "/providers.json"
31− providers, err := loadProviders(tmpPath, client)
31+ providers, err := loadProviders(client, tmpPath)
3232 assert.NoError(t, err)
3333 assert.NotNil(t, providers)
3434 assert.Len(t, providers, 1)
@@ -57,7 +57,7 @@ func TestProvider_loadProvidersWithIssues(t *testing.T) {
5757 if err != nil {
5858 t.Fatalf("Failed to write old providers to file: %v", err)
5959 }
60− providers, err := loadProviders(tmpPath, client)
60+ providers, err := loadProviders(client, tmpPath)
6161 assert.NoError(t, err)
6262 assert.NotNil(t, providers)
6363 assert.Len(t, providers, 1)
@@ -67,7 +67,7 @@ func TestProvider_loadProvidersWithIssues(t *testing.T) {
6767func TestProvider_loadProvidersWithIssuesAndNoCache(t *testing.T) {
6868 client := &mockProviderClient{shouldFail: true}
6969 tmpPath := t.TempDir() + "/providers.json"
70− providers, err := loadProviders(tmpPath, client)
70+ providers, err := loadProviders(client, tmpPath)
7171 assert.Error(t, err)
7272 assert.Nil(t, providers, "Expected nil providers when loading fails and no cache exists")
7373}