Skip to content

Commit

Permalink
This cache manager is HOT
Browse files Browse the repository at this point in the history
  • Loading branch information
aritchie committed Jun 5, 2024
1 parent 70b1b35 commit 910e1e2
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 37 deletions.
3 changes: 2 additions & 1 deletion src/Shiny.Mediator.Maui/MauiMediatorExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ public static ShinyConfigurator AddTimedMiddleware(this ShinyConfigurator cfg, T

public static ShinyConfigurator AddConnectivityCacheMiddleware(this ShinyConfigurator cfg)
{
cfg.AddOpenRequestMiddleware(typeof(ConnectivityCacheRequestMiddleware<,>));
// TODO: how to clear memory
cfg.AddOpenRequestMiddleware(typeof(CacheRequestMiddleware<,>));
return cfg;
}

Expand Down
206 changes: 206 additions & 0 deletions src/Shiny.Mediator.Maui/Middleware/CacheRequestMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
using System.Text.Json;
using Microsoft.Extensions.Configuration;

namespace Shiny.Mediator.Middleware;


public class CacheRequestMiddleware<TRequest, TResult>(
IConfiguration configuration,
IConnectivity connectivity,
IFileSystem fileSystem
) : IRequestMiddleware<TRequest, TResult> where TRequest : IRequest<TResult>
// IRequestHandler<FlushAllCacheRequest>,
// IRequestHandler<FlushCacheItemRequest>
{
public async Task<TResult> Process(TRequest request, RequestHandlerDelegate<TResult> next, CancellationToken cancellationToken)
{
// no caching for void requests
if (typeof(TResult) == typeof(Unit))
return await next().ConfigureAwait(false);

// no config no cache - TODO: could consider ICacheItem taking default values
var cfg = GetConfiguration(request);
if (cfg == null)
return await next().ConfigureAwait(false);

var result = default(TResult);
var connected = connectivity.NetworkAccess == NetworkAccess.Internet;
if (cfg.OnlyForOffline && connected)
{
result = await this.GetAndStore(request, next, cfg).ConfigureAwait(false);
return result;
}

var item = this.GetFromStore(request, cfg);
if (item == null)
{
if (connected)
result = await this.GetAndStore(request, next, cfg).ConfigureAwait(false);
}
else if (this.IsExpired(item, cfg))
{
result = item.Value;
if (connected)
result = await this.GetAndStore(request, next, cfg).ConfigureAwait(false);
}
else
result = item.Value;

return result;
}


protected virtual bool IsExpired(CachedItem<TResult> item, CacheConfiguration cfg)
{
if (cfg.MaxAge == null)
return false;

var expiry = item.CreatedOn.Add(cfg.MaxAge.Value);
var expired = expiry < DateTimeOffset.UtcNow;
return expired;
}


protected virtual async Task<TResult> GetAndStore(TRequest request, RequestHandlerDelegate<TResult> next, CacheConfiguration cfg)
{
var result = await next().ConfigureAwait(false);
this.Store(request, result, cfg);
return result;
}

protected virtual string GetCacheKey(TRequest request)
{
if (request is ICacheItem item)
return item.CacheKey;

var key = $"{typeof(TRequest).Namespace}.{typeof(TRequest).Name}";
return key;
}


protected virtual string GetCacheFilePath(TRequest request)
{
var key = this.GetCacheKey(request);
var path = Path.Combine(fileSystem.CacheDirectory, $"{key}.cache");
return path;
}


protected virtual void Store(TRequest request, TResult result, CacheConfiguration cfg)
{
switch (cfg.Storage)
{
case StoreType.File:
var path = this.GetCacheFilePath(request);
var json = JsonSerializer.Serialize(result);
File.WriteAllText(path, json);
break;

case StoreType.Memory:
var key = this.GetCacheKey(request);
lock (this.memCache)
{
this.memCache[key] = new CachedItem<object>(
DateTimeOffset.UtcNow,
result!
);
}
break;
}
}


protected virtual CachedItem<TResult>? GetFromStore(TRequest request, CacheConfiguration cfg)
{
CachedItem<TResult>? returnValue = null;

switch (cfg.Storage)
{
case StoreType.File:
var path = this.GetCacheFilePath(request);
if (File.Exists(path))
{
var json = File.ReadAllText(path);
var obj = JsonSerializer.Deserialize<TResult>(json)!;
var createdOn = File.GetCreationTimeUtc(path);
returnValue = new CachedItem<TResult>(createdOn, obj);
}
break;


case StoreType.Memory:
var key = this.GetCacheKey(request);
lock (this.memCache)
{
if (this.memCache.ContainsKey(key))
{
var item = this.memCache[key];
returnValue = new CachedItem<TResult>(item.CreatedOn, (TResult)item.Value);
}
}
break;
}

return returnValue;
}


readonly Dictionary<string, CachedItem<object>> memCache = new();
readonly object syncLock = new();
CacheConfiguration[]? configurations;


protected virtual CacheConfiguration? GetConfiguration(TRequest request)
{
var type = request.GetType();
var key = $"{type.Namespace}.{type.Name}";

if (this.configurations == null)
{
lock (this.syncLock)
{
this.configurations = configuration
.GetSection("Cache")
.Get<CacheConfiguration[]>();
}
}

var cfg = this.configurations?
.FirstOrDefault(x => x
.RequestType
.Equals(
key,
StringComparison.InvariantCultureIgnoreCase
)
);

return cfg;
}
}

// public record FlushCacheItemRequest(Type RequestType) : IRequest;
// public record FlushAllCacheRequest : IRequest;

public interface ICacheItem
{
string CacheKey { get; }
}

public record CachedItem<T>(
DateTimeOffset CreatedOn,
T Value
);

public enum StoreType
{
File,
Memory
}

public class CacheConfiguration
{
public string RequestType { get; set; }
public bool OnlyForOffline { get; set; }
public TimeSpan? MaxAge { get; set; }
public StoreType Storage { get; set; }
}

This file was deleted.

1 change: 1 addition & 0 deletions src/Shiny.Mediator.Maui/Shiny.Mediator.Maui.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
<PackageReference Include="Microsoft.Maui.Controls" Version="8.0.40" />
<ProjectReference Include="..\Shiny.Mediator\Shiny.Mediator.csproj" />
</ItemGroup>
Expand Down
6 changes: 6 additions & 0 deletions tests/Shiny.Mediator.Tests/CacheRequestMiddlewareTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Shiny.Mediator.Tests;

public class CacheRequestMiddlewareTests
{

}

0 comments on commit 910e1e2

Please sign in to comment.