using System.Linq.Expressions;
using System.Runtime.Serialization;
using System.Text.Json;
using AsyncKeyedLock;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Shared.Configuration;
using Microsoft.EntityFrameworkCore;
namespace Iceshrimp.Backend.Core.Services;
///
/// This is needed because static fields in generic classes aren't shared between instances with different generic type
/// arguments
///
file static class PluginStoreHelpers
{
public static readonly AsyncKeyedLocker KeyedLocker = new(o =>
{
o.PoolSize = 10;
o.PoolInitialFill = 2;
});
}
public class PluginStore(DatabaseContext db) where TPlugin : IPlugin, new() where TData : new()
{
private readonly IPlugin _plugin = new TPlugin();
///
public async Task GetDataAsync()
{
return (await GetOrCreateDataAsync()).data;
}
///
public async Task GetDataAsync(Expression> predicate)
{
var (_, data) = await GetOrCreateDataAsync();
return predicate.Compile().Invoke(data);
}
///
public async Task UpdateDataAsync(Action updateAction)
{
using (await PluginStoreHelpers.KeyedLocker.LockAsync(_plugin.Id))
{
var (entry, data) = await GetOrCreateDataAsync();
updateAction(data);
UpdateEntryIfModified(entry, data);
await db.SaveChangesAsync();
}
}
private static void UpdateEntryIfModified(PluginStoreEntry entry, TData data)
{
var serialized = JsonSerializer.Serialize(data, JsonSerialization.Options);
if (entry.Data != serialized)
entry.Data = serialized;
}
///
private async Task<(PluginStoreEntry entry, TData data)> GetOrCreateDataAsync()
{
TData data;
var entry = await db.PluginStore.FirstOrDefaultAsync(p => p.Id == _plugin.Id);
if (entry == null)
{
data = new TData();
entry = new PluginStoreEntry
{
Id = _plugin.Id,
Name = _plugin.Name,
Data = JsonSerializer.Serialize(data)
};
db.Add(entry);
}
else
{
data = JsonSerializer.Deserialize(entry.Data, JsonSerialization.Options) ??
throw new SerializationException("Failed to deserialize plugin data");
}
return (entry, data);
}
}