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