From bcf877d87dadfdcd01a323fac19d4e037fc30b77 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 27 Jun 2026 17:10:52 +0100 Subject: [PATCH] Add C# configured publication document handles --- src/GameCult.Mesh/CultMesh.cs | 146 ++++++++++++++++++ src/GameCult.Mesh/README.md | 3 + .../CultMeshStreamingTests.cs | 23 +-- 3 files changed, 161 insertions(+), 11 deletions(-) diff --git a/src/GameCult.Mesh/CultMesh.cs b/src/GameCult.Mesh/CultMesh.cs index 1f6e665f..e2e4de44 100644 --- a/src/GameCult.Mesh/CultMesh.cs +++ b/src/GameCult.Mesh/CultMesh.cs @@ -161,6 +161,105 @@ namespace GameCult.Mesh public CultDocumentRegistry? Registry { get; set; } } + /// + /// Describes where a typed document publication should be read from. + /// + public abstract class CultMeshDocumentPublicationSource + { + private CultMeshDocumentPublicationSource() + { + } + + /// Creates a publication source from a CultCache backing store. + public static CultMeshDocumentPublicationSource Store(CacheBackingStore store) + { + return new StoreSource(store); + } + + /// Creates a publication source from a single-file MessagePack CultCache publication. + public static CultMeshDocumentPublicationSource SingleFile(string path) + { + return new SingleFileSource(path); + } + + /// Creates a publication source from a remote CultNet snapshot response provider. + public static CultMeshDocumentPublicationSource PeerSnapshot(Func> snapshot) + { + return new PeerSnapshotSource(snapshot); + } + + /// Creates a publication source from a remote CultNet schema endpoint. + public static CultMeshDocumentPublicationSource PeerSnapshot(string endpoint) + { + return new PeerSnapshotEndpointSource(endpoint); + } + + /// Creates a publication source from a remote CultNet schema client factory. + public static CultMeshDocumentPublicationSource PeerSnapshot(Func createClient, string endpoint) + { + return new PeerSnapshotClientSource(createClient, endpoint); + } + + internal sealed class StoreSource : CultMeshDocumentPublicationSource + { + public StoreSource(CacheBackingStore store) + { + BackingStore = store ?? throw new ArgumentNullException(nameof(store)); + } + + public CacheBackingStore BackingStore { get; } + } + + internal sealed class SingleFileSource : CultMeshDocumentPublicationSource + { + public SingleFileSource(string path) + { + Path = string.IsNullOrWhiteSpace(path) + ? throw new ArgumentException("Value must be non-empty.", nameof(path)) + : path; + } + + public string Path { get; } + } + + internal sealed class PeerSnapshotSource : CultMeshDocumentPublicationSource + { + public PeerSnapshotSource(Func> snapshot) + { + Snapshot = snapshot ?? throw new ArgumentNullException(nameof(snapshot)); + } + + public Func> Snapshot { get; } + } + + internal sealed class PeerSnapshotEndpointSource : CultMeshDocumentPublicationSource + { + public PeerSnapshotEndpointSource(string endpoint) + { + Endpoint = string.IsNullOrWhiteSpace(endpoint) + ? throw new ArgumentException("Value must be non-empty.", nameof(endpoint)) + : endpoint; + } + + public string Endpoint { get; } + } + + internal sealed class PeerSnapshotClientSource : CultMeshDocumentPublicationSource + { + public PeerSnapshotClientSource(Func createClient, string endpoint) + { + CreateClient = createClient ?? throw new ArgumentNullException(nameof(createClient)); + Endpoint = string.IsNullOrWhiteSpace(endpoint) + ? throw new ArgumentException("Value must be non-empty.", nameof(endpoint)) + : endpoint; + } + + public Func CreateClient { get; } + + public string Endpoint { get; } + } + } + /// /// A locally hosted CultMesh node over CultCache, CultNet transport, and the mesh database facade. /// @@ -1265,6 +1364,53 @@ namespace GameCult.Mesh return DocumentFromSingleFile(path, key, verse.Context, options); } + /// + /// Creates a typed document handle from a configured publication source. + /// + public static CultMeshDocumentHandle DocumentFromPublication( + CultMeshDocumentPublicationSource source, + CultRecordKey key, + CultMeshVerseContext context, + CultMeshStoreDocumentOptions? storeOptions = null, + CultMeshPeerSnapshotDocumentOptions? peerOptions = null) + where TDocument : class + { + if (source == null) throw new ArgumentNullException(nameof(source)); + if (context == null) throw new ArgumentNullException(nameof(context)); + + switch (source) + { + case CultMeshDocumentPublicationSource.StoreSource store: + return DocumentFromStore(store.BackingStore, key, context, storeOptions); + case CultMeshDocumentPublicationSource.SingleFileSource file: + return DocumentFromSingleFile(file.Path, key, context, storeOptions); + case CultMeshDocumentPublicationSource.PeerSnapshotSource snapshot: + return DocumentFromPeerSnapshot(snapshot.Snapshot, key.Value, context, peerOptions); + case CultMeshDocumentPublicationSource.PeerSnapshotEndpointSource endpoint: + return DocumentFromPeerSnapshot(endpoint.Endpoint, key.Value, context, peerOptions); + case CultMeshDocumentPublicationSource.PeerSnapshotClientSource client: + return DocumentFromPeerSnapshot(client.CreateClient, client.Endpoint, key.Value, context, peerOptions); + default: + throw new NotSupportedException( + $"Unsupported CultMesh document publication source '{source.GetType().FullName}'."); + } + } + + /// + /// Creates a typed document handle from a configured publication source. + /// + public static CultMeshDocumentHandle DocumentFromPublication( + CultMeshDocumentPublicationSource source, + CultRecordKey key, + CultMeshVerse verse, + CultMeshStoreDocumentOptions? storeOptions = null, + CultMeshPeerSnapshotDocumentOptions? peerOptions = null) + where TDocument : class + { + if (verse == null) throw new ArgumentNullException(nameof(verse)); + return DocumentFromPublication(source, key, verse.Context, storeOptions, peerOptions); + } + /// /// Creates a typed document handle directly over one distributed CultNet database record. /// diff --git a/src/GameCult.Mesh/README.md b/src/GameCult.Mesh/README.md index c03e63d7..20ae25c2 100644 --- a/src/GameCult.Mesh/README.md +++ b/src/GameCult.Mesh/README.md @@ -182,6 +182,9 @@ var latestHealth = await health.LatestAsync(); `DocumentFromStore()` accepts any `CacheBackingStore`; both helpers pull, decode, resolve persisted schema metadata, and poll for updates behind the `CultMeshDocumentHandle` surface. +Use `DocumentFromPublication()` when the caller should provide a configured +source instead of branching between local files, backing stores, and remote +CultNet snapshots itself. ## Game Session diff --git a/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs b/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs index b753e86a..84db81eb 100644 --- a/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs +++ b/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs @@ -872,8 +872,8 @@ public sealed class CultMeshStreamingTests await directReadCache.PullAllBackingStoresAsync(); directReadCache.Get(key)!.Text.Should().Be("published"); - var handle = CultMesh.DocumentFromSingleFile( - filePath, + var handle = CultMesh.DocumentFromPublication( + CultMeshDocumentPublicationSource.SingleFile(filePath), key, CultMesh.Verse("starbridge", "unity-pilot"), new CultMeshStoreDocumentOptions @@ -921,16 +921,17 @@ public sealed class CultMeshStreamingTests .Register(CultNetDocumentBinding.ForDocument(CultDocumentRegistry.Shared)); var requests = new List(); - var handle = CultMesh.DocumentFromPeerSnapshot( - () => new MeshSnapshotSchemaClient(request => - { - requests.Add(request); - return registry.CreateRawSnapshotResponse(cache, request.MessageId, request); - }), - "cultnet://snapshot.test:3075", - key.Value, + var handle = CultMesh.DocumentFromPublication( + CultMeshDocumentPublicationSource.PeerSnapshot( + () => new MeshSnapshotSchemaClient(request => + { + requests.Add(request); + return registry.CreateRawSnapshotResponse(cache, request.MessageId, request); + }), + "cultnet://snapshot.test:3075"), + key, CultMesh.Verse("starbridge", "unity-pilot"), - new CultMeshPeerSnapshotDocumentOptions + peerOptions: new CultMeshPeerSnapshotDocumentOptions { PollInterval = TimeSpan.FromMilliseconds(10), MessageIdPrefix = "mesh-test-snapshot"