diff --git a/src/GameCult.Mesh/CultMesh.cs b/src/GameCult.Mesh/CultMesh.cs
index 38fb7954..dab5dd28 100644
--- a/src/GameCult.Mesh/CultMesh.cs
+++ b/src/GameCult.Mesh/CultMesh.cs
@@ -299,6 +299,14 @@ namespace GameCult.Mesh
CultMeshVerseContext context,
CultMeshStoreDocumentOptions? storeOptions,
CultMeshPeerSnapshotDocumentOptions? peerOptions);
+
+ /// Reads this publication document and hydrates it into a local node.
+ Task SyncAsync(
+ CultMeshNode node,
+ CultMeshDocumentPublicationSource source,
+ CultMeshVerseContext context,
+ CultMeshStoreDocumentOptions? storeOptions,
+ CultMeshPeerSnapshotDocumentOptions? peerOptions);
}
///
@@ -345,6 +353,32 @@ namespace GameCult.Mesh
CultMesh.WithPublicationBindingOptions(storeOptions, Key, DocumentId, SourceId),
CultMesh.WithPublicationBindingOptions(peerOptions, Key, DocumentId, SourceId));
}
+
+ ///
+ public async Task 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(
+ Source ?? source,
+ Key,
+ context,
+ CultMesh.WithPublicationBindingOptions(storeOptions, Key, DocumentId, SourceId),
+ CultMesh.WithPublicationBindingOptions(peerOptions, Key, DocumentId, SourceId));
+ var local = CultMesh.Document(
+ node,
+ Key,
+ context,
+ string.IsNullOrWhiteSpace(DocumentId) ? Key.Value : DocumentId);
+ await local.ReplaceAsync(await publication.LatestAsync().ConfigureAwait(false)).ConfigureAwait(false);
+ return local;
+ }
}
///
/// 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);
}
///
+ /// Reads configured publication documents, hydrates them into a local node, and returns local handles as a catalog.
+ ///
+ public static async Task SyncDocumentsFromPublicationAsync(
+ CultMeshNode node,
+ CultMeshDocumentPublicationSource source,
+ IEnumerable 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);
+ }
+
+ ///
+ /// Reads configured publication documents, hydrates them into a local node, and returns local handles as a catalog.
+ ///
+ public static Task SyncDocumentsFromPublicationAsync(
+ CultMeshNode node,
+ CultMeshDocumentPublicationSource source,
+ IEnumerable 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);
+ }
+ ///
/// Reads one typed document from a configured publication source and hydrates it into a local node.
///
public static async Task SyncDocumentFromPublicationAsync(
diff --git a/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs b/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs
index b7db4594..b5f5f308 100644
--- a/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs
+++ b/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs
@@ -2359,6 +2359,68 @@ public sealed class CultMeshStreamingTests
catalog.Document().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(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(
+ firstKey,
+ documentId: "local:first"),
+ CultMesh.PublicationDocument(
+ 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()).Text.Should().Be("first synced source");
+ (await catalog.LatestAsync()).Text.Should().Be("second synced source");
+ node.Cache.Get(firstKey)!.Revision.Should().Be(31);
+ (await node.Document(secondKey, CultMesh.Verse("starbridge", "unity-sync-catalog"))
+ .LatestAsync()).Text.Should().Be("second synced source");
+ (await node.Document(secondKey, CultMesh.Verse("starbridge", "unity-sync-catalog"))
+ .LatestAsync()).Revision.Should().Be(32);
+ }
+
[Test]
public async Task PublicationHelpers_SyncConfiguredPeerSourceIntoLocalNodeAlias()
{