CultLib/src/GameCult.Caching.MessagePack/CultCacheMessagePack.cs
Meta Vens b091d21f81 CultCache: list elements are written by their [Key] layout; registration and the codec refuse otherwise
Registration: a concrete element type of an object list that is not keyed, and
[MessagePackFormatter] on any type or member on the id path or on the id member, are
refused. The id path is a fixed point over the walk's edges. The codec refuses, once per
document type and on every encode and decode path, a resolver ahead of StandardResolver
that formats a type on that path. UnionArmGuardTests moves StubArm out of the list union.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 21:52:36 +02:00

65 lines
3 KiB
C#

using System;
using System.IO;
using System.Threading.Tasks;
namespace GameCult.Caching.MessagePack
{
public sealed class CultCacheOpenOptions
{
public CultDocumentRegistry? Registry { get; set; }
public bool ReadOnly { get; set; }
public bool FlushOnDispose { get; set; }
public bool StoreFlushOnDispose { get; set; }
public bool UseDirectoryStore { get; set; }
}
public static class CultCacheMessagePack
{
// The store's encoding as one port. Values serialize without the document guard but with their owning document's
// options, resolvers included, so dictionary keys compare by the bytes the store writes; a document type
// deserializes as a registered document, a member type as itself.
public static CultCodec CreateCodec(CultDocumentRegistry registry) => new CultCodec(
(value, type, document) => global::MessagePack.MessagePackSerializer.Serialize(type, value, CultDocumentMessagePackSerialization.OptionsForDocument(document)),
(type, document, bytes) => type == document
? CultDocumentMessagePackSerialization.DeserializeUntyped(type, bytes, registry)
: global::MessagePack.MessagePackSerializer.Deserialize(type, bytes, CultDocumentMessagePackSerialization.OptionsForDocument(document)),
CultDocumentMessagePackSerialization.OverlaySlots);
// The inspection model over that codec.
public static CultInspectorModel CreateInspectorModel(CultDocumentRegistry registry) =>
new CultInspectorModel(registry, CreateCodec(registry));
// Opening hydrates. A consumer that wants a fresh store removes the records it does not keep, upserts, and
// flushes; the atomic replace does the rest.
public static CultCache Create(string filePath, CultCacheOpenOptions? options = null)
{
if (string.IsNullOrWhiteSpace(filePath))
{
throw new ArgumentException("File path must be non-empty.", nameof(filePath));
}
options ??= new CultCacheOpenOptions();
var registry = options.Registry ?? CultDocumentRegistry.Shared;
var cache = new CultCache(registry, CreateCodec(registry))
{
FlushAttachedStoresOnDispose = options.FlushOnDispose
};
CacheBackingStore store = options.UseDirectoryStore || Directory.Exists(DirectoryMessagePackBackingStore.DefaultRecordDirectoryPath(filePath))
? new DirectoryMessagePackBackingStore(filePath, readOnly: options.ReadOnly)
: new SingleFileMessagePackBackingStore(filePath, options.ReadOnly);
store.FlushOnDispose = options.StoreFlushOnDispose;
cache.AddBackingStore(store);
return cache;
}
public static Task<CultCache> OpenAsync(string filePath, CultCacheOpenOptions? options = null)
{
return Task.FromResult(Create(filePath, options));
}
}
}