Add C# configured publication document handles

This commit is contained in:
Meta Vens 2026-06-27 17:10:52 +01:00
commit bcf877d87d
3 changed files with 161 additions and 11 deletions

View file

@ -161,6 +161,105 @@ namespace GameCult.Mesh
public CultDocumentRegistry? Registry { get; set; }
}
/// <summary>
/// Describes where a typed document publication should be read from.
/// </summary>
public abstract class CultMeshDocumentPublicationSource
{
private CultMeshDocumentPublicationSource()
{
}
/// <summary>Creates a publication source from a CultCache backing store.</summary>
public static CultMeshDocumentPublicationSource Store(CacheBackingStore store)
{
return new StoreSource(store);
}
/// <summary>Creates a publication source from a single-file MessagePack CultCache publication.</summary>
public static CultMeshDocumentPublicationSource SingleFile(string path)
{
return new SingleFileSource(path);
}
/// <summary>Creates a publication source from a remote CultNet snapshot response provider.</summary>
public static CultMeshDocumentPublicationSource PeerSnapshot(Func<CultMeshQueryContext, Task<CultNetSnapshotResponseRawMessage>> snapshot)
{
return new PeerSnapshotSource(snapshot);
}
/// <summary>Creates a publication source from a remote CultNet schema endpoint.</summary>
public static CultMeshDocumentPublicationSource PeerSnapshot(string endpoint)
{
return new PeerSnapshotEndpointSource(endpoint);
}
/// <summary>Creates a publication source from a remote CultNet schema client factory.</summary>
public static CultMeshDocumentPublicationSource PeerSnapshot(Func<ICultNetSchemaClient> 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<CultMeshQueryContext, Task<CultNetSnapshotResponseRawMessage>> snapshot)
{
Snapshot = snapshot ?? throw new ArgumentNullException(nameof(snapshot));
}
public Func<CultMeshQueryContext, Task<CultNetSnapshotResponseRawMessage>> 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<ICultNetSchemaClient> 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<ICultNetSchemaClient> CreateClient { get; }
public string Endpoint { get; }
}
}
/// <summary>
/// A locally hosted CultMesh node over CultCache, CultNet transport, and the mesh database facade.
/// </summary>
@ -1265,6 +1364,53 @@ namespace GameCult.Mesh
return DocumentFromSingleFile<TDocument>(path, key, verse.Context, options);
}
/// <summary>
/// Creates a typed document handle from a configured publication source.
/// </summary>
public static CultMeshDocumentHandle<TDocument> DocumentFromPublication<TDocument>(
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<TDocument>(store.BackingStore, key, context, storeOptions);
case CultMeshDocumentPublicationSource.SingleFileSource file:
return DocumentFromSingleFile<TDocument>(file.Path, key, context, storeOptions);
case CultMeshDocumentPublicationSource.PeerSnapshotSource snapshot:
return DocumentFromPeerSnapshot<TDocument>(snapshot.Snapshot, key.Value, context, peerOptions);
case CultMeshDocumentPublicationSource.PeerSnapshotEndpointSource endpoint:
return DocumentFromPeerSnapshot<TDocument>(endpoint.Endpoint, key.Value, context, peerOptions);
case CultMeshDocumentPublicationSource.PeerSnapshotClientSource client:
return DocumentFromPeerSnapshot<TDocument>(client.CreateClient, client.Endpoint, key.Value, context, peerOptions);
default:
throw new NotSupportedException(
$"Unsupported CultMesh document publication source '{source.GetType().FullName}'.");
}
}
/// <summary>
/// Creates a typed document handle from a configured publication source.
/// </summary>
public static CultMeshDocumentHandle<TDocument> DocumentFromPublication<TDocument>(
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<TDocument>(source, key, verse.Context, storeOptions, peerOptions);
}
/// <summary>
/// Creates a typed document handle directly over one distributed CultNet database record.
/// </summary>

View file

@ -182,6 +182,9 @@ var latestHealth = await health.LatestAsync();
`DocumentFromStore<T>()` accepts any `CacheBackingStore`; both helpers pull,
decode, resolve persisted schema metadata, and poll for updates behind the
`CultMeshDocumentHandle<T>` surface.
Use `DocumentFromPublication<T>()` when the caller should provide a configured
source instead of branching between local files, backing stores, and remote
CultNet snapshots itself.
## Game Session

View file

@ -872,8 +872,8 @@ public sealed class CultMeshStreamingTests
await directReadCache.PullAllBackingStoresAsync();
directReadCache.Get<MeshPublicationNoteDocument>(key)!.Text.Should().Be("published");
var handle = CultMesh.DocumentFromSingleFile<MeshPublicationNoteDocument>(
filePath,
var handle = CultMesh.DocumentFromPublication<MeshPublicationNoteDocument>(
CultMeshDocumentPublicationSource.SingleFile(filePath),
key,
CultMesh.Verse("starbridge", "unity-pilot"),
new CultMeshStoreDocumentOptions
@ -921,16 +921,17 @@ public sealed class CultMeshStreamingTests
.Register(CultNetDocumentBinding.ForDocument<MeshNoteDocument>(CultDocumentRegistry.Shared));
var requests = new List<CultNetSnapshotRequestMessage>();
var handle = CultMesh.DocumentFromPeerSnapshot<MeshNoteDocument>(
() => new MeshSnapshotSchemaClient(request =>
{
requests.Add(request);
return registry.CreateRawSnapshotResponse(cache, request.MessageId, request);
}),
"cultnet://snapshot.test:3075",
key.Value,
var handle = CultMesh.DocumentFromPublication<MeshNoteDocument>(
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"