Add C# publication catalog sync

This commit is contained in:
Meta Vens 2026-06-27 23:53:35 +01:00
commit e94cd0973d
2 changed files with 147 additions and 0 deletions

View file

@ -299,6 +299,14 @@ namespace GameCult.Mesh
CultMeshVerseContext context,
CultMeshStoreDocumentOptions? storeOptions,
CultMeshPeerSnapshotDocumentOptions? peerOptions);
/// <summary>Reads this publication document and hydrates it into a local node.</summary>
Task<ICultMeshDocumentHandle> SyncAsync(
CultMeshNode node,
CultMeshDocumentPublicationSource source,
CultMeshVerseContext context,
CultMeshStoreDocumentOptions? storeOptions,
CultMeshPeerSnapshotDocumentOptions? peerOptions);
}
/// <summary>
@ -345,6 +353,32 @@ namespace GameCult.Mesh
CultMesh.WithPublicationBindingOptions(storeOptions, Key, DocumentId, SourceId),
CultMesh.WithPublicationBindingOptions(peerOptions, Key, DocumentId, SourceId));
}
/// <inheritdoc />
public async Task<ICultMeshDocumentHandle> SyncAsync(
CultMeshNode node,
CultMeshDocumentPublicationSource source,
CultMeshVerseContext context,
CultMeshStoreDocumentOptions? storeOptions,
CultMeshPeerSnapshotDocumentOptions? peerOptions)
{
if (node == null) throw new ArgumentNullException(nameof(node));
if (context == null) throw new ArgumentNullException(nameof(context));
var publication = CultMesh.DocumentFromPublication<TDocument>(
Source ?? source,
Key,
context,
CultMesh.WithPublicationBindingOptions(storeOptions, Key, DocumentId, SourceId),
CultMesh.WithPublicationBindingOptions(peerOptions, Key, DocumentId, SourceId));
var local = CultMesh.Document<TDocument>(
node,
Key,
context,
string.IsNullOrWhiteSpace(DocumentId) ? Key.Value : DocumentId);
await local.ReplaceAsync(await publication.LatestAsync().ConfigureAwait(false)).ConfigureAwait(false);
return local;
}
}
/// <summary>
/// A locally hosted CultMesh node over CultCache, CultNet transport, and the mesh database facade.
@ -1667,6 +1701,57 @@ namespace GameCult.Mesh
return DocumentsFromPublication(source, bindings, verse.Context, storeOptions, peerOptions);
}
/// <summary>
/// Reads configured publication documents, hydrates them into a local node, and returns local handles as a catalog.
/// </summary>
public static async Task<CultMeshDocumentCatalog> SyncDocumentsFromPublicationAsync(
CultMeshNode node,
CultMeshDocumentPublicationSource source,
IEnumerable<ICultMeshPublicationDocumentBinding> bindings,
CultMeshVerseContext context,
CultMeshStoreDocumentOptions? storeOptions = null,
CultMeshPeerSnapshotDocumentOptions? peerOptions = null,
bool flush = false)
{
if (node == null) throw new ArgumentNullException(nameof(node));
if (source == null) throw new ArgumentNullException(nameof(source));
if (bindings == null) throw new ArgumentNullException(nameof(bindings));
if (context == null) throw new ArgumentNullException(nameof(context));
var handles = await Task.WhenAll(bindings.Select(binding =>
{
if (binding == null) throw new ArgumentException("CultMesh publication bindings cannot contain null.", nameof(bindings));
return binding.SyncAsync(node, source, context, storeOptions, peerOptions);
})).ConfigureAwait(false);
if (flush)
await node.FlushAsync().ConfigureAwait(false);
return Documents(handles);
}
/// <summary>
/// Reads configured publication documents, hydrates them into a local node, and returns local handles as a catalog.
/// </summary>
public static Task<CultMeshDocumentCatalog> SyncDocumentsFromPublicationAsync(
CultMeshNode node,
CultMeshDocumentPublicationSource source,
IEnumerable<ICultMeshPublicationDocumentBinding> bindings,
CultMeshVerse verse,
CultMeshStoreDocumentOptions? storeOptions = null,
CultMeshPeerSnapshotDocumentOptions? peerOptions = null,
bool flush = false)
{
if (verse == null) throw new ArgumentNullException(nameof(verse));
return SyncDocumentsFromPublicationAsync(
node,
source,
bindings,
verse.Context,
storeOptions,
peerOptions,
flush);
}
/// <summary>
/// Reads one typed document from a configured publication source and hydrates it into a local node.
/// </summary>
public static async Task<TDocument> SyncDocumentFromPublicationAsync<TDocument>(

View file

@ -2359,6 +2359,68 @@ public sealed class CultMeshStreamingTests
catalog.Document<MeshNoteAliasDocument>().Sources.Should().ContainSingle().Which.SourceId.Should().Be("daemon:second");
}
[Test]
public async Task PublicationHelpers_SyncConfiguredDocumentsIntoLocalCatalog()
{
using var directory = new TemporaryDirectory();
var firstPath = Path.Combine(directory.Path, "sync-first.ccmp");
var secondPath = Path.Combine(directory.Path, "sync-second.ccmp");
var firstKey = new CultRecordKey("mesh-note:publication-sync-catalog-first");
var secondKey = new CultRecordKey("mesh-note:publication-sync-catalog-second");
var firstWriter = new CultCache();
firstWriter.AddBackingStore(new SingleFileMessagePackBackingStore(firstPath));
await firstWriter.UpsertAsync(typeof(MeshPublicationNoteDocument), new MeshPublicationNoteDocument
{
Schema = "tests.mesh_publication_note.v1",
Text = "first synced source",
Revision = 31
}, firstKey);
firstWriter.FlushAllBackingStores();
var secondWriter = new CultCache();
secondWriter.AddBackingStore(new SingleFileMessagePackBackingStore(secondPath));
await secondWriter.UpsertAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "second synced source",
Revision = 32
}, new CultRecordHandle<MeshNoteDocument>(secondKey));
secondWriter.FlushAllBackingStores();
using var node = await CultMesh.CreateNodeAsync(
Path.Combine(directory.Path, "target.ccmp"),
new CultMeshNodeOptions
{
StartServer = false
});
var catalog = await CultMesh.SyncDocumentsFromPublicationAsync(
node,
CultMeshDocumentPublicationSource.SingleFile(firstPath),
new ICultMeshPublicationDocumentBinding[]
{
CultMesh.PublicationDocument<MeshPublicationNoteDocument>(
firstKey,
documentId: "local:first"),
CultMesh.PublicationDocument<MeshNoteAliasDocument>(
secondKey,
documentId: "local:second",
source: CultMeshDocumentPublicationSource.SingleFile(secondPath))
},
CultMesh.Verse("starbridge", "unity-sync-catalog"),
new CultMeshStoreDocumentOptions
{
PollInterval = TimeSpan.FromMilliseconds(10)
});
catalog.Documents.Select(document => document.DocumentId).Should().Equal("local:first", "local:second");
(await catalog.LatestAsync<MeshPublicationNoteDocument>()).Text.Should().Be("first synced source");
(await catalog.LatestAsync<MeshNoteAliasDocument>()).Text.Should().Be("second synced source");
node.Cache.Get<MeshPublicationNoteDocument>(firstKey)!.Revision.Should().Be(31);
(await node.Document<MeshNoteDocument>(secondKey, CultMesh.Verse("starbridge", "unity-sync-catalog"))
.LatestAsync()).Text.Should().Be("second synced source");
(await node.Document<MeshNoteAliasDocument>(secondKey, CultMesh.Verse("starbridge", "unity-sync-catalog"))
.LatestAsync()).Revision.Should().Be(32);
}
[Test]
public async Task PublicationHelpers_SyncConfiguredPeerSourceIntoLocalNodeAlias()
{