Skip to content

Commit

Permalink
Improve persistent storage by removing user liability on making a fil…
Browse files Browse the repository at this point in the history
…ename safe request key
  • Loading branch information
aritchie committed Jun 29, 2024
1 parent 9d86661 commit 079c72f
Show file tree
Hide file tree
Showing 13 changed files with 240 additions and 125 deletions.
4 changes: 3 additions & 1 deletion src/Shiny.Mediator.Caching/CacheExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Shiny.Mediator.Caching;
using Shiny.Mediator.Caching.Infrastructure;
using Shiny.Mediator.Middleware;

namespace Shiny.Mediator;

Expand All @@ -10,6 +11,7 @@ public static class CacheExtensions
public static ShinyConfigurator AddMemoryCaching(this ShinyConfigurator cfg, Action<MemoryCacheOptions> configureCache)
{
cfg.Services.AddMemoryCache(configureCache);
cfg.Services.AddSingleton<IEventHandler<FlushAllStoresEvent>, FlushCacheEventHandler>();
cfg.AddOpenRequestMiddleware(typeof(CachingRequestMiddleware<,>));
return cfg;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Microsoft.Extensions.Caching.Memory;
using Shiny.Mediator.Infrastructure;

namespace Shiny.Mediator.Caching;
namespace Shiny.Mediator.Caching.Infrastructure;


public class CachingRequestMiddleware<TRequest, TResult>(IMemoryCache cache) : IRequestMiddleware<TRequest, TResult>
Expand Down Expand Up @@ -36,8 +35,15 @@ CancellationToken cancellationToken
);
return result!;
}


protected virtual string GetCacheKey(object request, IRequestHandler handler)
=> Utils.GetRequestKey(request);
{
if (request is IRequestKey keyProvider)
return keyProvider.GetKey();

var t = request.GetType();
var key = $"{t.Namespace}_{t.Name}";
return key;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Microsoft.Extensions.Caching.Memory;
using Shiny.Mediator.Middleware;

namespace Shiny.Mediator.Caching.Infrastructure;


public class FlushCacheEventHandler(IMemoryCache cache) : IEventHandler<FlushAllStoresEvent>
{
public Task Handle(FlushAllStoresEvent @event, CancellationToken cancellationToken)
{
cache.Clear();
return Task.CompletedTask;
}
}
11 changes: 11 additions & 0 deletions src/Shiny.Mediator.Maui/Infrastructure/IStorageManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Shiny.Mediator.Infrastructure;

public interface IStorageManager
{
void Store(object request, object result, bool isPeristent);

TResult? Get<TResult>(object request, bool isPeristent);

void ClearAll();
// TODO: remove(request) & clearall, clearbyrequesttype
}
139 changes: 139 additions & 0 deletions src/Shiny.Mediator.Maui/Infrastructure/Impl/StorageManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
using System.Text.Json;

namespace Shiny.Mediator.Infrastructure.Impl;


// TODO: can the request keys get "dirty" - chances are yes
public class StorageManager(IFileSystem fileSystem) : IStorageManager
{
readonly Dictionary<string, object> memCache = new();
Dictionary<string, string> keys = null!;


public void Store(object request, object result, bool isPersistent)
{
if (isPersistent)
{
var path = this.GetFilePath(request, true);
var json = JsonSerializer.Serialize(result);
File.WriteAllText(path, json);
}
else
{
var key = this.GetStoreKeyFromRequest(request);
lock (this.memCache)
this.memCache[key] = result!;
}
}


public TResult? Get<TResult>(object request, bool isPersistent)
{
TResult? returnValue = default;

if (isPersistent)
{
var path = this.GetFilePath(request, false);
if (File.Exists(path))
{
var json = File.ReadAllText(path);
var obj = JsonSerializer.Deserialize<TResult>(json)!;
returnValue = obj;
}
}
else
{
var key = this.GetStoreKeyFromRequest(request);
lock (this.memCache)
{
if (this.memCache.ContainsKey(key))
{
var item = this.memCache[key];
returnValue = (TResult)item;
}
}
}
return returnValue;
}

public void ClearAll()
{
lock (this.memCache)
this.memCache.Clear();

lock (this.keys)
this.keys.Clear();

Directory.GetFiles(fileSystem.CacheDirectory, "*.mediator").ToList().ForEach(File.Delete);
Directory.GetFiles(fileSystem.AppDataDirectory, "*.mediator").ToList().ForEach(File.Delete);
}


string GetStoreKeyFromRequest(object request)
{
if (request is IRequestKey keyProvider)
return keyProvider.GetKey();

var t = request.GetType();
var key = $"{t.Namespace}_{t.Name}";

return key;
}


protected virtual string GetPersistentStoreKey(object request, bool createIfNotExists)
{
var key = this.GetStoreKeyFromRequest(request);
this.EnsureKeyLoad();
if (!this.keys.ContainsKey(key))
{
key = this.keys[key];
}
else if (createIfNotExists)
{
var newKey = Guid.NewGuid().ToString();
this.keys.Add(key, newKey);
key = newKey;

this.PersistKeyStore();
}

return key;
}


protected virtual string GetFilePath(object request, bool createIfNotExists)
{
var key = this.GetPersistentStoreKey(request, createIfNotExists);
var path = Path.Combine(fileSystem.CacheDirectory, $"{key}.mediator");
return path;
}


bool initialized = false;
void EnsureKeyLoad()
{
if (this.initialized)
return;

var storePath = Path.Combine(fileSystem.AppDataDirectory, "keys.mediator");
if (File.Exists(storePath))
{
var json = File.ReadAllText(storePath);
this.keys = JsonSerializer.Deserialize<Dictionary<string, string>>(json)!;
}
else
{
this.keys = new();
}
this.initialized = true;
}


void PersistKeyStore()
{
var storePath = Path.Combine(fileSystem.AppDataDirectory, "keys.mediator");
var json = JsonSerializer.Serialize(this.keys);
File.WriteAllText(storePath, json);
}
}
19 changes: 15 additions & 4 deletions src/Shiny.Mediator.Maui/MauiMediatorExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Shiny.Mediator.Infrastructure;
using Shiny.Mediator.Infrastructure.Impl;
using Shiny.Mediator.Maui;
using Shiny.Mediator.Middleware;

Expand All @@ -20,6 +22,16 @@ public static void RunInBackground(this Task task, ILogger errorLogger)
errorLogger.LogError(x.Exception, "Fire & Forget trapped error");
}, TaskContinuationOptions.OnlyOnFaulted);


