1313import java .util .Collections ;
1414import java .util .List ;
1515import java .util .Optional ;
16- import java .util .concurrent .CompletableFuture ;
17- import java .util .concurrent .ExecutionException ;
16+ import java .util .concurrent .* ;
17+ import java .util .concurrent .atomic . AtomicReference ;
1818
1919/**
20- *
20+ * Provides configuration values from the language client with caching and timeout protection.
21+ * Cache is updated asynchronously in the background to ensure changes are picked up.
2122 */
2223public class ConfigProvider {
2324 private final LanguageClient languageClient ;
2425
26+ // Cache with thread-safe atomic reference
27+ private final AtomicReference <CachedConfig > cachedConfig = new AtomicReference <>(null );
28+
29+ // Background refresh executor
30+ private final ScheduledExecutorService refreshExecutor = Executors .newSingleThreadScheduledExecutor (r -> {
31+ Thread t = new Thread (r , "ConfigProvider-Refresh" );
32+ t .setDaemon (true );
33+ return t ;
34+ });
35+
36+ // Configuration timeouts
37+ private static final long FETCH_TIMEOUT_MS = 500 ; // Fast timeout for blocking calls
38+ private static final long CACHE_DURATION_MS = 5000 ; // 5 seconds before refresh
39+ private static final long REFRESH_INTERVAL_MS = 10000 ; // Check for updates every 10 seconds
40+
41+ // Track if we've shown warning to avoid spam
42+ private volatile boolean hasShownTimeoutWarning = false ;
43+
2544 public ConfigProvider (LanguageClient languageClient ) {
2645 this .languageClient = languageClient ;
46+
47+ // Start background refresh task
48+ refreshExecutor .scheduleAtFixedRate (
49+ this ::refreshCacheAsync ,
50+ REFRESH_INTERVAL_MS ,
51+ REFRESH_INTERVAL_MS ,
52+ TimeUnit .MILLISECONDS
53+ );
54+
55+ // Initial fetch (non-blocking)
56+ refreshCacheAsync ();
2757 }
2858
59+ /**
60+ * Get configuration value with caching and timeout protection.
61+ * Returns cached value if available, otherwise attempts quick fetch with timeout.
62+ */
2963 public String getConfig (String key , String defaultValue ) {
64+ CachedConfig cached = cachedConfig .get ();
65+
66+ // Return cached value if valid
67+ if (cached != null && cached .isValid ()) {
68+ String value = cached .getValue (key );
69+ return value != null ? value : defaultValue ;
70+ }
71+
72+ // Try quick fetch if no valid cache
73+ if (cached == null ) {
74+ String value = fetchConfigWithTimeout (key , defaultValue );
75+ return value ;
76+ }
77+
78+ // Return stale cache while refresh happens in background
79+ String value = cached .getValue (key );
80+ return value != null ? value : defaultValue ;
81+ }
82+
83+ /**
84+ * Fetch config with timeout protection
85+ */
86+ private String fetchConfigWithTimeout (String key , String defaultValue ) {
3087 ConfigurationItem ci = new ConfigurationItem ();
3188 ci .setSection ("wurst" );
32- CompletableFuture <List <Object >> res = languageClient .configuration (new ConfigurationParams (Collections .singletonList (ci )));
89+ CompletableFuture <List <Object >> res = languageClient .configuration (
90+ new ConfigurationParams (Collections .singletonList (ci ))
91+ );
92+
3393 try {
34- List <Object > config = res .get ();
94+ List <Object > config = res .get (FETCH_TIMEOUT_MS , TimeUnit . MILLISECONDS );
3595 for (Object c : config ) {
3696 if (c instanceof JsonObject ) {
3797 JsonObject cfg = (JsonObject ) c ;
98+
99+ // Update cache with full config
100+ cachedConfig .set (new CachedConfig (cfg ));
101+
38102 JsonElement result = cfg .get (key );
39103 if (result instanceof JsonNull ) {
40104 return null ;
@@ -44,14 +108,46 @@ public String getConfig(String key, String defaultValue) {
44108 }
45109 }
46110 return defaultValue ;
47- } catch (InterruptedException | ExecutionException e ) {
48- String msg = "Could not get config " + key + ", using default value " + defaultValue ;
49- WLogger .warning (msg , e );
50- languageClient .showMessage (new MessageParams (MessageType .Warning , msg ));
111+ } catch (TimeoutException e ) {
112+ if (!hasShownTimeoutWarning ) {
113+ WLogger .warning ("Config request timed out for " + key + " after " + FETCH_TIMEOUT_MS + "ms, using default: " + defaultValue );
114+ hasShownTimeoutWarning = true ;
115+ }
116+ return defaultValue ;
117+ } catch (InterruptedException e ) {
118+ Thread .currentThread ().interrupt ();
119+ WLogger .warning ("Config request interrupted for " + key + ", using default: " + defaultValue );
120+ return defaultValue ;
121+ } catch (ExecutionException e ) {
122+ WLogger .warning ("Could not get config " + key + ", using default value " + defaultValue , e );
51123 return defaultValue ;
52124 }
53125 }
54126
127+ /**
128+ * Asynchronously refresh the cache in background
129+ */
130+ private void refreshCacheAsync () {
131+ ConfigurationItem ci = new ConfigurationItem ();
132+ ci .setSection ("wurst" );
133+
134+ languageClient .configuration (new ConfigurationParams (Collections .singletonList (ci )))
135+ .thenAccept (config -> {
136+ for (Object c : config ) {
137+ if (c instanceof JsonObject ) {
138+ JsonObject cfg = (JsonObject ) c ;
139+ cachedConfig .set (new CachedConfig (cfg ));
140+ WLogger .trace ("Config cache refreshed successfully" );
141+ return ;
142+ }
143+ }
144+ })
145+ .exceptionally (e -> {
146+ WLogger .trace ("Background config refresh failed (this is normal if client is busy): " + e .getMessage ());
147+ return null ;
148+ });
149+ }
150+
55151 public String getJhcrExe () {
56152 return getConfig ("jhcrExe" , "jhcr.exe" );
57153 }
@@ -66,4 +162,46 @@ public Optional<String> getWc3RunArgs() {
66162 public Optional <String > getMapDocumentPath () {
67163 return Optional .ofNullable (getConfig ("mapDocumentPath" , null ));
68164 }
165+
166+ /**
167+ * Shutdown the background refresh executor
168+ */
169+ public void shutdown () {
170+ refreshExecutor .shutdown ();
171+ try {
172+ if (!refreshExecutor .awaitTermination (1 , TimeUnit .SECONDS )) {
173+ refreshExecutor .shutdownNow ();
174+ }
175+ } catch (InterruptedException e ) {
176+ refreshExecutor .shutdownNow ();
177+ Thread .currentThread ().interrupt ();
178+ }
179+ }
180+
181+ /**
182+ * Immutable cached configuration with timestamp
183+ */
184+ private static class CachedConfig {
185+ private final JsonObject config ;
186+ private final long timestamp ;
187+
188+ CachedConfig (JsonObject config ) {
189+ this .config = config ;
190+ this .timestamp = System .currentTimeMillis ();
191+ }
192+
193+ boolean isValid () {
194+ return (System .currentTimeMillis () - timestamp ) < CACHE_DURATION_MS ;
195+ }
196+
197+ String getValue (String key ) {
198+ JsonElement result = config .get (key );
199+ if (result instanceof JsonNull ) {
200+ return null ;
201+ } else if (result != null ) {
202+ return result .getAsString ();
203+ }
204+ return null ;
205+ }
206+ }
69207}
0 commit comments