using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database.Tables; using Microsoft.EntityFrameworkCore; namespace Iceshrimp.Backend.Core.Services; public class MetaService([FromKeyedServices("cache")] DatabaseContext db) { public async Task Get(Meta meta) => meta.ConvertGet(await Fetch(meta.Key)); public async Task> GetMany(params Meta[] entities) { var res = await FetchMany(entities.Select(p => p.Key)); return entities.Select(p => p.ConvertGet(res.GetValueOrDefault(p.Key, null))).ToArray(); } public async Task EnsureSet(Meta meta, T value) => await EnsureSet(meta, () => value); public async Task EnsureSet(Meta meta, Func value) { if (await Fetch(meta.Key) != null) return; await Set(meta, value()); } public async Task EnsureSet(Meta meta, Func> value) { if (await Fetch(meta.Key) != null) return; await Set(meta, await value()); } public async Task EnsureSet(IReadOnlyList> metas, Func> values) { if (await db.MetaStore.CountAsync(p => metas.Select(m => m.Key).Contains(p.Key)) == metas.Count) return; var resolvedValues = values(); if (resolvedValues.Count != metas.Count) throw new Exception("Metas count doesn't match values count"); for (var i = 0; i < metas.Count; i++) await Set(metas[i], resolvedValues[i]); } public async Task Set(Meta meta, T value) => await Set(meta.Key, meta.ConvertSet(value)); // Ensures the table is in memory (we could use pg_prewarm for this but that extension requires superuser privileges to install) public async Task WarmupCache() => await db.MetaStore.ToListAsync(); private async Task Fetch(string key) => await db.MetaStore.Where(p => p.Key == key).Select(p => p.Value).FirstOrDefaultAsync(); private async Task> FetchMany(IEnumerable keys) => await db.MetaStore.Where(p => keys.Contains(p.Key)) .ToDictionaryAsync(p => p.Key, p => p.Value); private async Task Set(string key, string? value) { var entity = await db.MetaStore.FirstOrDefaultAsync(p => p.Key == key); if (entity != null) { entity.Value = value; await db.SaveChangesAsync(); } else { await db.MetaStore.Upsert(new MetaStoreEntry { Key = key, Value = value }) .On(p => p.Key) .WhenMatched((_, orig) => new MetaStoreEntry { Value = orig.Value, }) .RunAsync(); } } } public static class MetaEntity { public static readonly StringMeta VapidPrivateKey = new("vapid_private_key"); public static readonly StringMeta VapidPublicKey = new("vapid_public_key"); public static readonly NullableStringMeta InstanceName = new("instance_name"); public static readonly NullableStringMeta InstanceDescription = new("instance_description"); public static readonly NullableStringMeta AdminContactEmail = new("admin_contact_email"); } public class Meta( string key, Func getConverter, Func setConverter, bool isNullable = true ) : Meta(key, typeof(T), isNullable, val => val != null ? getConverter(val) : null) { public Func ConvertGet => getConverter; public Func ConvertSet => setConverter; } public class Meta(string key, Type type, bool isNullable, Func cacheConverter) { public Type Type => type; public bool IsNullable => isNullable; public string Key => key; public Func ConvertCache => cacheConverter; } public class StringMeta(string key) : NonNullableMeta(key, val => val, val => val); public class NullableStringMeta(string key) : Meta(key, val => val, val => val); public class IntMeta(string key) : NonNullableMeta(key, int.Parse, val => val.ToString()); public class NullableIntMeta(string key) : Meta(key, val => int.TryParse(val, out var result) ? result : null, val => val?.ToString()); public class NonNullableMeta(string key, Func getConverter, Func setConverter) : Meta(key, val => getConverter(val ?? throw new Exception($"Fetched meta value {key} was null")), setConverter, false);