Add synced snapshot endpoint handles

This commit is contained in:
Meta Vens 2026-06-27 18:34:45 +01:00
commit c8fd765aef
2 changed files with 83 additions and 2 deletions

View file

@ -99,6 +99,9 @@ namespace GameCult.Mesh
/// <summary>Binds this document request to an endpoint-backed handle.</summary>
ICultMeshDocumentHandle Bind(CultMeshSnapshotEndpoint endpoint);
/// <summary>Binds this document request to an endpoint-backed handle that syncs into a local node before returning values.</summary>
ICultMeshDocumentHandle BindSynced(CultMeshSnapshotEndpoint endpoint, CultMeshNode node, bool flush);
}
/// <summary>
@ -130,6 +133,13 @@ namespace GameCult.Mesh
if (endpoint == null) throw new ArgumentNullException(nameof(endpoint));
return endpoint.Document<TDocument>(RecordKey, DocumentId);
}
/// <inheritdoc />
public ICultMeshDocumentHandle BindSynced(CultMeshSnapshotEndpoint endpoint, CultMeshNode node, bool flush)
{
if (endpoint == null) throw new ArgumentNullException(nameof(endpoint));
return endpoint.SyncedDocument<TDocument>(node, RecordKey, DocumentId, flush);
}
}
/// <summary>
@ -284,6 +294,34 @@ namespace GameCult.Mesh
$"CultNet snapshot endpoint '{Endpoint}' did not return {typeof(TDocument).FullName} record '{recordKey}'.");
}
/// <summary>Creates a typed document handle that fetches one endpoint record and syncs it into a local node before returning values.</summary>
public CultMeshDocumentHandle<TDocument> SyncedDocument<TDocument>(
CultMeshNode node,
string recordKey,
string? documentId = null,
bool flush = false)
where TDocument : class
{
if (node == null) throw new ArgumentNullException(nameof(node));
if (string.IsNullOrWhiteSpace(recordKey)) throw new ArgumentException("Value must be non-empty.", nameof(recordKey));
var descriptor = CultDocumentRegistry.Shared.GetRequired<TDocument>();
var sources = new[]
{
CultMesh.ProjectionSource($"{SourceId}:{recordKey}", descriptor.SchemaId, "synced CultNet snapshot endpoint")
};
var watch = CultMesh.PollingQueryWatcher<CultMeshDocumentQueryParameters, TDocument>(
async (_parameters, _context) => await SyncDocumentAsync<TDocument>(node, recordKey, flush).ConfigureAwait(false),
new CultMeshPollingWatchOptions<TDocument>(PollInterval));
return CultMesh.Document<TDocument>(
string.IsNullOrWhiteSpace(documentId) ? recordKey : documentId!,
Context,
_ => SyncDocumentAsync<TDocument>(node, recordKey, flush),
queryContext => watch(CultMeshDocumentQueryParameters.Empty, queryContext),
sources,
RouteHint);
}
/// <summary>Creates a typed document handle over one remote endpoint record.</summary>
public CultMeshDocumentHandle<TDocument> Document<TDocument>(
string recordKey,
@ -326,6 +364,29 @@ namespace GameCult.Mesh
}));
}
/// <summary>Creates a schema-aware catalog from typed endpoint bindings that sync into a local node before returning values.</summary>
public CultMeshDocumentCatalog SyncedDocuments(
CultMeshNode node,
params ICultMeshSnapshotDocumentBinding[] documents)
{
return SyncedDocuments(node, flush: false, documents);
}
/// <summary>Creates a schema-aware catalog from typed endpoint bindings that sync into a local node before returning values.</summary>
public CultMeshDocumentCatalog SyncedDocuments(
CultMeshNode node,
bool flush = false,
params ICultMeshSnapshotDocumentBinding[] documents)
{
if (node == null) throw new ArgumentNullException(nameof(node));
if (documents == null) throw new ArgumentNullException(nameof(documents));
return CultMesh.Documents(documents.Select(document =>
{
if (document == null) throw new ArgumentNullException(nameof(documents));
return document.BindSynced(this, node, flush);
}));
}
private CultMeshSnapshotRequestOptions CreateRequest(
IReadOnlyList<string>? schemaIds,
IReadOnlyList<string>? recordKeys)

View file

@ -1224,14 +1224,34 @@ public sealed class CultMeshStreamingTests
var catalog = surface.Documents(
CultMesh.SnapshotDocument<MeshNoteDocument>(key.Value, "daemon:mesh-note:snapshot-endpoint"));
var catalogAlias = await catalog.LatestAsync<MeshNoteAliasDocument>();
await sourceCache.UpsertAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "snapshot-endpoint-synced",
Revision = 20
}, new CultRecordHandle<MeshNoteDocument>(key));
var syncedHandle = surface.SyncedDocument<MeshNoteAliasDocument>(node, key.Value);
var syncedLatest = await syncedHandle.LatestAsync();
await sourceCache.UpsertAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "snapshot-endpoint-catalog-synced",
Revision = 21
}, new CultRecordHandle<MeshNoteDocument>(key));
var syncedCatalog = surface.SyncedDocuments(
node,
CultMesh.SnapshotDocument<MeshNoteDocument>(key.Value));
var syncedCatalogAlias = await syncedCatalog.LatestAsync<MeshNoteAliasDocument>();
fetchedAlias.Text.Should().Be("snapshot-endpoint");
syncedAlias.Text.Should().Be("snapshot-endpoint");
node.Cache.Get<MeshNoteDocument>(key)!.Revision.Should().Be(19);
aliasLatest.Revision.Should().Be(19);
catalog.Document<MeshNoteDocument>().DocumentId.Should().Be("daemon:mesh-note:snapshot-endpoint");
catalogAlias.Text.Should().Be("snapshot-endpoint");
requests.Should().HaveCount(4);
syncedLatest.Text.Should().Be("snapshot-endpoint-synced");
node.Cache.Get<MeshNoteDocument>(key)!.Revision.Should().Be(21);
syncedCatalogAlias.Text.Should().Be("snapshot-endpoint-catalog-synced");
requests.Should().HaveCount(6);
requests.Should().OnlyContain(request =>
request.SchemaIds!.SequenceEqual(new[]
{