@@ -18,7 +18,8 @@ import (
1818)
1919
2020// GetLatest fetches the latest config with metadata, using cache when possible
21- // The refreshInterval parameter determines how long cached configs are considered fresh
21+ // The refreshInterval parameter determines how long cached configs are considered fresh.
22+ // The effective refresh interval will be the minimum of refreshInterval and the server's CacheTTL.
2223func (c * Client ) GetLatest (ctx context.Context , configURL string , refreshInterval time.Duration ) (* Response , error ) {
2324 cacheDir , err := c .getCacheDir (configURL )
2425 if err != nil {
@@ -54,6 +55,26 @@ func (c *Client) GetLatest(ctx context.Context, configURL string, refreshInterva
5455 return nil , fmt .Errorf ("failed to fetch from remote (%w) and no valid cache available (%w)" , err , cacheErr )
5556 }
5657
58+ // Successfully fetched new config, now check if server CacheTTL requires using cached version
59+ if resp .Config != nil && cacheErr == nil {
60+ effectiveInterval := calculateEffectiveRefreshInterval (refreshInterval , resp .Config .CacheTTL )
61+ if effectiveInterval < refreshInterval {
62+ log .Debugf ("server CacheTTL (%ds) is shorter than user refresh interval (%s), using effective interval %s" ,
63+ resp .Config .CacheTTL , refreshInterval , effectiveInterval )
64+
65+ // Re-check if cached version is still fresh under the effective (shorter) interval
66+ if cachedResp .CacheAge < effectiveInterval {
67+ log .Debugf ("cached config is still fresh under server CacheTTL (%s)" , effectiveInterval )
68+ return cachedResp , nil
69+ }
70+ // If cached version is also stale under server TTL, continue with newly fetched config
71+ log .Debugf ("cached config is stale even under server CacheTTL, using newly fetched config" )
72+ } else {
73+ log .Debugf ("using user refresh interval (%s), server CacheTTL (%ds) is longer or not specified" ,
74+ refreshInterval , resp .Config .CacheTTL )
75+ }
76+ }
77+
5778 // Clean up old versions
5879 if err := c .cleanupOldVersions (cacheDir ); err != nil {
5980 log .Warnf ("failed to cleanup old versions: %v" , err )
@@ -75,48 +96,101 @@ func (c *Client) GetCached(cacheDir string) (*Response, error) {
7596// MustGetConfig returns config with fallbacks to hardcoded defaults
7697// For cache-only behavior, pass a cancelled context
7798// This method never returns an error and always returns usable mainnet values
99+ // The effective refresh interval will be the minimum of refreshInterval and the server's CacheTTL
78100func (c * Client ) MustGetConfig (ctx context.Context , configURL string , refreshInterval time.Duration ) * Config {
79101 resp , err := c .GetLatest (ctx , configURL , refreshInterval )
80102 var config * Config
81103 if err == nil {
82104 config = resp .Config
83105 }
84106 if err != nil {
85- // Return fallback config
107+ // Return fallback config with new structure
86108 return & Config {
87- Bootstrap : FallbackBootstrapPeers ,
88- DNSResolvers : FallbackDNSResolvers ,
89- DelegatedRouters : map [string ]DelegatedRouterConfig {
90- MainnetProfileNodesWithDHT : DelegatedRouterConfig (FallbackDelegatedRouters ),
91- MainnetProfileNodesWithoutDHT : DelegatedRouterConfig (FallbackDelegatedRouters ),
109+ AutoConfigVersion : 0 , // Indicates fallback config
110+ AutoConfigSchema : 4 ,
111+ SystemRegistry : map [string ]SystemConfig {
112+ SystemAminoDHT : {
113+ Description : "Fallback AminoDHT configuration" ,
114+ NativeConfig : & NativeConfig {
115+ Bootstrap : FallbackBootstrapPeers ,
116+ },
117+ DelegatedConfig : & DelegatedConfig {
118+ Read : []string {"/routing/v1/providers" , "/routing/v1/peers" , "/routing/v1/ipns" },
119+ Write : []string {"/routing/v1/ipns" },
120+ },
121+ },
122+ SystemIPNI : {
123+ Description : "Fallback IPNI configuration" ,
124+ DelegatedConfig : & DelegatedConfig {
125+ Read : []string {"/routing/v1/providers" },
126+ Write : []string {},
127+ },
128+ },
92129 },
93- DelegatedPublishers : map [string ]DelegatedPublisherConfig {
94- MainnetProfileIPNSPublishers : DelegatedPublisherConfig (FallbackDelegatedPublishers ),
130+ DNSResolvers : FallbackDNSResolvers ,
131+ DelegatedEndpoints : map [string ]EndpointConfig {
132+ "https://ipni.example.com" : {
133+ Systems : []string {SystemIPNI },
134+ Read : []string {"/routing/v1/providers" },
135+ Write : []string {},
136+ },
95137 },
96138 }
97139 }
98140
99- // Fill in missing fields with fallbacks
100- if len ( config .Bootstrap ) == 0 {
101- config .Bootstrap = FallbackBootstrapPeers
141+ // Fill in missing fields with fallbacks for new structure
142+ if config .SystemRegistry == nil {
143+ config .SystemRegistry = make ( map [ string ] SystemConfig )
102144 }
103145 if len (config .DNSResolvers ) == 0 {
104146 config .DNSResolvers = FallbackDNSResolvers
105147 }
106- if config .DelegatedRouters == nil {
107- config .DelegatedRouters = make (map [string ]DelegatedRouterConfig )
108- }
109- if len (config .DelegatedRouters [MainnetProfileNodesWithDHT ]) == 0 {
110- config .DelegatedRouters [MainnetProfileNodesWithDHT ] = DelegatedRouterConfig (FallbackDelegatedRouters )
148+ if config .DelegatedEndpoints == nil {
149+ config .DelegatedEndpoints = make (map [string ]EndpointConfig )
111150 }
112- if len (config .DelegatedRouters [MainnetProfileNodesWithoutDHT ]) == 0 {
113- config .DelegatedRouters [MainnetProfileNodesWithoutDHT ] = DelegatedRouterConfig (FallbackDelegatedRouters )
151+
152+ // Ensure AminoDHT system exists with fallback bootstrap
153+ if _ , exists := config .SystemRegistry [SystemAminoDHT ]; ! exists {
154+ config .SystemRegistry [SystemAminoDHT ] = SystemConfig {
155+ Description : "Fallback AminoDHT configuration" ,
156+ NativeConfig : & NativeConfig {
157+ Bootstrap : FallbackBootstrapPeers ,
158+ },
159+ DelegatedConfig : & DelegatedConfig {
160+ Read : []string {"/routing/v1/providers" , "/routing/v1/peers" , "/routing/v1/ipns" },
161+ Write : []string {"/routing/v1/ipns" },
162+ },
163+ }
164+ } else if config .SystemRegistry [SystemAminoDHT ].NativeConfig == nil || len (config .SystemRegistry [SystemAminoDHT ].NativeConfig .Bootstrap ) == 0 {
165+ // Update existing system with fallback bootstrap if missing
166+ system := config .SystemRegistry [SystemAminoDHT ]
167+ if system .NativeConfig == nil {
168+ system .NativeConfig = & NativeConfig {}
169+ }
170+ if len (system .NativeConfig .Bootstrap ) == 0 {
171+ system .NativeConfig .Bootstrap = FallbackBootstrapPeers
172+ }
173+ config .SystemRegistry [SystemAminoDHT ] = system
114174 }
115- if config .DelegatedPublishers == nil {
116- config .DelegatedPublishers = make (map [string ]DelegatedPublisherConfig )
175+
176+ // Ensure IPNI system exists
177+ if _ , exists := config .SystemRegistry [SystemIPNI ]; ! exists {
178+ config .SystemRegistry [SystemIPNI ] = SystemConfig {
179+ Description : "Fallback IPNI configuration" ,
180+ DelegatedConfig : & DelegatedConfig {
181+ Read : []string {"/routing/v1/providers" },
182+ Write : []string {},
183+ },
184+ }
117185 }
118- if len (config .DelegatedPublishers [MainnetProfileIPNSPublishers ]) == 0 {
119- config .DelegatedPublishers [MainnetProfileIPNSPublishers ] = DelegatedPublisherConfig (FallbackDelegatedPublishers )
186+
187+ // Ensure at least one delegated endpoint exists
188+ if len (config .DelegatedEndpoints ) == 0 {
189+ config .DelegatedEndpoints ["https://ipni.example.com" ] = EndpointConfig {
190+ Systems : []string {SystemIPNI },
191+ Read : []string {"/routing/v1/providers" },
192+ Write : []string {},
193+ }
120194 }
121195
122196 return config
@@ -432,10 +506,14 @@ func formatDuration(d time.Duration) string {
432506
433507// validateConfig validates all multiaddr and URL values in the config
434508func (c * Client ) validateConfig (config * Config ) error {
435- // Validate Bootstrap multiaddrs
436- for i , bootstrap := range config .Bootstrap {
437- if _ , err := ma .NewMultiaddr (bootstrap ); err != nil {
438- return fmt .Errorf ("Bootstrap[%d] invalid multiaddr %q: %w" , i , bootstrap , err )
509+ // Validate SystemRegistry bootstrap multiaddrs
510+ for systemName , system := range config .SystemRegistry {
511+ if system .NativeConfig != nil {
512+ for i , bootstrap := range system .NativeConfig .Bootstrap {
513+ if _ , err := ma .NewMultiaddr (bootstrap ); err != nil {
514+ return fmt .Errorf ("SystemRegistry[%q].NativeConfig.Bootstrap[%d] invalid multiaddr %q: %w" , systemName , i , bootstrap , err )
515+ }
516+ }
439517 }
440518 }
441519
@@ -448,23 +526,57 @@ func (c *Client) validateConfig(config *Config) error {
448526 }
449527 }
450528
451- // Validate DelegatedRouters URLs
452- for routerType , routerConfig := range config .DelegatedRouters {
453- for i , urlStr := range routerConfig {
454- if _ , err := url .Parse (urlStr ); err != nil {
455- return fmt .Errorf ("DelegatedRouters[%q][%d] invalid URL %q: %w" , routerType , i , urlStr , err )
529+ // Validate DelegatedEndpoints URLs (must be absolute HTTP/HTTPS URLs)
530+ for endpointURL , endpointConfig := range config .DelegatedEndpoints {
531+ parsed , err := url .Parse (endpointURL )
532+ if err != nil {
533+ return fmt .Errorf ("DelegatedEndpoints URL %q invalid: %w" , endpointURL , err )
534+ }
535+
536+ // Require absolute URLs with HTTP/HTTPS scheme
537+ if parsed .Scheme == "" {
538+ return fmt .Errorf ("DelegatedEndpoints URL %q must be absolute (missing scheme)" , endpointURL )
539+ }
540+ if parsed .Host == "" {
541+ return fmt .Errorf ("DelegatedEndpoints URL %q must have a host" , endpointURL )
542+ }
543+ if parsed .Scheme != "http" && parsed .Scheme != "https" {
544+ return fmt .Errorf ("DelegatedEndpoints URL %q must use http or https scheme, got %q" , endpointURL , parsed .Scheme )
545+ }
546+
547+ // Validate Read paths
548+ for i , path := range endpointConfig .Read {
549+ if ! strings .HasPrefix (path , "/" ) {
550+ return fmt .Errorf ("DelegatedEndpoints[%q].Read[%d] path %q must start with /" , endpointURL , i , path )
456551 }
457552 }
458- }
459553
460- // Validate DelegatedPublishers URLs
461- for publisherType , publisherConfig := range config .DelegatedPublishers {
462- for i , urlStr := range publisherConfig {
463- if _ , err := url .Parse (urlStr ); err != nil {
464- return fmt .Errorf ("DelegatedPublishers[%q][%d] invalid URL %q: %w" , publisherType , i , urlStr , err )
554+ // Validate Write paths
555+ for i , path := range endpointConfig .Write {
556+ if ! strings .HasPrefix (path , "/" ) {
557+ return fmt .Errorf ("DelegatedEndpoints[%q].Write[%d] path %q must start with /" , endpointURL , i , path )
465558 }
466559 }
467560 }
468561
469562 return nil
470563}
564+
565+ // calculateEffectiveRefreshInterval returns the minimum of user-provided interval and server CacheTTL.
566+ // This ensures that both user preferences and server cache policies are respected.
567+ // If cacheTTLSeconds is 0 or negative, only the user interval is used.
568+ func calculateEffectiveRefreshInterval (userInterval time.Duration , cacheTTLSeconds int ) time.Duration {
569+ if cacheTTLSeconds <= 0 {
570+ // Server doesn't specify TTL or specifies invalid TTL, use user preference
571+ return userInterval
572+ }
573+
574+ serverTTL := time .Duration (cacheTTLSeconds ) * time .Second
575+ if serverTTL < userInterval {
576+ // Server wants shorter cache period, respect it
577+ return serverTTL
578+ }
579+
580+ // User wants shorter cache period or same as server, respect user preference
581+ return userInterval
582+ }
0 commit comments