internal static ShinyConfigurator EnsureInfrastructure(this ShinyConfigurator cfg)
{
cfg.Services.TryAddSingleton<IEventHandler<FlushAllStoresEvent>, FlushAllCacheEventHandler>();
cfg.Services.TryAddSingleton<IStorageManager, StorageManager>();
cfg.Services.TryAddSingleton(Connectivity.Current);
cfg.Services.TryAddSingleton(FileSystem.Current);
return cfg;
}

/// <summary>
/// Adds Maui Event Collector to mediator
/// </summary>
Expand Down Expand Up @@ -64,18 +76,17 @@ public static ShinyConfigurator AddMainThreadMiddleware(this ShinyConfigurator c

public static ShinyConfigurator AddOfflineAvailabilityMiddleware(this ShinyConfigurator cfg)
{
cfg.Services.TryAddSingleton(Connectivity.Current);
cfg.Services.TryAddSingleton(FileSystem.Current);
cfg.EnsureInfrastructure();
cfg.Services.AddSingletonAsImplementedInterfaces<OfflineAvailableFlushRequestHandler>();
cfg.AddOpenRequestMiddleware(typeof(OfflineAvailableRequestMiddleware<,>));

return cfg;
}


public static ShinyConfigurator AddReplayStreamMiddleware(this ShinyConfigurator cfg)
{
cfg.Services.TryAddSingleton(Connectivity.Current);
cfg.Services.TryAddSingleton(FileSystem.Current);
cfg.EnsureInfrastructure();
cfg.AddOpenStreamMiddleware(typeof(ReplayStreamMiddleware<,>));
return cfg;
}
Expand Down
13 changes: 13 additions & 0 deletions src/Shiny.Mediator.Maui/Middleware/FlushAllCacheEventHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Shiny.Mediator.Infrastructure;

namespace Shiny.Mediator.Middleware;


public class FlushAllCacheEventHandler(IStorageManager storage) : IEventHandler<FlushAllStoresEvent>
{
public Task Handle(FlushAllStoresEvent @event, CancellationToken cancellationToken)
{
storage.ClearAll();
return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
using System.Text.Json;
using Shiny.Mediator.Infrastructure;

namespace Shiny.Mediator.Middleware;


public class OfflineAvailableRequestMiddleware<TRequest, TResult>(IConnectivity connectivity, IFileSystem fileSystem) : IRequestMiddleware<TRequest, TResult>
public class OfflineAvailableRequestMiddleware<TRequest, TResult>(IConnectivity connectivity, IStorageManager storage) : IRequestMiddleware<TRequest, TResult>
{
readonly Dictionary<string, object> memCache = new();

public async Task<TResult> Process(
TRequest request,
RequestHandlerDelegate<TResult> next,
Expand All @@ -28,70 +25,12 @@ CancellationToken cancellationToken
{
result = await next().ConfigureAwait(false);
if (result != null)
this.Store(request!, result, cfg.AvailableAcrossSessions);
storage.Store(request!, result, cfg.AvailableAcrossSessions);
}
else
{
result = this.GetFromStore(request, cfg.AvailableAcrossSessions);
result = storage.Get<TResult>(request, cfg.AvailableAcrossSessions);
}
return result;
}

protected virtual string GetStoreKey(object request)
=> Utils.GetRequestKey(request);


protected virtual string GetFilePath(object request)
{
var key = this.GetStoreKey(request);
var path = Path.Combine(fileSystem.CacheDirectory, $"{key}.off");
return path;
}


protected virtual void Store(object request, object result, bool acrossSessions)
{
if (acrossSessions)
{
var path = this.GetFilePath(request);
var json = JsonSerializer.Serialize(result);
File.WriteAllText(path, json);
}
else
{
var key = this.GetStoreKey(request);
lock (this.memCache)
this.memCache[key] = result!;
}
}


protected virtual TResult? GetFromStore(object request, bool acrossSessions)
{
TResult? returnValue = default;

if (acrossSessions)
{
var path = this.GetFilePath(request);
if (File.Exists(path))
{
var json = File.ReadAllText(path);
var obj = JsonSerializer.Deserialize<TResult>(json)!;
returnValue = obj;
}
}
else
{
var key = this.GetStoreKey(request);
lock (this.memCache)
{
if (this.memCache.ContainsKey(key))
{
var item = this.memCache[key];
returnValue = (TResult)item;
}
}
}
return returnValue;
}
}
Loading

0 comments on commit 079c72f

Please sign in to comment.