diff --git a/src/GameCult.Mesh/CultMesh.cs b/src/GameCult.Mesh/CultMesh.cs
index 1f6e665f..e2e4de44 100644
--- a/src/GameCult.Mesh/CultMesh.cs
+++ b/src/GameCult.Mesh/CultMesh.cs
@@ -161,6 +161,105 @@ namespace GameCult.Mesh
public CultDocumentRegistry? Registry { get; set; }
}
+ ///
+ /// Describes where a typed document publication should be read from.
+ ///
+ public abstract class CultMeshDocumentPublicationSource
+ {
+ private CultMeshDocumentPublicationSource()
+ {
+ }
+
+ /// Creates a publication source from a CultCache backing store.
+ public static CultMeshDocumentPublicationSource Store(CacheBackingStore store)
+ {
+ return new StoreSource(store);
+ }
+
+ /// Creates a publication source from a single-file MessagePack CultCache publication.
+ public static CultMeshDocumentPublicationSource SingleFile(string path)
+ {
+ return new SingleFileSource(path);
+ }
+
+ /// Creates a publication source from a remote CultNet snapshot response provider.
+ public static CultMeshDocumentPublicationSource PeerSnapshot(Func> snapshot)
+ {
+ return new PeerSnapshotSource(snapshot);
+ }
+
+ /// Creates a publication source from a remote CultNet schema endpoint.
+ public static CultMeshDocumentPublicationSource PeerSnapshot(string endpoint)
+ {
+ return new PeerSnapshotEndpointSource(endpoint);
+ }
+
+ /// Creates a publication source from a remote CultNet schema client factory.
+ public static CultMeshDocumentPublicationSource PeerSnapshot(Func createClient, string endpoint)
+ {
+ return new PeerSnapshotClientSource(createClient, endpoint);
+ }
+
+ internal sealed class StoreSource : CultMeshDocumentPublicationSource
+ {
+ public StoreSource(CacheBackingStore store)
+ {
+ BackingStore = store ?? throw new ArgumentNullException(nameof(store));
+ }
+
+ public CacheBackingStore BackingStore { get; }
+ }
+
+ internal sealed class SingleFileSource : CultMeshDocumentPublicationSource
+ {
+ public SingleFileSource(string path)
+ {
+ Path = string.IsNullOrWhiteSpace(path)
+ ? throw new ArgumentException("Value must be non-empty.", nameof(path))
+ : path;
+ }
+
+ public string Path { get; }
+ }
+
+ internal sealed class PeerSnapshotSource : CultMeshDocumentPublicationSource
+ {
+ public PeerSnapshotSource(Func> snapshot)
+ {
+ Snapshot = snapshot ?? throw new ArgumentNullException(nameof(snapshot));
+ }
+
+ public Func> Snapshot { get; }
+ }
+
+ internal sealed class PeerSnapshotEndpointSource : CultMeshDocumentPublicationSource
+ {
+ public PeerSnapshotEndpointSource(string endpoint)
+ {
+ Endpoint = string.IsNullOrWhiteSpace(endpoint)
+ ? throw new ArgumentException("Value must be non-empty.", nameof(endpoint))
+ : endpoint;
+ }
+
+ public string Endpoint { get; }
+ }
+
+ internal sealed class PeerSnapshotClientSource : CultMeshDocumentPublicationSource
+ {
+ public PeerSnapshotClientSource(Func createClient, string endpoint)
+ {
+ CreateClient = createClient ?? throw new ArgumentNullException(nameof(createClient));
+ Endpoint = string.IsNullOrWhiteSpace(endpoint)
+ ? throw new ArgumentException("Value must be non-empty.", nameof(endpoint))
+ : endpoint;
+ }
+
+ public Func CreateClient { get; }
+
+ public string Endpoint { get; }
+ }
+ }
+
///
/// A locally hosted CultMesh node over CultCache, CultNet transport, and the mesh database facade.
///
@@ -1265,6 +1364,53 @@ namespace GameCult.Mesh
return DocumentFromSingleFile(path, key, verse.Context, options);
}
+ ///
+ /// Creates a typed document handle from a configured publication source.
+ ///
+ public static CultMeshDocumentHandle DocumentFromPublication(
+ CultMeshDocumentPublicationSource source,
+ CultRecordKey key,
+ CultMeshVerseContext context,
+ CultMeshStoreDocumentOptions? storeOptions = null,
+ CultMeshPeerSnapshotDocumentOptions? peerOptions = null)
+ where TDocument : class
+ {
+ if (source == null) throw new ArgumentNullException(nameof(source));
+ if (context == null) throw new ArgumentNullException(nameof(context));
+
+ switch (source)
+ {
+ case CultMeshDocumentPublicationSource.StoreSource store:
+ return DocumentFromStore(store.BackingStore, key, context, storeOptions);
+ case CultMeshDocumentPublicationSource.SingleFileSource file:
+ return DocumentFromSingleFile(file.Path, key, context, storeOptions);
+ case CultMeshDocumentPublicationSource.PeerSnapshotSource snapshot:
+ return DocumentFromPeerSnapshot(snapshot.Snapshot, key.Value, context, peerOptions);
+ case CultMeshDocumentPublicationSource.PeerSnapshotEndpointSource endpoint:
+ return DocumentFromPeerSnapshot(endpoint.Endpoint, key.Value, context, peerOptions);
+ case CultMeshDocumentPublicationSource.PeerSnapshotClientSource client:
+ return DocumentFromPeerSnapshot(client.CreateClient, client.Endpoint, key.Value, context, peerOptions);
+ default:
+ throw new NotSupportedException(
+ $"Unsupported CultMesh document publication source '{source.GetType().FullName}'.");
+ }
+ }
+
+ ///
+ /// Creates a typed document handle from a configured publication source.
+ ///
+ public static CultMeshDocumentHandle DocumentFromPublication(
+ CultMeshDocumentPublicationSource source,
+ CultRecordKey key,
+ CultMeshVerse verse,
+ CultMeshStoreDocumentOptions? storeOptions = null,
+ CultMeshPeerSnapshotDocumentOptions? peerOptions = null)
+ where TDocument : class
+ {
+ if (verse == null) throw new ArgumentNullException(nameof(verse));
+ return DocumentFromPublication(source, key, verse.Context, storeOptions, peerOptions);
+ }
+
///
/// Creates a typed document handle directly over one distributed CultNet database record.
///
diff --git a/src/GameCult.Mesh/README.md b/src/GameCult.Mesh/README.md
index c03e63d7..20ae25c2 100644
--- a/src/GameCult.Mesh/README.md
+++ b/src/GameCult.Mesh/README.md
@@ -182,6 +182,9 @@ var latestHealth = await health.LatestAsync();
`DocumentFromStore()` accepts any `CacheBackingStore`; both helpers pull,
decode, resolve persisted schema metadata, and poll for updates behind the
`CultMeshDocumentHandle` surface.
+Use `DocumentFromPublication()` when the caller should provide a configured
+source instead of branching between local files, backing stores, and remote
+CultNet snapshots itself.
## Game Session
diff --git a/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs b/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs
index b753e86a..84db81eb 100644
--- a/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs
+++ b/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs
@@ -872,8 +872,8 @@ public sealed class CultMeshStreamingTests
await directReadCache.PullAllBackingStoresAsync();
directReadCache.Get(key)!.Text.Should().Be("published");
- var handle = CultMesh.DocumentFromSingleFile(
- filePath,
+ var handle = CultMesh.DocumentFromPublication(
+ CultMeshDocumentPublicationSource.SingleFile(filePath),
key,
CultMesh.Verse("starbridge", "unity-pilot"),
new CultMeshStoreDocumentOptions
@@ -921,16 +921,17 @@ public sealed class CultMeshStreamingTests
.Register(CultNetDocumentBinding.ForDocument(CultDocumentRegistry.Shared));
var requests = new List();
- var handle = CultMesh.DocumentFromPeerSnapshot(
- () => new MeshSnapshotSchemaClient(request =>
- {
- requests.Add(request);
- return registry.CreateRawSnapshotResponse(cache, request.MessageId, request);
- }),
- "cultnet://snapshot.test:3075",
- key.Value,
+ var handle = CultMesh.DocumentFromPublication(
+ CultMeshDocumentPublicationSource.PeerSnapshot(
+ () => new MeshSnapshotSchemaClient(request =>
+ {
+ requests.Add(request);
+ return registry.CreateRawSnapshotResponse(cache, request.MessageId, request);
+ }),
+ "cultnet://snapshot.test:3075"),
+ key,
CultMesh.Verse("starbridge", "unity-pilot"),
- new CultMeshPeerSnapshotDocumentOptions
+ peerOptions: new CultMeshPeerSnapshotDocumentOptions
{
PollInterval = TimeSpan.FromMilliseconds(10),
MessageIdPrefix = "mesh-test-snapshot"