Add CultMesh document registry helpers

This commit is contained in:
Meta Vens 2026-06-27 18:01:12 +01:00
commit c45a11e2c8
3 changed files with 184 additions and 0 deletions

View file

@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.Linq;
using GameCult.Caching;
using GameCult.Networking;
namespace GameCult.Mesh
{
public static partial class CultMesh
{
/// <summary>
/// Creates a CultCache document registry and resolves the supplied document types into it.
/// </summary>
public static CultDocumentRegistry CreateCultCacheDocumentRegistry(params Type[] documentTypes)
{
return CreateCultCacheDocumentRegistry((IEnumerable<Type>)documentTypes);
}
/// <summary>
/// Creates a CultCache document registry and resolves the supplied document types into it.
/// </summary>
public static CultDocumentRegistry CreateCultCacheDocumentRegistry(IEnumerable<Type> documentTypes)
{
if (documentTypes == null)
{
throw new ArgumentNullException(nameof(documentTypes));
}
var registry = new CultDocumentRegistry();
foreach (var documentType in DistinctDocumentTypes(documentTypes))
{
registry.GetRequired(documentType);
}
return registry;
}
/// <summary>
/// Creates a CultNet document registry with default bindings for the supplied document types.
/// </summary>
public static CultNetDocumentRegistry CreateCultNetDocumentRegistry(params Type[] documentTypes)
{
return CreateCultNetDocumentRegistry((IEnumerable<Type>)documentTypes);
}
/// <summary>
/// Creates a CultNet document registry with default bindings for the supplied document types.
/// </summary>
public static CultNetDocumentRegistry CreateCultNetDocumentRegistry(IEnumerable<Type> documentTypes)
{
return CreateCultNetDocumentRegistry(documentTypes, null);
}
/// <summary>
/// Creates a CultNet document registry with default bindings for the supplied document types.
/// </summary>
public static CultNetDocumentRegistry CreateCultNetDocumentRegistry(
IEnumerable<Type> documentTypes,
CultDocumentRegistry? documents)
{
if (documentTypes == null)
{
throw new ArgumentNullException(nameof(documentTypes));
}
documents ??= CreateCultCacheDocumentRegistry(documentTypes);
var registry = new CultNetDocumentRegistry(documents);
var descriptors = DistinctDocumentTypes(documentTypes)
.Select(documentType => documents.GetRequired(documentType))
.ToArray();
foreach (var descriptorGroup in descriptors.GroupBy(descriptor => descriptor.SchemaId, StringComparer.Ordinal))
{
foreach (var descriptor in descriptorGroup.Reverse())
{
registry.Register(CultNetDocumentBinding.ForDocument(descriptor.DocumentType, documents));
}
}
return registry;
}
/// <summary>
/// Creates a CultNet document registry with default bindings for the supplied document types.
/// </summary>
public static CultNetDocumentRegistry CreateCultNetDocumentRegistry(
CultDocumentRegistry documents,
params Type[] documentTypes)
{
return CreateCultNetDocumentRegistry(documentTypes, documents);
}
/// <summary>
/// Creates a CultNet document registry with default bindings for the supplied document types.
/// </summary>
public static CultNetDocumentRegistry CreateCultNetDocumentRegistry(
CultDocumentRegistry documents,
IEnumerable<Type> documentTypes)
{
return CreateCultNetDocumentRegistry(documentTypes, documents);
}
private static IEnumerable<Type> DistinctDocumentTypes(IEnumerable<Type> documentTypes)
{
return documentTypes
.Select(documentType => documentType ?? throw new ArgumentException(
"Document type collections cannot contain null entries.",
nameof(documentTypes)))
.Distinct();
}
}
}

View file

@ -94,6 +94,42 @@ namespace GameCult.Networking
? payloadDeserializer(payload)
: CultDocumentMessagePackSerialization.Deserialize<T>(payload));
}
/// <summary>
/// Creates a document binding for a CultCache document type discovered at runtime.
/// </summary>
public static CultNetDocumentBinding ForDocument(
Type documentType,
CultDocumentRegistry? registry = null,
string? schemaId = null,
Func<object, byte[]>? payloadSerializer = null,
Func<byte[], object>? payloadDeserializer = null)
{
if (documentType == null)
{
throw new ArgumentNullException(nameof(documentType));
}
var descriptor = (registry ?? CultDocumentRegistry.Shared).GetRequired(documentType);
return new CultNetDocumentBinding(
documentType,
string.IsNullOrWhiteSpace(schemaId) ? descriptor.SchemaId : schemaId!,
document =>
{
if (!documentType.IsInstanceOfType(document))
{
throw new InvalidOperationException(
$"Document payload type {document.GetType().FullName} is not assignable to {documentType.FullName}.");
}
return payloadSerializer != null
? payloadSerializer(document)
: CultDocumentMessagePackSerialization.SerializeUntyped(document, documentType);
},
payload => payloadDeserializer != null
? payloadDeserializer(payload)
: CultDocumentMessagePackSerialization.DeserializeUntyped(documentType, payload));
}
}
/// <summary>

View file

@ -1101,6 +1101,43 @@ public sealed class CultMeshStreamingTests
request.MessageId.StartsWith("mesh-test-scoped-snapshot:", StringComparison.Ordinal));
}
[Test]
public async Task DocumentRegistryHelpers_CreateTypedNetworkRegistriesForAliases()
{
var sourceCache = new CultCache();
var cacheRegistry = CultMesh.CreateCultCacheDocumentRegistry(
typeof(MeshNoteDocument),
typeof(MeshNoteAliasDocument));
var networkRegistry = CultMesh.CreateCultNetDocumentRegistry(
new[] { typeof(MeshNoteDocument), typeof(MeshNoteAliasDocument) },
cacheRegistry);
var key = new CultRecordKey("mesh-note:registry-helper");
await sourceCache.UpsertAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "registry-helper",
Revision = 23
}, new CultRecordHandle<MeshNoteDocument>(key));
var endpoint = "cultnet://registry-helper.test:3077";
var documents = await CultMesh.FetchSnapshotDocumentsAsync<MeshNoteAliasDocument>(
endpoint,
new CultMeshSnapshotRequestOptions
{
RecordKeys = new[] { key.Value },
CreateClient = () => new MeshSnapshotSchemaClient(request =>
networkRegistry.CreateRawSnapshotResponse(sourceCache, request.MessageId, request))
},
networkRegistry);
var schemaId = cacheRegistry.GetRequired<MeshNoteDocument>().SchemaId;
cacheRegistry.GetRequired<MeshNoteAliasDocument>().SchemaId.Should().Be(schemaId);
networkRegistry.GetByDocumentType(typeof(MeshNoteDocument)).Should().NotBeNull();
networkRegistry.GetByDocumentType(typeof(MeshNoteAliasDocument)).Should().NotBeNull();
networkRegistry.GetBySchemaId(schemaId)!.DocumentType.Should().Be(typeof(MeshNoteDocument));
documents.Should().ContainSingle().Which.Text.Should().Be("registry-helper");
}
[Test]
public async Task SnapshotEndpoint_ProvidesTypedHandlesAndSchemaAliases()
{