Document CultCache and CultNet public surface
This commit is contained in:
parent
08208b9722
commit
322cda12a6
11 changed files with 995 additions and 5 deletions
|
|
@ -9,10 +9,16 @@ using Microsoft.CodeAnalysis.Text;
|
|||
|
||||
namespace GameCult.Caching.MessagePack.Generator
|
||||
{
|
||||
[Generator]
|
||||
public sealed class CultDocumentMessagePackGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
/// <summary>
|
||||
/// Emits MessagePack payload serializers for CultCache document types.
|
||||
/// </summary>
|
||||
[Generator]
|
||||
public sealed class CultDocumentMessagePackGenerator : IIncrementalGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers the generator pipeline.
|
||||
/// </summary>
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
var documentShapes = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(
|
||||
|
|
|
|||
|
|
@ -7,11 +7,20 @@ using MessagePack.Resolvers;
|
|||
|
||||
namespace GameCult.Caching.MessagePack;
|
||||
|
||||
/// <summary>
|
||||
/// MessagePack resolver for CultCache-specific value types.
|
||||
/// </summary>
|
||||
public sealed class CultDocumentResolver : IFormatterResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the shared resolver instance.
|
||||
/// </summary>
|
||||
public static readonly CultDocumentResolver Instance = new();
|
||||
private CultDocumentResolver() { }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a formatter for the requested type, when this resolver owns it.
|
||||
/// </summary>
|
||||
public IMessagePackFormatter<T>? GetFormatter<T>()
|
||||
{
|
||||
var type = typeof(T);
|
||||
|
|
@ -25,12 +34,18 @@ public sealed class CultDocumentResolver : IFormatterResolver
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MessagePack serialization helpers for CultCache documents and backing stores.
|
||||
/// </summary>
|
||||
public static class CultDocumentMessagePackSerialization
|
||||
{
|
||||
private const int PersistedRecordFieldCount = 4;
|
||||
private const int SchemaCatalogEntryFieldCount = 6;
|
||||
private const int StoreSnapshotFieldCount = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the shared MessagePack serializer options for CultCache payloads.
|
||||
/// </summary>
|
||||
public static readonly MessagePackSerializerOptions Options =
|
||||
MessagePackSerializerOptions.Standard
|
||||
.WithResolver(CompositeResolver.Create(
|
||||
|
|
@ -38,16 +53,25 @@ public static class CultDocumentMessagePackSerialization
|
|||
StandardResolver.Instance))
|
||||
.WithSecurity(MessagePackSecurity.UntrustedData);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a typed value with the CultCache MessagePack options.
|
||||
/// </summary>
|
||||
public static byte[] Serialize<T>(T value)
|
||||
{
|
||||
return MessagePackSerializer.Serialize(value, Options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a typed value with the CultCache MessagePack options.
|
||||
/// </summary>
|
||||
public static T Deserialize<T>(byte[] payload)
|
||||
{
|
||||
return MessagePackSerializer.Deserialize<T>(payload, Options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a value whose document type is known at runtime.
|
||||
/// </summary>
|
||||
public static byte[] SerializeUntyped(object value, Type type)
|
||||
{
|
||||
if (value != null)
|
||||
|
|
@ -62,6 +86,9 @@ public static class CultDocumentMessagePackSerialization
|
|||
return MessagePackSerializer.Serialize(type, value, Options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a value whose document type is known at runtime.
|
||||
/// </summary>
|
||||
public static object DeserializeUntyped(Type type, byte[] payload)
|
||||
{
|
||||
var descriptor = CultDocumentRegistry.Shared.GetRequired(type);
|
||||
|
|
@ -73,6 +100,9 @@ public static class CultDocumentMessagePackSerialization
|
|||
return MessagePackSerializer.Deserialize(type, payload, Options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes one persisted store record.
|
||||
/// </summary>
|
||||
public static byte[] SerializePersistedRecord(CultPersistedRecord record)
|
||||
{
|
||||
var buffer = new global::System.Buffers.ArrayBufferWriter<byte>();
|
||||
|
|
@ -82,12 +112,18 @@ public static class CultDocumentMessagePackSerialization
|
|||
return buffer.WrittenSpan.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes one persisted store record.
|
||||
/// </summary>
|
||||
public static CultPersistedRecord DeserializePersistedRecord(byte[] payload)
|
||||
{
|
||||
var reader = new MessagePackReader(payload);
|
||||
return ReadPersistedRecord(ref reader);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a schema catalog.
|
||||
/// </summary>
|
||||
public static byte[] SerializeSchemaCatalog(CultSchemaCatalogEntry[] catalog)
|
||||
{
|
||||
var buffer = new global::System.Buffers.ArrayBufferWriter<byte>();
|
||||
|
|
@ -102,6 +138,9 @@ public static class CultDocumentMessagePackSerialization
|
|||
return buffer.WrittenSpan.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a schema catalog.
|
||||
/// </summary>
|
||||
public static CultSchemaCatalogEntry[] DeserializeSchemaCatalog(byte[] payload)
|
||||
{
|
||||
var reader = new MessagePackReader(payload);
|
||||
|
|
@ -115,6 +154,9 @@ public static class CultDocumentMessagePackSerialization
|
|||
return catalog;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a complete persisted store snapshot.
|
||||
/// </summary>
|
||||
public static byte[] SerializeSnapshot(CultPersistedStoreSnapshot snapshot)
|
||||
{
|
||||
var buffer = new global::System.Buffers.ArrayBufferWriter<byte>();
|
||||
|
|
@ -137,6 +179,9 @@ public static class CultDocumentMessagePackSerialization
|
|||
return buffer.WrittenSpan.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a complete persisted store snapshot.
|
||||
/// </summary>
|
||||
public static CultPersistedStoreSnapshot DeserializeSnapshot(byte[] payload)
|
||||
{
|
||||
var reader = new MessagePackReader(payload);
|
||||
|
|
@ -282,27 +327,45 @@ public static class CultDocumentMessagePackSerialization
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Single-file CultCache backing store that persists snapshots as MessagePack.
|
||||
/// </summary>
|
||||
public class SingleFileMessagePackBackingStore : SingleFileBackingStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a MessagePack single-file backing store.
|
||||
/// </summary>
|
||||
public SingleFileMessagePackBackingStore(string filePath) : base(filePath)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a store snapshot.
|
||||
/// </summary>
|
||||
protected override byte[] SerializeSnapshot(CultPersistedStoreSnapshot snapshot)
|
||||
{
|
||||
return CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a store snapshot.
|
||||
/// </summary>
|
||||
protected override CultPersistedStoreSnapshot DeserializeSnapshot(byte[] data)
|
||||
{
|
||||
return CultDocumentMessagePackSerialization.DeserializeSnapshot(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes one document payload.
|
||||
/// </summary>
|
||||
protected override byte[] SerializePayload(object document)
|
||||
{
|
||||
return CultDocumentMessagePackSerialization.SerializeUntyped(document, document.GetType());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes one document payload.
|
||||
/// </summary>
|
||||
protected override object DeserializePayload(Type documentType, byte[] payload)
|
||||
{
|
||||
return CultDocumentMessagePackSerialization.DeserializeUntyped(documentType, payload);
|
||||
|
|
|
|||
|
|
@ -3,13 +3,22 @@ using MessagePack.Formatters;
|
|||
|
||||
namespace GameCult.Caching;
|
||||
|
||||
/// <summary>
|
||||
/// Serializes typed CultCache record references as their persisted key strings.
|
||||
/// </summary>
|
||||
public sealed class CultRecordRefFormatter<T> : IMessagePackFormatter<CultRecordRef<T>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes a typed record reference to MessagePack.
|
||||
/// </summary>
|
||||
public void Serialize(ref MessagePackWriter writer, CultRecordRef<T> value, MessagePackSerializerOptions options)
|
||||
{
|
||||
options.Resolver.GetFormatterWithVerify<string>().Serialize(ref writer, value.Key.Value, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a typed record reference from MessagePack.
|
||||
/// </summary>
|
||||
public CultRecordRef<T> Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
|
||||
{
|
||||
var key = options.Resolver.GetFormatterWithVerify<string>().Deserialize(ref reader, options) ?? string.Empty;
|
||||
|
|
|
|||
|
|
@ -13,31 +13,82 @@ using R3;
|
|||
|
||||
namespace GameCult.Caching
|
||||
{
|
||||
/// <summary>
|
||||
/// Persisted schema metadata embedded in a CultCache backing store.
|
||||
/// </summary>
|
||||
public sealed class CultSchemaCatalogEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the content-derived schema identifier.
|
||||
/// </summary>
|
||||
public string SchemaId { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the stable schema name.
|
||||
/// </summary>
|
||||
public string SchemaName { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the schema version string.
|
||||
/// </summary>
|
||||
public string SchemaVersion { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the canonical schema content hash.
|
||||
/// </summary>
|
||||
public string ContentHash { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the canonical schema description.
|
||||
/// </summary>
|
||||
public string CanonicalSchemaJson { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets schema identifiers compatible with this entry.
|
||||
/// </summary>
|
||||
public string[] CompatibleSchemaIds { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persisted serialized document record.
|
||||
/// </summary>
|
||||
public sealed class CultPersistedRecord
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the persisted record key.
|
||||
/// </summary>
|
||||
public string Key { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the schema identifier used to deserialize the payload.
|
||||
/// </summary>
|
||||
public string SchemaId { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the storage timestamp.
|
||||
/// </summary>
|
||||
public string StoredAt { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the serialized document payload.
|
||||
/// </summary>
|
||||
public byte[] Payload { get; set; } = Array.Empty<byte>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Complete snapshot persisted by a single-file CultCache backing store.
|
||||
/// </summary>
|
||||
public sealed class CultPersistedStoreSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the backing store format version.
|
||||
/// </summary>
|
||||
public string FormatVersion { get; set; } = "cultcache.store.v1";
|
||||
/// <summary>
|
||||
/// Gets or sets the embedded schema catalog.
|
||||
/// </summary>
|
||||
public CultSchemaCatalogEntry[] SchemaCatalog { get; set; } = Array.Empty<CultSchemaCatalogEntry>();
|
||||
/// <summary>
|
||||
/// Gets or sets the persisted records.
|
||||
/// </summary>
|
||||
public CultPersistedRecord[] Records { get; set; } = Array.Empty<CultPersistedRecord>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime descriptor for a CultCache document type.
|
||||
/// </summary>
|
||||
public sealed class CultDocumentDescriptor
|
||||
{
|
||||
internal CultDocumentDescriptor(
|
||||
|
|
@ -70,20 +121,53 @@ namespace GameCult.Caching
|
|||
Members = members;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the CLR document type.
|
||||
/// </summary>
|
||||
public Type DocumentType { get; }
|
||||
/// <summary>
|
||||
/// Gets the stable schema name.
|
||||
/// </summary>
|
||||
public string SchemaName { get; }
|
||||
/// <summary>
|
||||
/// Gets the schema version string.
|
||||
/// </summary>
|
||||
public string SchemaVersion { get; }
|
||||
/// <summary>
|
||||
/// Gets the content-derived schema identifier.
|
||||
/// </summary>
|
||||
public string SchemaId { get; }
|
||||
/// <summary>
|
||||
/// Gets the canonical schema content hash.
|
||||
/// </summary>
|
||||
public string ContentHash { get; }
|
||||
/// <summary>
|
||||
/// Gets the canonical schema description.
|
||||
/// </summary>
|
||||
public string CanonicalSchemaJson { get; }
|
||||
/// <summary>
|
||||
/// Gets whether this document type stores one global record.
|
||||
/// </summary>
|
||||
public bool IsGlobal { get; }
|
||||
/// <summary>
|
||||
/// Gets the document name member, if any.
|
||||
/// </summary>
|
||||
public string? NameMember { get; }
|
||||
internal Func<object, string?>? NameAccessor { get; }
|
||||
/// <summary>
|
||||
/// Gets the generated payload serializer, if one is available.
|
||||
/// </summary>
|
||||
public Func<object, byte[]>? GeneratedPayloadSerializer { get; }
|
||||
/// <summary>
|
||||
/// Gets the generated payload deserializer, if one is available.
|
||||
/// </summary>
|
||||
public Func<byte[], object>? GeneratedPayloadDeserializer { get; }
|
||||
internal IReadOnlyDictionary<string, Func<object, string>> IndexAccessors { get; }
|
||||
internal IReadOnlyList<CultDocumentMemberDescriptor> Members { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Converts this descriptor into a persisted schema catalog entry.
|
||||
/// </summary>
|
||||
public CultSchemaCatalogEntry ToCatalogEntry()
|
||||
{
|
||||
return new CultSchemaCatalogEntry
|
||||
|
|
@ -118,8 +202,14 @@ namespace GameCult.Caching
|
|||
public string? IndexAlias { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores a resolved document together with its descriptor and key.
|
||||
/// </summary>
|
||||
public sealed class CultStoredDocument
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a stored document wrapper.
|
||||
/// </summary>
|
||||
public CultStoredDocument(
|
||||
CultRecordKey key,
|
||||
string storedAt,
|
||||
|
|
@ -132,12 +222,27 @@ namespace GameCult.Caching
|
|||
Document = document;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the record key.
|
||||
/// </summary>
|
||||
public CultRecordKey Key { get; }
|
||||
/// <summary>
|
||||
/// Gets the storage timestamp.
|
||||
/// </summary>
|
||||
public string StoredAt { get; }
|
||||
/// <summary>
|
||||
/// Gets the document descriptor.
|
||||
/// </summary>
|
||||
public CultDocumentDescriptor Descriptor { get; }
|
||||
/// <summary>
|
||||
/// Gets the document instance.
|
||||
/// </summary>
|
||||
public object Document { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discovers, indexes, and resolves CultCache document descriptors.
|
||||
/// </summary>
|
||||
public sealed class CultDocumentRegistry
|
||||
{
|
||||
private static readonly Lazy<CultDocumentRegistry> SharedRegistry =
|
||||
|
|
@ -149,15 +254,27 @@ namespace GameCult.Caching
|
|||
private readonly ConcurrentDictionary<string, CultDocumentDescriptor> _bySchemaName =
|
||||
new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the shared process-wide document registry.
|
||||
/// </summary>
|
||||
public static CultDocumentRegistry Shared => SharedRegistry.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a registry and discovers currently loaded document metadata.
|
||||
/// </summary>
|
||||
public CultDocumentRegistry()
|
||||
{
|
||||
Refresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all known document descriptors.
|
||||
/// </summary>
|
||||
public IEnumerable<CultDocumentDescriptor> AllDescriptors => _byType.Values.OrderBy(d => d.SchemaName, StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the registry from generated metadata and reflected document attributes.
|
||||
/// </summary>
|
||||
public void Refresh()
|
||||
{
|
||||
_byType.Clear();
|
||||
|
|
@ -178,6 +295,9 @@ namespace GameCult.Caching
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the descriptor for a document type, building it when needed.
|
||||
/// </summary>
|
||||
public CultDocumentDescriptor GetRequired(Type type)
|
||||
{
|
||||
if (_byType.TryGetValue(type, out var descriptor))
|
||||
|
|
@ -204,11 +324,17 @@ namespace GameCult.Caching
|
|||
return descriptor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the descriptor for a document type.
|
||||
/// </summary>
|
||||
public CultDocumentDescriptor GetRequired<T>() where T : class
|
||||
{
|
||||
return GetRequired(typeof(T));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a descriptor by its schema identifier.
|
||||
/// </summary>
|
||||
public CultDocumentDescriptor GetRequiredBySchemaId(string schemaId)
|
||||
{
|
||||
if (_bySchemaId.TryGetValue(schemaId, out var descriptor))
|
||||
|
|
@ -219,6 +345,9 @@ namespace GameCult.Caching
|
|||
throw new InvalidOperationException($"Unknown CultCache schema id '{schemaId}'.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a persisted schema identifier against the local registry and embedded catalog.
|
||||
/// </summary>
|
||||
public CultDocumentDescriptor ResolvePersistedSchema(string schemaId, IReadOnlyCollection<CultSchemaCatalogEntry> catalog)
|
||||
{
|
||||
if (_bySchemaId.TryGetValue(schemaId, out var exact))
|
||||
|
|
@ -583,6 +712,9 @@ namespace GameCult.Caching
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In-memory document cache with pluggable persisted backing stores.
|
||||
/// </summary>
|
||||
public sealed class CultCache : IDisposable
|
||||
{
|
||||
private readonly CultDocumentRegistry _registry;
|
||||
|
|
@ -594,24 +726,42 @@ namespace GameCult.Caching
|
|||
private readonly ConditionalWeakTable<object, DocumentHandleBox> _documentHandles = new();
|
||||
private ILogger _logger = new NullLogger();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a cache using the supplied document registry or the shared registry.
|
||||
/// </summary>
|
||||
public CultCache(CultDocumentRegistry? registry = null)
|
||||
{
|
||||
_registry = registry ?? CultDocumentRegistry.Shared;
|
||||
InitializeGlobals();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cache logger.
|
||||
/// </summary>
|
||||
public ILogger Logger
|
||||
{
|
||||
get => _logger;
|
||||
set => _logger = value ?? new NullLogger();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised when a backing store adds, updates, or removes a document.
|
||||
/// </summary>
|
||||
public event Action<object?, object?>? OnUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// Gets all document instances currently held by the cache.
|
||||
/// </summary>
|
||||
public IEnumerable<object> AllEntries => _entries.Values.Select(entry => entry.Document);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the document registry used by this cache.
|
||||
/// </summary>
|
||||
public CultDocumentRegistry Registry => _registry;
|
||||
|
||||
/// <summary>
|
||||
/// Attaches a backing store to this cache.
|
||||
/// </summary>
|
||||
public void AddBackingStore(CacheBackingStore store)
|
||||
{
|
||||
if (store == null) throw new ArgumentNullException(nameof(store));
|
||||
|
|
@ -623,6 +773,9 @@ namespace GameCult.Caching
|
|||
_backingStores.Add(store);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pulls all documents from every attached backing store.
|
||||
/// </summary>
|
||||
public async Task PullAllBackingStoresAsync()
|
||||
{
|
||||
foreach (var store in _backingStores)
|
||||
|
|
@ -633,6 +786,9 @@ namespace GameCult.Caching
|
|||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds or replaces a typed document and returns its record handle.
|
||||
/// </summary>
|
||||
public async Task<CultRecordHandle<T>> AddAsync<T>(T document, CultRecordHandle<T>? handle = null)
|
||||
{
|
||||
if (document == null) throw new ArgumentNullException(nameof(document));
|
||||
|
|
@ -643,6 +799,9 @@ namespace GameCult.Caching
|
|||
return new CultRecordHandle<T>(stored.Key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the record handle for a document instance, if it is tracked.
|
||||
/// </summary>
|
||||
public CultRecordHandle<T>? TryGetHandle<T>(T document)
|
||||
{
|
||||
if (document == null) throw new ArgumentNullException(nameof(document));
|
||||
|
|
@ -651,6 +810,9 @@ namespace GameCult.Caching
|
|||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a document by record key.
|
||||
/// </summary>
|
||||
public object? Get(CultRecordKey key)
|
||||
{
|
||||
return _entries.TryGetValue(key.Value, out var stored)
|
||||
|
|
@ -658,11 +820,17 @@ namespace GameCult.Caching
|
|||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a typed document by record key.
|
||||
/// </summary>
|
||||
public T? Get<T>(CultRecordKey key) where T : class
|
||||
{
|
||||
return Get(key) as T;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all cached documents assignable to the requested type.
|
||||
/// </summary>
|
||||
public IEnumerable<T> GetAll<T>() where T : class
|
||||
{
|
||||
var type = typeof(T);
|
||||
|
|
@ -671,6 +839,9 @@ namespace GameCult.Caching
|
|||
.Select(entry => (T)entry.Document);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the global document for the requested type, if one exists.
|
||||
/// </summary>
|
||||
public T? GetGlobal<T>() where T : class
|
||||
{
|
||||
return _globalKeys.TryGetValue(typeof(T), out var key)
|
||||
|
|
@ -678,6 +849,9 @@ namespace GameCult.Caching
|
|||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a typed document by its CultName value.
|
||||
/// </summary>
|
||||
public T? GetByName<T>(string name) where T : class
|
||||
{
|
||||
var type = typeof(T);
|
||||
|
|
@ -690,6 +864,9 @@ namespace GameCult.Caching
|
|||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a typed document by an indexed value.
|
||||
/// </summary>
|
||||
public T? GetByIndex<T>(string alias, string value) where T : class
|
||||
{
|
||||
if (_indexMaps.TryGetValue((typeof(T), alias), out var map) &&
|
||||
|
|
@ -701,11 +878,17 @@ namespace GameCult.Caching
|
|||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a typed document reference against this cache.
|
||||
/// </summary>
|
||||
public T? Resolve<T>(CultRecordRef<T> reference) where T : class
|
||||
{
|
||||
return Get<T>(reference.Key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a document by typed handle.
|
||||
/// </summary>
|
||||
public void Remove<T>(CultRecordHandle<T> handle)
|
||||
{
|
||||
if (_entries.TryGetValue(handle.Key.Value, out var stored))
|
||||
|
|
@ -714,6 +897,9 @@ namespace GameCult.Caching
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes attached disposable backing stores.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var store in _backingStores.OfType<IDisposable>())
|
||||
|
|
@ -904,25 +1090,46 @@ namespace GameCult.Caching
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class for CultCache persistence adapters.
|
||||
/// </summary>
|
||||
public abstract class CacheBackingStore : IDisposable
|
||||
{
|
||||
private CultDocumentRegistry? _registry;
|
||||
private ILogger _logger = new NullLogger();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the attached document registry.
|
||||
/// </summary>
|
||||
protected CultDocumentRegistry Registry =>
|
||||
_registry ?? throw new InvalidOperationException("Backing store is not attached to a CultDocumentRegistry.");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the backing store entries by record key.
|
||||
/// </summary>
|
||||
protected ConcurrentDictionary<string, CultStoredDocument> Entries { get; } =
|
||||
new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the backing store logger.
|
||||
/// </summary>
|
||||
public ILogger Logger
|
||||
{
|
||||
get => _logger;
|
||||
set => _logger = value ?? new NullLogger();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes documents added by the backing store.
|
||||
/// </summary>
|
||||
public Subject<CultStoredDocument> EntryAdded { get; } = new();
|
||||
/// <summary>
|
||||
/// Publishes documents updated by the backing store.
|
||||
/// </summary>
|
||||
public Subject<CultStoredDocument> EntryUpdated { get; } = new();
|
||||
/// <summary>
|
||||
/// Publishes documents deleted by the backing store.
|
||||
/// </summary>
|
||||
public Subject<CultStoredDocument> EntryDeleted { get; } = new();
|
||||
|
||||
internal void AttachRegistry(CultDocumentRegistry registry)
|
||||
|
|
@ -930,10 +1137,25 @@ namespace GameCult.Caching
|
|||
_registry = registry ?? throw new ArgumentNullException(nameof(registry));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pulls all persisted records into the backing store.
|
||||
/// </summary>
|
||||
public abstract void PullAll();
|
||||
/// <summary>
|
||||
/// Pushes one stored document into the backing store.
|
||||
/// </summary>
|
||||
public abstract void Push(CultStoredDocument entry);
|
||||
/// <summary>
|
||||
/// Deletes one stored document from the backing store.
|
||||
/// </summary>
|
||||
public abstract void Delete(CultStoredDocument entry);
|
||||
/// <summary>
|
||||
/// Persists all current backing store entries.
|
||||
/// </summary>
|
||||
public abstract void PushAll(bool soft = false);
|
||||
/// <summary>
|
||||
/// Releases backing store event subjects.
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
EntryAdded.Dispose();
|
||||
|
|
@ -941,6 +1163,9 @@ namespace GameCult.Caching
|
|||
EntryDeleted.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a stored document into a persisted record.
|
||||
/// </summary>
|
||||
protected CultPersistedRecord ToPersistedRecord(CultStoredDocument entry, Func<object, byte[]> serializePayload)
|
||||
{
|
||||
return new CultPersistedRecord
|
||||
|
|
@ -952,6 +1177,9 @@ namespace GameCult.Caching
|
|||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a persisted record into a stored document.
|
||||
/// </summary>
|
||||
protected CultStoredDocument ToStoredDocument(
|
||||
CultPersistedRecord record,
|
||||
IReadOnlyCollection<CultSchemaCatalogEntry> catalog,
|
||||
|
|
@ -967,19 +1195,43 @@ namespace GameCult.Caching
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class for backing stores that persist a complete snapshot to one file.
|
||||
/// </summary>
|
||||
public abstract class SingleFileBackingStore : CacheBackingStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a single-file backing store.
|
||||
/// </summary>
|
||||
protected SingleFileBackingStore(string filePath)
|
||||
{
|
||||
FileInfo = new FileInfo(filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file used by this backing store.
|
||||
/// </summary>
|
||||
protected FileInfo FileInfo { get; }
|
||||
/// <summary>
|
||||
/// Serializes a full store snapshot.
|
||||
/// </summary>
|
||||
protected abstract byte[] SerializeSnapshot(CultPersistedStoreSnapshot snapshot);
|
||||
/// <summary>
|
||||
/// Deserializes a full store snapshot.
|
||||
/// </summary>
|
||||
protected abstract CultPersistedStoreSnapshot DeserializeSnapshot(byte[] data);
|
||||
/// <summary>
|
||||
/// Serializes one document payload.
|
||||
/// </summary>
|
||||
protected abstract byte[] SerializePayload(object document);
|
||||
/// <summary>
|
||||
/// Deserializes one document payload.
|
||||
/// </summary>
|
||||
protected abstract object DeserializePayload(Type documentType, byte[] payload);
|
||||
|
||||
/// <summary>
|
||||
/// Loads every persisted record from disk.
|
||||
/// </summary>
|
||||
public override void PullAll()
|
||||
{
|
||||
if (!FileInfo.Exists)
|
||||
|
|
@ -996,16 +1248,25 @@ namespace GameCult.Caching
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stages one stored document in memory.
|
||||
/// </summary>
|
||||
public override void Push(CultStoredDocument entry)
|
||||
{
|
||||
Entries[entry.Key.Value] = entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes one staged document.
|
||||
/// </summary>
|
||||
public override void Delete(CultStoredDocument entry)
|
||||
{
|
||||
Entries.TryRemove(entry.Key.Value, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the staged snapshot to disk.
|
||||
/// </summary>
|
||||
public override void PushAll(bool soft = false)
|
||||
{
|
||||
var snapshot = new CultPersistedStoreSnapshot
|
||||
|
|
|
|||
|
|
@ -2,9 +2,15 @@ using System;
|
|||
|
||||
namespace GameCult.Caching
|
||||
{
|
||||
/// <summary>
|
||||
/// Marks a type as a CultCache document with a stable schema name and version.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public sealed class CultDocumentAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a document marker for the supplied schema identity.
|
||||
/// </summary>
|
||||
public CultDocumentAttribute(string schemaName, string schemaVersion)
|
||||
{
|
||||
SchemaName = string.IsNullOrWhiteSpace(schemaName)
|
||||
|
|
@ -15,87 +21,172 @@ namespace GameCult.Caching
|
|||
: schemaVersion;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stable schema name used by CultCache and CultNet.
|
||||
/// </summary>
|
||||
public string SchemaName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the semantic schema version string for this document shape.
|
||||
/// </summary>
|
||||
public string SchemaVersion { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks a document type as having one global record in a cache.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public sealed class CultGlobalAttribute : Attribute
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks the member that supplies the human-readable document name.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true)]
|
||||
public sealed class CultNameAttribute : Attribute
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks a member as an indexed lookup value.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
|
||||
public sealed class CultIndexAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an index marker with an optional alias.
|
||||
/// </summary>
|
||||
public CultIndexAttribute(string? alias = null)
|
||||
{
|
||||
Alias = alias;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the external index alias, or null to use the member name.
|
||||
/// </summary>
|
||||
public string? Alias { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks a member as a reference to another CultCache document.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true)]
|
||||
public sealed class CultReferenceAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a document reference marker.
|
||||
/// </summary>
|
||||
public CultReferenceAttribute(Type? targetType = null, bool many = false)
|
||||
{
|
||||
TargetType = targetType;
|
||||
Many = many;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the explicit referenced document type, when one is required.
|
||||
/// </summary>
|
||||
public Type? TargetType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this member stores many references rather than one reference.
|
||||
/// </summary>
|
||||
public bool Many { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies a record inside a CultCache backing store.
|
||||
/// </summary>
|
||||
public readonly struct CultRecordKey : IEquatable<CultRecordKey>
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a record key from its persisted string value.
|
||||
/// </summary>
|
||||
public CultRecordKey(string value)
|
||||
{
|
||||
Value = value ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the persisted key value.
|
||||
/// </summary>
|
||||
public string Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the persisted key value.
|
||||
/// </summary>
|
||||
public override string ToString() => Value;
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether this key and another key contain the same persisted value.
|
||||
/// </summary>
|
||||
public bool Equals(CultRecordKey other) => string.Equals(Value, other.Value, StringComparison.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether this key and another object contain the same persisted value.
|
||||
/// </summary>
|
||||
public override bool Equals(object? obj) => obj is CultRecordKey other && Equals(other);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a hash code for the persisted key value.
|
||||
/// </summary>
|
||||
public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Carries the persisted record key for a typed document.
|
||||
/// </summary>
|
||||
public readonly struct CultRecordHandle<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a typed document handle.
|
||||
/// </summary>
|
||||
public CultRecordHandle(CultRecordKey key)
|
||||
{
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the persisted record key.
|
||||
/// </summary>
|
||||
public CultRecordKey Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the persisted key value.
|
||||
/// </summary>
|
||||
public override string ToString() => Key.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores a typed reference to a CultCache document.
|
||||
/// </summary>
|
||||
public readonly struct CultRecordRef<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a typed document reference.
|
||||
/// </summary>
|
||||
public CultRecordRef(CultRecordKey key)
|
||||
{
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the referenced record key.
|
||||
/// </summary>
|
||||
public CultRecordKey Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the referenced record against the provided cache.
|
||||
/// </summary>
|
||||
public T? Resolve(CultCache cache)
|
||||
{
|
||||
if (cache == null) throw new ArgumentNullException(nameof(cache));
|
||||
return cache.Get(Key) is T typed ? typed : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the referenced key value.
|
||||
/// </summary>
|
||||
public override string ToString() => Key.ToString();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,24 +6,45 @@ using System.Text;
|
|||
|
||||
namespace GameCult.Caching
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers a generated CultCache document metadata provider with an assembly.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
|
||||
public sealed class CultGeneratedDocumentMetadataProviderAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an assembly metadata provider registration.
|
||||
/// </summary>
|
||||
public CultGeneratedDocumentMetadataProviderAttribute(Type providerType)
|
||||
{
|
||||
ProviderType = providerType ?? throw new ArgumentNullException(nameof(providerType));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the provider type that can create generated document definitions.
|
||||
/// </summary>
|
||||
public Type ProviderType { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Supplies generated CultCache document metadata for an assembly.
|
||||
/// </summary>
|
||||
public interface ICultGeneratedDocumentMetadataProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the generated document definitions exposed by the provider.
|
||||
/// </summary>
|
||||
IEnumerable<CultGeneratedDocumentDefinition> GetDocumentDefinitions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes one generated CultCache document contract.
|
||||
/// </summary>
|
||||
public sealed class CultGeneratedDocumentDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a generated document contract definition.
|
||||
/// </summary>
|
||||
public CultGeneratedDocumentDefinition(
|
||||
Type documentType,
|
||||
string schemaName,
|
||||
|
|
@ -52,20 +73,65 @@ namespace GameCult.Caching
|
|||
Members = members ?? Array.Empty<CultGeneratedDocumentMemberDefinition>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the CLR document type.
|
||||
/// </summary>
|
||||
public Type DocumentType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stable CultCache schema name.
|
||||
/// </summary>
|
||||
public string SchemaName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the schema version string.
|
||||
/// </summary>
|
||||
public string SchemaVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this document type stores one global record.
|
||||
/// </summary>
|
||||
public bool IsGlobal { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the member used as the document name, if any.
|
||||
/// </summary>
|
||||
public string? NameMember { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the generated name accessor, if any.
|
||||
/// </summary>
|
||||
public Func<object, string?>? NameAccessor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the generated payload serializer, if one was emitted.
|
||||
/// </summary>
|
||||
public Func<object, byte[]>? SerializePayload { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the generated payload deserializer, if one was emitted.
|
||||
/// </summary>
|
||||
public Func<byte[], object>? DeserializePayload { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the generated index accessors.
|
||||
/// </summary>
|
||||
public IReadOnlyList<CultGeneratedDocumentIndexAccessor> IndexAccessors { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the generated persisted member definitions.
|
||||
/// </summary>
|
||||
public IReadOnlyList<CultGeneratedDocumentMemberDefinition> Members { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a generated accessor for one CultCache index.
|
||||
/// </summary>
|
||||
public sealed class CultGeneratedDocumentIndexAccessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a generated index accessor.
|
||||
/// </summary>
|
||||
public CultGeneratedDocumentIndexAccessor(string alias, Func<object, string> accessor)
|
||||
{
|
||||
Alias = string.IsNullOrWhiteSpace(alias)
|
||||
|
|
@ -74,12 +140,25 @@ namespace GameCult.Caching
|
|||
Accessor = accessor ?? throw new ArgumentNullException(nameof(accessor));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index alias.
|
||||
/// </summary>
|
||||
public string Alias { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the function that extracts the index value.
|
||||
/// </summary>
|
||||
public Func<object, string> Accessor { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes one generated persisted document member.
|
||||
/// </summary>
|
||||
public sealed class CultGeneratedDocumentMemberDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a generated persisted member definition.
|
||||
/// </summary>
|
||||
public CultGeneratedDocumentMemberDefinition(
|
||||
string memberName,
|
||||
int slot,
|
||||
|
|
@ -104,13 +183,44 @@ namespace GameCult.Caching
|
|||
IndexAlias = indexAlias;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the CLR member name.
|
||||
/// </summary>
|
||||
public string MemberName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the persisted slot number.
|
||||
/// </summary>
|
||||
public int Slot { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the persisted type name.
|
||||
/// </summary>
|
||||
public string TypeName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this member stores a document reference.
|
||||
/// </summary>
|
||||
public bool IsReference { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this member stores many references.
|
||||
/// </summary>
|
||||
public bool IsMany { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target schema name for reference members.
|
||||
/// </summary>
|
||||
public string? TargetSchemaName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this member is the document name.
|
||||
/// </summary>
|
||||
public bool IsName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index alias for indexed members.
|
||||
/// </summary>
|
||||
public string? IndexAlias { get; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,15 +8,36 @@ using GameCult.Caching.MessagePack;
|
|||
|
||||
namespace GameCult.Networking
|
||||
{
|
||||
/// <summary>
|
||||
/// Optional metadata applied when creating CultNet document messages.
|
||||
/// </summary>
|
||||
public sealed class CultNetDocumentMessageOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets an explicit storage timestamp.
|
||||
/// </summary>
|
||||
public string? StoredAt { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the runtime that produced the document.
|
||||
/// </summary>
|
||||
public string? SourceRuntimeId { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the agent that produced the document.
|
||||
/// </summary>
|
||||
public string? SourceAgentId { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the role that produced the document.
|
||||
/// </summary>
|
||||
public string? SourceRole { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets optional document tags.
|
||||
/// </summary>
|
||||
public string[]? Tags { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds a CultCache document type to CultNet payload serialization.
|
||||
/// </summary>
|
||||
public sealed class CultNetDocumentBinding
|
||||
{
|
||||
internal CultNetDocumentBinding(
|
||||
|
|
@ -31,11 +52,26 @@ namespace GameCult.Networking
|
|||
PayloadDeserializer = payloadDeserializer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bound document type.
|
||||
/// </summary>
|
||||
public Type DocumentType { get; }
|
||||
/// <summary>
|
||||
/// Gets the bound schema identifier.
|
||||
/// </summary>
|
||||
public string SchemaId { get; }
|
||||
/// <summary>
|
||||
/// Gets the payload serializer.
|
||||
/// </summary>
|
||||
public Func<object, byte[]> PayloadSerializer { get; }
|
||||
/// <summary>
|
||||
/// Gets the payload deserializer.
|
||||
/// </summary>
|
||||
public Func<byte[], object> PayloadDeserializer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a document binding for a typed CultCache document.
|
||||
/// </summary>
|
||||
public static CultNetDocumentBinding ForDocument<T>(
|
||||
CultDocumentRegistry? registry = null,
|
||||
Func<T, byte[]>? payloadSerializer = null,
|
||||
|
|
@ -59,6 +95,9 @@ namespace GameCult.Networking
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and applies CultNet messages that replicate CultCache documents.
|
||||
/// </summary>
|
||||
public sealed class CultNetDocumentRegistry
|
||||
{
|
||||
private readonly CultDocumentRegistry _documents;
|
||||
|
|
@ -66,6 +105,9 @@ namespace GameCult.Networking
|
|||
new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<Type, CultNetDocumentBinding> _bindingsByType = new();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a document registry with optional bindings.
|
||||
/// </summary>
|
||||
public CultNetDocumentRegistry(
|
||||
CultDocumentRegistry? documents = null,
|
||||
IEnumerable<CultNetDocumentBinding>? bindings = null)
|
||||
|
|
@ -82,6 +124,9 @@ namespace GameCult.Networking
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a document binding.
|
||||
/// </summary>
|
||||
public CultNetDocumentRegistry Register(CultNetDocumentBinding binding)
|
||||
{
|
||||
_bindingsBySchemaId[binding.SchemaId] = binding;
|
||||
|
|
@ -89,16 +134,25 @@ namespace GameCult.Networking
|
|||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a binding by schema identifier.
|
||||
/// </summary>
|
||||
public CultNetDocumentBinding? GetBySchemaId(string schemaId)
|
||||
{
|
||||
return _bindingsBySchemaId.TryGetValue(schemaId, out var binding) ? binding : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a binding by CLR document type.
|
||||
/// </summary>
|
||||
public CultNetDocumentBinding? GetByDocumentType(Type documentType)
|
||||
{
|
||||
return _bindingsByType.TryGetValue(documentType, out var binding) ? binding : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a document delete message.
|
||||
/// </summary>
|
||||
public CultNetDocumentDeleteMessage CreateDocumentDeleteMessage(
|
||||
string messageId,
|
||||
string schemaId,
|
||||
|
|
@ -112,6 +166,9 @@ namespace GameCult.Networking
|
|||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a raw document put message for a typed document.
|
||||
/// </summary>
|
||||
public CultNetDocumentPutRawMessage CreateRawDocumentPutMessage<T>(
|
||||
string messageId,
|
||||
CultRecordHandle<T> handle,
|
||||
|
|
@ -141,6 +198,9 @@ namespace GameCult.Networking
|
|||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a snapshot request message.
|
||||
/// </summary>
|
||||
public CultNetSnapshotRequestMessage CreateSnapshotRequest(
|
||||
string messageId,
|
||||
IEnumerable<string>? schemaIds = null,
|
||||
|
|
@ -154,6 +214,9 @@ namespace GameCult.Networking
|
|||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a raw snapshot response from the cache.
|
||||
/// </summary>
|
||||
public CultNetSnapshotResponseRawMessage CreateRawSnapshotResponse(
|
||||
CultCache cache,
|
||||
string messageId,
|
||||
|
|
@ -225,6 +288,9 @@ namespace GameCult.Networking
|
|||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a raw document put message to a cache.
|
||||
/// </summary>
|
||||
public async Task<object> ApplyRawDocumentPutMessageAsync(
|
||||
CultCache cache,
|
||||
CultNetDocumentPutRawMessage message)
|
||||
|
|
@ -253,6 +319,9 @@ namespace GameCult.Networking
|
|||
return document;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a raw document put message and returns the typed document.
|
||||
/// </summary>
|
||||
public async Task<T> ApplyRawDocumentPutMessageAsync<T>(
|
||||
CultCache cache,
|
||||
CultNetDocumentPutRawMessage message)
|
||||
|
|
@ -261,6 +330,9 @@ namespace GameCult.Networking
|
|||
return (T)await ApplyRawDocumentPutMessageAsync(cache, message).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies all documents from a raw snapshot response.
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<object>> ApplyRawSnapshotResponseAsync(
|
||||
CultCache cache,
|
||||
CultNetSnapshotResponseRawMessage response)
|
||||
|
|
@ -280,6 +352,9 @@ namespace GameCult.Networking
|
|||
return applied;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies all typed documents from a raw snapshot response.
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<T>> ApplyRawSnapshotResponseAsync<T>(
|
||||
CultCache cache,
|
||||
CultNetSnapshotResponseRawMessage response)
|
||||
|
|
|
|||
|
|
@ -8,21 +8,33 @@ namespace GameCult.Networking
|
|||
/// </summary>
|
||||
public static class CultNetSchemaMessageSerialization
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the serializer options used for CultNet schema-v0 messages.
|
||||
/// </summary>
|
||||
public static readonly MessagePackSerializerOptions Options =
|
||||
MessagePackSerializerOptions.Standard.WithSecurity(MessagePackSecurity.UntrustedData);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a typed CultNet schema message.
|
||||
/// </summary>
|
||||
public static byte[] Serialize<T>(T message) where T : ICultNetSchemaMessage
|
||||
{
|
||||
if (message == null) throw new ArgumentNullException(nameof(message));
|
||||
return MessagePackSerializer.Serialize(message, Options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a typed CultNet schema message.
|
||||
/// </summary>
|
||||
public static T Deserialize<T>(byte[] payload) where T : ICultNetSchemaMessage
|
||||
{
|
||||
if (payload == null) throw new ArgumentNullException(nameof(payload));
|
||||
return MessagePackSerializer.Deserialize<T>(payload, Options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a CultNet schema message by inspecting its schema version.
|
||||
/// </summary>
|
||||
public static ICultNetSchemaMessage Deserialize(byte[] payload)
|
||||
{
|
||||
if (payload == null) throw new ArgumentNullException(nameof(payload));
|
||||
|
|
@ -54,7 +66,7 @@ namespace GameCult.Networking
|
|||
};
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
[MessagePackObject(AllowPrivate = true)]
|
||||
internal sealed class CultNetSchemaHeader
|
||||
{
|
||||
[Key("schemaVersion")] public string SchemaVersion { get; set; } = string.Empty;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,13 @@ namespace GameCult.Networking
|
|||
/// </summary>
|
||||
public static class CultNetWireContracts
|
||||
{
|
||||
/// <summary>
|
||||
/// schema v0 contract identifier.
|
||||
/// </summary>
|
||||
public const string SchemaV0 = "cultnet.schema.v0";
|
||||
/// <summary>
|
||||
/// game cult networking v0 contract identifier.
|
||||
/// </summary>
|
||||
public const string GameCultNetworkingV0 = "gamecult.networking.v0";
|
||||
}
|
||||
|
||||
|
|
@ -17,19 +23,61 @@ namespace GameCult.Networking
|
|||
/// </summary>
|
||||
public static class CultNetSchemaVersions
|
||||
{
|
||||
/// <summary>
|
||||
/// hello contract identifier.
|
||||
/// </summary>
|
||||
public const string Hello = "cultnet.hello.v0";
|
||||
/// <summary>
|
||||
/// login contract identifier.
|
||||
/// </summary>
|
||||
public const string Login = "cultnet.login.v0";
|
||||
/// <summary>
|
||||
/// register contract identifier.
|
||||
/// </summary>
|
||||
public const string Register = "cultnet.register.v0";
|
||||
/// <summary>
|
||||
/// verify contract identifier.
|
||||
/// </summary>
|
||||
public const string Verify = "cultnet.verify.v0";
|
||||
/// <summary>
|
||||
/// login success contract identifier.
|
||||
/// </summary>
|
||||
public const string LoginSuccess = "cultnet.login_success.v0";
|
||||
/// <summary>
|
||||
/// error contract identifier.
|
||||
/// </summary>
|
||||
public const string Error = "cultnet.error.v0";
|
||||
/// <summary>
|
||||
/// sample change name contract identifier.
|
||||
/// </summary>
|
||||
public const string SampleChangeName = "cultnet.sample.change_name.v0";
|
||||
/// <summary>
|
||||
/// sample chat contract identifier.
|
||||
/// </summary>
|
||||
public const string SampleChat = "cultnet.sample.chat.v0";
|
||||
/// <summary>
|
||||
/// document delete contract identifier.
|
||||
/// </summary>
|
||||
public const string DocumentDelete = "cultnet.document_delete.v0";
|
||||
/// <summary>
|
||||
/// document put raw contract identifier.
|
||||
/// </summary>
|
||||
public const string DocumentPutRaw = "cultnet.document_put_raw.v0";
|
||||
/// <summary>
|
||||
/// snapshot request contract identifier.
|
||||
/// </summary>
|
||||
public const string SnapshotRequest = "cultnet.snapshot_request.v0";
|
||||
/// <summary>
|
||||
/// snapshot response raw contract identifier.
|
||||
/// </summary>
|
||||
public const string SnapshotResponseRaw = "cultnet.snapshot_response_raw.v0";
|
||||
/// <summary>
|
||||
/// schema catalog request contract identifier.
|
||||
/// </summary>
|
||||
public const string SchemaCatalogRequest = "cultnet.schema_catalog_request.v0";
|
||||
/// <summary>
|
||||
/// schema catalog response contract identifier.
|
||||
/// </summary>
|
||||
public const string SchemaCatalogResponse = "cultnet.schema_catalog_response.v0";
|
||||
}
|
||||
|
||||
|
|
@ -38,155 +86,413 @@ namespace GameCult.Networking
|
|||
/// </summary>
|
||||
public interface ICultNetSchemaMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the schema version carried by this message.
|
||||
/// </summary>
|
||||
string SchemaVersion { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CultNet hello message used to announce a runtime and its supported contracts.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class CultNetHelloMessage : ICultNetSchemaMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the schema version.
|
||||
/// </summary>
|
||||
[Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.Hello;
|
||||
/// <summary>
|
||||
/// Gets or sets the runtime id.
|
||||
/// </summary>
|
||||
[Key("runtimeId")] public string RuntimeId { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the runtime kind.
|
||||
/// </summary>
|
||||
[Key("runtimeKind")] public string RuntimeKind { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the agent id.
|
||||
/// </summary>
|
||||
[Key("agentId")] public string? AgentId { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the role.
|
||||
/// </summary>
|
||||
[Key("role")] public string? Role { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the display name.
|
||||
/// </summary>
|
||||
[Key("displayName")] public string? DisplayName { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the supported document types.
|
||||
/// </summary>
|
||||
[Key("supportedDocumentTypes")] public string[]? SupportedDocumentTypes { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the supported message versions.
|
||||
/// </summary>
|
||||
[Key("supportedMessageVersions")] public string[]? SupportedMessageVersions { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the supports schema catalog.
|
||||
/// </summary>
|
||||
[Key("supportsSchemaCatalog")] public bool SupportsSchemaCatalog { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CultNet login request message.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class CultNetLoginMessage : ICultNetSchemaMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the schema version.
|
||||
/// </summary>
|
||||
[Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.Login;
|
||||
/// <summary>
|
||||
/// Gets or sets the nonce.
|
||||
/// </summary>
|
||||
[Key("nonce")] public string Nonce { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the auth.
|
||||
/// </summary>
|
||||
[Key("auth")] public string Auth { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the password.
|
||||
/// </summary>
|
||||
[Key("password")] public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CultNet registration request message.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class CultNetRegisterMessage : ICultNetSchemaMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the schema version.
|
||||
/// </summary>
|
||||
[Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.Register;
|
||||
/// <summary>
|
||||
/// Gets or sets the nonce.
|
||||
/// </summary>
|
||||
[Key("nonce")] public string Nonce { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the email.
|
||||
/// </summary>
|
||||
[Key("email")] public string Email { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the password.
|
||||
/// </summary>
|
||||
[Key("password")] public string Password { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
[Key("name")] public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CultNet session verification request message.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class CultNetVerifyMessage : ICultNetSchemaMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the schema version.
|
||||
/// </summary>
|
||||
[Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.Verify;
|
||||
/// <summary>
|
||||
/// Gets or sets the nonce.
|
||||
/// </summary>
|
||||
[Key("nonce")] public string Nonce { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the session.
|
||||
/// </summary>
|
||||
[Key("session")] public string Session { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CultNet message returned after successful session verification.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class CultNetLoginSuccessMessage : ICultNetSchemaMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the schema version.
|
||||
/// </summary>
|
||||
[Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.LoginSuccess;
|
||||
/// <summary>
|
||||
/// Gets or sets the nonce.
|
||||
/// </summary>
|
||||
[Key("nonce")] public string Nonce { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the session.
|
||||
/// </summary>
|
||||
[Key("session")] public string Session { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CultNet error response message.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class CultNetErrorMessage : ICultNetSchemaMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the schema version.
|
||||
/// </summary>
|
||||
[Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.Error;
|
||||
/// <summary>
|
||||
/// Gets or sets the error.
|
||||
/// </summary>
|
||||
[Key("error")] public string Error { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sample CultNet message used by development clients to change a display name.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class CultNetSampleChangeNameMessage : ICultNetSchemaMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the schema version.
|
||||
/// </summary>
|
||||
[Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.SampleChangeName;
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
[Key("name")] public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sample CultNet chat message used by development clients.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class CultNetSampleChatMessage : ICultNetSchemaMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the schema version.
|
||||
/// </summary>
|
||||
[Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.SampleChat;
|
||||
/// <summary>
|
||||
/// Gets or sets the text.
|
||||
/// </summary>
|
||||
[Key("text")] public string Text { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raw document record carried over CultNet.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class CultNetRawDocumentRecord
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the schema id.
|
||||
/// </summary>
|
||||
[Key("schemaId")] public string SchemaId { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the record key.
|
||||
/// </summary>
|
||||
[Key("recordKey")] public string RecordKey { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the stored at.
|
||||
/// </summary>
|
||||
[Key("storedAt")] public string StoredAt { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the payload encoding.
|
||||
/// </summary>
|
||||
[Key("payloadEncoding")] public string PayloadEncoding { get; set; } = "messagepack";
|
||||
/// <summary>
|
||||
/// Gets or sets the payload.
|
||||
/// </summary>
|
||||
[Key("payload")] public byte[] Payload { get; set; } = Array.Empty<byte>();
|
||||
/// <summary>
|
||||
/// Gets or sets the source runtime id.
|
||||
/// </summary>
|
||||
[Key("sourceRuntimeId")] public string? SourceRuntimeId { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the source agent id.
|
||||
/// </summary>
|
||||
[Key("sourceAgentId")] public string? SourceAgentId { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the source role.
|
||||
/// </summary>
|
||||
[Key("sourceRole")] public string? SourceRole { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the tags.
|
||||
/// </summary>
|
||||
[Key("tags")] public string[]? Tags { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CultNet message requesting deletion of a document record.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class CultNetDocumentDeleteMessage : ICultNetSchemaMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the schema version.
|
||||
/// </summary>
|
||||
[Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.DocumentDelete;
|
||||
/// <summary>
|
||||
/// Gets or sets the message id.
|
||||
/// </summary>
|
||||
[Key("messageId")] public string MessageId { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the schema id.
|
||||
/// </summary>
|
||||
[Key("schemaId")] public string SchemaId { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the record key.
|
||||
/// </summary>
|
||||
[Key("recordKey")] public string RecordKey { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CultNet message carrying one raw document record.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class CultNetDocumentPutRawMessage : ICultNetSchemaMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the schema version.
|
||||
/// </summary>
|
||||
[Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.DocumentPutRaw;
|
||||
/// <summary>
|
||||
/// Gets or sets the message id.
|
||||
/// </summary>
|
||||
[Key("messageId")] public string MessageId { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the document.
|
||||
/// </summary>
|
||||
[Key("document")] public CultNetRawDocumentRecord Document { get; set; } = new CultNetRawDocumentRecord();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CultNet message requesting a document snapshot.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class CultNetSnapshotRequestMessage : ICultNetSchemaMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the schema version.
|
||||
/// </summary>
|
||||
[Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.SnapshotRequest;
|
||||
/// <summary>
|
||||
/// Gets or sets the message id.
|
||||
/// </summary>
|
||||
[Key("messageId")] public string MessageId { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the schema ids.
|
||||
/// </summary>
|
||||
[Key("schemaIds")] public string[]? SchemaIds { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the record keys.
|
||||
/// </summary>
|
||||
[Key("recordKeys")] public string[]? RecordKeys { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CultNet message returning raw document records from a snapshot request.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class CultNetSnapshotResponseRawMessage : ICultNetSchemaMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the schema version.
|
||||
/// </summary>
|
||||
[Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.SnapshotResponseRaw;
|
||||
/// <summary>
|
||||
/// Gets or sets the message id.
|
||||
/// </summary>
|
||||
[Key("messageId")] public string MessageId { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the documents.
|
||||
/// </summary>
|
||||
[Key("documents")] public CultNetRawDocumentRecord[] Documents { get; set; } = Array.Empty<CultNetRawDocumentRecord>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes one schema exposed through the CultNet schema catalog.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class CultNetSchemaDescriptor
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the schema id.
|
||||
/// </summary>
|
||||
[Key("schemaId")] public string SchemaId { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the kind.
|
||||
/// </summary>
|
||||
[Key("kind")] public string Kind { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the schema version.
|
||||
/// </summary>
|
||||
[Key("schemaVersion")] public string? SchemaVersion { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the document type.
|
||||
/// </summary>
|
||||
[Key("documentType")] public string? DocumentType { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the title.
|
||||
/// </summary>
|
||||
[Key("title")] public string? Title { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the wire contracts.
|
||||
/// </summary>
|
||||
[Key("wireContracts")] public string[] WireContracts { get; set; } = Array.Empty<string>();
|
||||
/// <summary>
|
||||
/// Gets or sets the content hash.
|
||||
/// </summary>
|
||||
[Key("contentHash")] public string ContentHash { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the schema json.
|
||||
/// </summary>
|
||||
[Key("schemaJson")] public string? SchemaJson { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CultNet message requesting schema catalog entries.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class CultNetSchemaCatalogRequestMessage : ICultNetSchemaMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the schema version.
|
||||
/// </summary>
|
||||
[Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.SchemaCatalogRequest;
|
||||
/// <summary>
|
||||
/// Gets or sets the message id.
|
||||
/// </summary>
|
||||
[Key("messageId")] public string MessageId { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the include schema json.
|
||||
/// </summary>
|
||||
[Key("includeSchemaJson")] public bool IncludeSchemaJson { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the schema ids.
|
||||
/// </summary>
|
||||
[Key("schemaIds")] public string[]? SchemaIds { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the kinds.
|
||||
/// </summary>
|
||||
[Key("kinds")] public string[]? Kinds { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CultNet message returning schema catalog entries.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class CultNetSchemaCatalogResponseMessage : ICultNetSchemaMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the schema version.
|
||||
/// </summary>
|
||||
[Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.SchemaCatalogResponse;
|
||||
/// <summary>
|
||||
/// Gets or sets the message id.
|
||||
/// </summary>
|
||||
[Key("messageId")] public string MessageId { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the schemas.
|
||||
/// </summary>
|
||||
[Key("schemas")] public CultNetSchemaDescriptor[] Schemas { get; set; } = Array.Empty<CultNetSchemaDescriptor>();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ namespace GameCult.Networking
|
|||
{
|
||||
private readonly Dictionary<string, RegisteredSchema> _entries = new Dictionary<string, RegisteredSchema>(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a schema registry with optional initial entries.
|
||||
/// </summary>
|
||||
public CultNetSchemaRegistry(IEnumerable<CultNetSchemaRegistration>? entries = null)
|
||||
{
|
||||
if (entries == null)
|
||||
|
|
@ -30,6 +33,9 @@ namespace GameCult.Networking
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds or replaces one schema registration.
|
||||
/// </summary>
|
||||
public CultNetSchemaRegistry Register(CultNetSchemaRegistration entry)
|
||||
{
|
||||
if (entry == null) throw new ArgumentNullException(nameof(entry));
|
||||
|
|
@ -47,6 +53,9 @@ namespace GameCult.Networking
|
|||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets one schema descriptor by schema identifier.
|
||||
/// </summary>
|
||||
public CultNetSchemaDescriptor? Get(string schemaId, bool includeSchemaJson = false)
|
||||
{
|
||||
if (schemaId == null) throw new ArgumentNullException(nameof(schemaId));
|
||||
|
|
@ -55,6 +64,9 @@ namespace GameCult.Networking
|
|||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lists schema descriptors, optionally filtered by identifier or kind.
|
||||
/// </summary>
|
||||
public CultNetSchemaDescriptor[] List(
|
||||
bool includeSchemaJson = false,
|
||||
IEnumerable<string>? schemaIds = null,
|
||||
|
|
@ -75,6 +87,9 @@ namespace GameCult.Networking
|
|||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a schema catalog response for a request.
|
||||
/// </summary>
|
||||
public CultNetSchemaCatalogResponseMessage CreateCatalogResponse(
|
||||
CultNetSchemaCatalogRequestMessage request)
|
||||
{
|
||||
|
|
@ -90,6 +105,9 @@ namespace GameCult.Networking
|
|||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a registry populated from embedded CultNet schema resources.
|
||||
/// </summary>
|
||||
public static CultNetSchemaRegistry CreateBuiltIn()
|
||||
{
|
||||
return new CultNetSchemaRegistry(BuiltInSchemaManifest.Select(LoadEmbeddedRegistration));
|
||||
|
|
@ -98,6 +116,9 @@ namespace GameCult.Networking
|
|||
private static readonly Lazy<CultNetSchemaRegistry> BuiltInRegistry =
|
||||
new Lazy<CultNetSchemaRegistry>(CreateBuiltIn);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the lazily initialized built-in schema registry.
|
||||
/// </summary>
|
||||
public static CultNetSchemaRegistry BuiltIn => BuiltInRegistry.Value;
|
||||
|
||||
private static CultNetSchemaRegistration LoadEmbeddedRegistration(SchemaResourceSpec spec)
|
||||
|
|
@ -357,12 +378,33 @@ namespace GameCult.Networking
|
|||
/// </summary>
|
||||
public sealed class CultNetSchemaRegistration
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the schema identifier.
|
||||
/// </summary>
|
||||
public string SchemaId { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the schema kind.
|
||||
/// </summary>
|
||||
public string Kind { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the wire contracts implemented by the schema.
|
||||
/// </summary>
|
||||
public string[] WireContracts { get; set; } = Array.Empty<string>();
|
||||
/// <summary>
|
||||
/// Gets or sets the schema document.
|
||||
/// </summary>
|
||||
public string SchemaJson { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the message or document schema version.
|
||||
/// </summary>
|
||||
public string? SchemaVersion { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the document type associated with the schema.
|
||||
/// </summary>
|
||||
public string? DocumentType { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the display title.
|
||||
/// </summary>
|
||||
public string? Title { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,20 +4,35 @@ using MessagePack;
|
|||
|
||||
namespace GameCult.Networking
|
||||
{
|
||||
/// <summary>
|
||||
/// Basic player account document used by the sample networking stack.
|
||||
/// </summary>
|
||||
[CultDocument("gamecult.player_data", "gamecult.player_data.v1")]
|
||||
public class PlayerData
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique player identifier.
|
||||
/// </summary>
|
||||
[Key(0)]
|
||||
[CultIndex]
|
||||
public Guid PlayerId = Guid.NewGuid();
|
||||
|
||||
/// <summary>
|
||||
/// Player email address.
|
||||
/// </summary>
|
||||
[Key(1)]
|
||||
[CultIndex]
|
||||
public string Email = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Stored password hash.
|
||||
/// </summary>
|
||||
[Key(2)]
|
||||
public string PasswordHash = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Player display name.
|
||||
/// </summary>
|
||||
[Key(3)]
|
||||
[CultName]
|
||||
public string Username = string.Empty;
|
||||
|
|
|
|||
Loading…
Reference in a new issue