From 3720a8d39d37e2b8a98d74b38fb01cba9ff2af39 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 27 Jun 2026 18:30:17 +0100 Subject: [PATCH] Add snapshot endpoint document bindings --- src/GameCult.Mesh/CultMeshSnapshots.cs | 71 +++++++++++++++++++ .../CultMeshStreamingTests.cs | 4 +- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/src/GameCult.Mesh/CultMeshSnapshots.cs b/src/GameCult.Mesh/CultMeshSnapshots.cs index eb60a8d6..62978b35 100644 --- a/src/GameCult.Mesh/CultMeshSnapshots.cs +++ b/src/GameCult.Mesh/CultMeshSnapshots.cs @@ -83,6 +83,55 @@ namespace GameCult.Mesh public TimeSpan PollInterval { get; set; } = TimeSpan.FromMilliseconds(250); } + /// + /// Describes one typed document handle to bind from a snapshot endpoint. + /// + public interface ICultMeshSnapshotDocumentBinding + { + /// Gets the CLR document type requested by this binding. + Type DocumentType { get; } + + /// Gets the remote record key to read. + string RecordKey { get; } + + /// Gets the optional semantic document id for the resulting handle. + string? DocumentId { get; } + + /// Binds this document request to an endpoint-backed handle. + ICultMeshDocumentHandle Bind(CultMeshSnapshotEndpoint endpoint); + } + + /// + /// Describes one typed document handle to bind from a snapshot endpoint. + /// + public sealed class CultMeshSnapshotDocumentBinding : ICultMeshSnapshotDocumentBinding + where TDocument : class + { + internal CultMeshSnapshotDocumentBinding(string recordKey, string? documentId) + { + RecordKey = string.IsNullOrWhiteSpace(recordKey) + ? throw new ArgumentException("Value must be non-empty.", nameof(recordKey)) + : recordKey; + DocumentId = documentId; + } + + /// + public Type DocumentType => typeof(TDocument); + + /// + public string RecordKey { get; } + + /// + public string? DocumentId { get; } + + /// + public ICultMeshDocumentHandle Bind(CultMeshSnapshotEndpoint endpoint) + { + if (endpoint == null) throw new ArgumentNullException(nameof(endpoint)); + return endpoint.Document(RecordKey, DocumentId); + } + } + /// /// Result from syncing a remote snapshot into a local CultMesh node. /// @@ -266,6 +315,17 @@ namespace GameCult.Mesh return CultMesh.Documents(documents); } + /// Creates a schema-aware catalog from endpoint-backed typed document bindings. + public CultMeshDocumentCatalog Documents(params ICultMeshSnapshotDocumentBinding[] documents) + { + if (documents == null) throw new ArgumentNullException(nameof(documents)); + return CultMesh.Documents(documents.Select(document => + { + if (document == null) throw new ArgumentNullException(nameof(documents)); + return document.Bind(this); + })); + } + private CultMeshSnapshotRequestOptions CreateRequest( IReadOnlyList? schemaIds, IReadOnlyList? recordKeys) @@ -318,6 +378,17 @@ namespace GameCult.Mesh return new CultMeshSnapshotEndpoint(endpoint, options); } + /// + /// Describes one typed document to bind from a CultMesh snapshot endpoint. + /// + public static CultMeshSnapshotDocumentBinding SnapshotDocument( + string recordKey, + string? documentId = null) + where TDocument : class + { + return new CultMeshSnapshotDocumentBinding(recordKey, documentId); + } + /// /// Fetches one scoped raw CultNet snapshot from an endpoint. /// diff --git a/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs b/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs index 6f8fae18..01443beb 100644 --- a/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs +++ b/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs @@ -1221,13 +1221,15 @@ public sealed class CultMeshStreamingTests var syncedAlias = await surface.SyncDocumentAsync(node, key.Value); var aliasHandle = surface.Document(key.Value); var aliasLatest = await aliasHandle.LatestAsync(); - var catalog = surface.Documents(surface.Document(key.Value)); + var catalog = surface.Documents( + CultMesh.SnapshotDocument(key.Value, "daemon:mesh-note:snapshot-endpoint")); var catalogAlias = await catalog.LatestAsync(); fetchedAlias.Text.Should().Be("snapshot-endpoint"); syncedAlias.Text.Should().Be("snapshot-endpoint"); node.Cache.Get(key)!.Revision.Should().Be(19); aliasLatest.Revision.Should().Be(19); + catalog.Document().DocumentId.Should().Be("daemon:mesh-note:snapshot-endpoint"); catalogAlias.Text.Should().Be("snapshot-endpoint"); requests.Should().HaveCount(4); requests.Should().OnlyContain(request =>