Document CultCache and CultNet public surface

This commit is contained in:
Meta Vens 2026-05-08 13:11:01 +01:00
commit 322cda12a6
11 changed files with 995 additions and 5 deletions

View file

@ -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(

View file

@ -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);

View file

@ -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;

View file

@ -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

View file

@ -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();
}
}

View file

@ -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; }
}

View file

@ -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)

View file

@ -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;

View file

@ -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>();
}
}

View file

@ -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; }
}
}

View file

@ -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;