55// The .NET Foundation licenses this file to you under the MIT license.
66// See the LICENSE file in the project root for more information.
77
8- using System ;
98using System . Collections . Concurrent ;
10- using System . Collections . Generic ;
9+ using System . ComponentModel ;
1110using System . Diagnostics ;
12- using System . IO ;
13- using System . Linq ;
14- using System . Net . Http ;
15- using System . Threading ;
16- using System . Threading . Tasks ;
1711
1812namespace StabilityMatrix . Avalonia . Controls . VendorLabs . Cache ;
1913
14+ [ Localizable ( false ) ]
2015internal abstract class CacheBase < T >
2116{
22- private class ConcurrentRequest
17+ private class ConcurrentRequest ( Func < Task < T ? > > factory )
2318 {
24- public Task < T ? > ? Task { get ; set ; }
19+ private readonly Lazy < Task < T ? > > _factory = new ( factory , LazyThreadSafetyMode . ExecutionAndPublication ) ;
20+
21+ public Task < T ? > Task => _factory . Value ;
2522
2623 public bool EnsureCachedCopy { get ; set ; }
24+
25+ public bool IsCompletedSuccessfully =>
26+ _factory is { IsValueCreated : true , Value . IsCompletedSuccessfully : true } ;
2727 }
2828
2929 private readonly SemaphoreSlim _cacheFolderSemaphore = new SemaphoreSlim ( 1 ) ;
@@ -33,8 +33,7 @@ private class ConcurrentRequest
3333 private string ? _cacheFolder = null ;
3434 protected InMemoryStorage < T > ? InMemoryFileStorage = new ( ) ;
3535
36- private ConcurrentDictionary < string , ConcurrentRequest > _concurrentTasks =
37- new ConcurrentDictionary < string , ConcurrentRequest > ( ) ;
36+ private readonly ConcurrentDictionary < string , ConcurrentRequest > _concurrentTasks = new ( ) ;
3837
3938 private HttpMessageHandler ? _httpMessageHandler ;
4039 private HttpClient ? _httpClient = null ;
@@ -346,50 +345,97 @@ private static ulong CreateHash64(string str)
346345 CancellationToken cancellationToken
347346 )
348347 {
349- var instance = default ( T ) ;
350-
351348 var fileName = GetCacheFileName ( uri ) ;
352- _concurrentTasks . TryGetValue ( fileName , out var request ) ;
353349
354- // if similar request exists check if it was preCacheOnly and validate that current request isn't preCacheOnly
355- if ( request != null && request . EnsureCachedCopy && ! preCacheOnly )
350+ // Check if already in memory cache
351+ if ( InMemoryFileStorage ? . MaxItemCount > 0 )
356352 {
357- if ( request . Task != null )
358- await request . Task . ConfigureAwait ( false ) ;
359- request = null ;
353+ var msi = InMemoryFileStorage ? . GetItem ( fileName , CacheDuration ) ;
354+ if ( msi != null )
355+ {
356+ return msi . Item ;
357+ }
360358 }
361359
362- if ( request == null )
363- {
364- request = new ConcurrentRequest ( )
360+ // Atomically get or add
361+ var request = _concurrentTasks . GetOrAdd (
362+ fileName ,
363+ key =>
365364 {
366- Task = GetFromCacheOrDownloadAsync ( uri , fileName , preCacheOnly , cancellationToken ) ,
367- EnsureCachedCopy = preCacheOnly
368- } ;
369-
370- _concurrentTasks [ fileName ] = request ;
371- }
365+ return new ConcurrentRequest (
366+ ( ) => GetFromCacheOrDownloadAsync ( uri , key , preCacheOnly , cancellationToken )
367+ ) ;
368+ }
369+ ) ;
372370
373371 try
374372 {
375- if ( request . Task != null )
376- instance = await request . Task . ConfigureAwait ( false ) ;
373+ // Wait for the task to complete
374+ var itemTask = request . Task ;
375+ var instance = await itemTask . ConfigureAwait ( false ) ;
376+
377+ // --- Handle In-Memory Caching ---
378+ // If the current request is not preCacheOnly, and the instance was successfully retrieved,
379+ // ensure it's in the memory cache.
380+ if ( ! preCacheOnly && instance != null && InMemoryFileStorage is { MaxItemCount : > 0 } )
381+ {
382+ var memItem = InMemoryFileStorage . GetItem ( fileName , CacheDuration ) ;
383+ if ( memItem == null || memItem . Item == null ) // Check if not already in memory or expired
384+ {
385+ var folder = await GetCacheFolderAsync ( ) . ConfigureAwait ( false ) ;
386+ var lastWriteTime = DateTime . Now ;
387+ if ( folder != null )
388+ {
389+ var baseFile = Path . Combine ( folder , fileName ) ;
390+ try
391+ {
392+ if ( File . Exists ( baseFile ) ) // Check existence before FileInfo
393+ lastWriteTime = new FileInfo ( baseFile ) . LastWriteTime ;
394+ }
395+ catch ( IOException ioEx )
396+ {
397+ Debug . WriteLine (
398+ $ "CacheBase: Error getting FileInfo for memory cache update on { fileName } : { ioEx . Message } "
399+ ) ;
400+ }
401+ catch ( Exception ex )
402+ {
403+ Debug . WriteLine (
404+ $ "CacheBase: Error getting FileInfo for memory cache update on { fileName } : { ex . Message } "
405+ ) ;
406+ }
407+ }
408+
409+ var msi = new InMemoryStorageItem < T > ( fileName , lastWriteTime , instance ) ;
410+ InMemoryFileStorage . SetItem ( msi ) ;
411+ }
412+ }
413+
414+ return instance ;
377415 }
378416 catch ( Exception ex )
379417 {
380- Debug . WriteLine ( $ "Image loading failed for (url={ uri } , file={ fileName } ): { ex . Message } ") ;
418+ Debug . WriteLine ( $ "CacheBase: Exception during GetItemAsync for { fileName } (URI: { uri } ): { ex } ") ;
419+
420+ // Attempt to remove the entry associated with the failed task.
421+ _concurrentTasks . TryRemove ( new KeyValuePair < string , ConcurrentRequest > ( fileName , request ) ) ;
381422
382423 if ( throwOnError )
383424 {
384425 throw ;
385426 }
427+ return default ;
386428 }
387429 finally
388430 {
389- _concurrentTasks . TryRemove ( fileName , out _ ) ;
431+ // If the request was created and its underlying task completed successfully, remove the entry.
432+ // Ensure we don't remove entries for tasks still running.
433+ if ( request . IsCompletedSuccessfully )
434+ {
435+ // Remove the entry only if it still contains the Lazy instance we worked with.
436+ _concurrentTasks . TryRemove ( new KeyValuePair < string , ConcurrentRequest > ( fileName , request ) ) ;
437+ }
390438 }
391-
392- return instance ;
393439 }
394440
395441 private async Task < T ? > GetFromCacheOrDownloadAsync (
@@ -519,6 +565,8 @@ CancellationToken cancellationToken
519565 {
520566 var instance = default ( T ) ;
521567
568+ Debug . WriteLine ( $ "CacheBase Getting: { uri } ") ;
569+
522570 using var ms = new MemoryStream ( ) ;
523571 await using ( var stream = await HttpClient . GetStreamAsync ( uri , cancellationToken ) )
524572 {
0 commit comments