CultLib/src/GameCult.Caching.MessagePack/CultCacheMessagePack.cs
Meta Vens dbc87b9447 C# core: variant records, top-level Set overrides, resolution in the admitting hold
A variant record stores [baseKey, overrides[]] in record slot 4 and an empty
payload; the cache resolves it against its base inside the hold that admits it or
any record on its base chain, and re-resolves dependents in the same Apply, so a
base change reaches its variants with contiguous sequences and no stamp. Plain
Upsert at a variant key is refused (Flatten is the explicit way); cycle, missing
base, different concrete type, evicted base with survivors and a name that
shadows the base are refused naming the keys. Overrides of a dropped slot or an
undecodable value are ignored with a warning and reported as compatible drift.

The store writes cultcache.store.v2 only when it holds a variant and reads v1 and
v2. The codec pair CultInspectorModel took becomes CultCodec, shared with the
cache, which needs it to clone documents and overlay override values. The
directory store, CultMesh single-file reads and CultNet row building refuse a
variant by name until their own cuts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:51:53 +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.OptionsFor(document.Assembly)),
(type, document, bytes) => type == document
? CultDocumentMessagePackSerialization.DeserializeUntyped(type, bytes, registry)
: global::MessagePack.MessagePackSerializer.Deserialize(type, bytes, CultDocumentMessagePackSerialization.OptionsFor(document.Assembly)),
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));
}
}
}