Add CultMesh collection handles

This commit is contained in:
Meta Vens 2026-06-27 15:09:36 +01:00
commit eb52b56088
5 changed files with 593 additions and 0 deletions

View file

@ -1136,6 +1136,215 @@ namespace GameCult.Mesh
return new CultMeshDocumentCatalog(documents);
}
/// <summary>
/// Creates a typed collection handle over all local CultCache records assignable to the document type.
/// </summary>
public static CultMeshCollectionHandle<TDocument> Collection<TDocument>(
CultCache cache,
string? collectionId = null,
IEnumerable<CultMeshProjectionSource>? sources = null,
CultMeshRouteHint? routeHint = null)
where TDocument : class
{
if (cache == null) throw new ArgumentNullException(nameof(cache));
var descriptor = cache.Registry.GetRequired<TDocument>();
var sourceList = sources?.ToArray()
?? new[] { ProjectionSource(CollectionId(collectionId, descriptor), descriptor.SchemaId, "CultCache collection") };
var route = routeHint ?? new CultMeshRouteHint(CultMeshLocalityKind.InProcess, "CultCache collection");
return new CultMeshCollectionHandle<TDocument>(
CollectionId(collectionId, descriptor),
() => Task.FromResult<IReadOnlyList<TDocument>>(cache.GetAll<TDocument>().ToArray()),
() => cache.Watch<TDocument>().Select(change => ToCollectionChange(change, descriptor.SchemaId)),
sourceList,
route);
}
/// <summary>
/// Creates a typed collection handle over the local CultCache record with the supplied CultName.
/// </summary>
public static CultMeshCollectionHandle<TDocument> CollectionByName<TDocument>(
CultCache cache,
string name,
string? collectionId = null,
IEnumerable<CultMeshProjectionSource>? sources = null,
CultMeshRouteHint? routeHint = null)
where TDocument : class
{
if (cache == null) throw new ArgumentNullException(nameof(cache));
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value must be non-empty.", nameof(name));
var descriptor = cache.Registry.GetRequired<TDocument>();
var resolvedId = collectionId ?? $"{descriptor.SchemaId}:name:{name}";
var sourceList = sources?.ToArray()
?? new[] { ProjectionSource(resolvedId, descriptor.SchemaId, "CultCache named collection") };
var route = routeHint ?? new CultMeshRouteHint(CultMeshLocalityKind.InProcess, "CultCache named collection");
return new CultMeshCollectionHandle<TDocument>(
resolvedId,
() => Task.FromResult<IReadOnlyList<TDocument>>(Optional(cache.GetByName<TDocument>(name))),
() => cache.Watch<TDocument>()
.Where(change => MatchesName(cache, name, change))
.Select(change => ToCollectionChange(change, descriptor.SchemaId)),
sourceList,
route);
}
/// <summary>
/// Creates a typed collection handle over local CultCache records matched by an indexed value.
/// </summary>
public static CultMeshCollectionHandle<TDocument> CollectionByIndex<TDocument>(
CultCache cache,
string alias,
string value,
string? collectionId = null,
IEnumerable<CultMeshProjectionSource>? sources = null,
CultMeshRouteHint? routeHint = null)
where TDocument : class
{
if (cache == null) throw new ArgumentNullException(nameof(cache));
if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentException("Value must be non-empty.", nameof(alias));
if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("Value must be non-empty.", nameof(value));
var descriptor = cache.Registry.GetRequired<TDocument>();
var resolvedId = collectionId ?? $"{descriptor.SchemaId}:index:{alias}:{value}";
var sourceList = sources?.ToArray()
?? new[] { ProjectionSource(resolvedId, descriptor.SchemaId, "CultCache indexed collection") };
var route = routeHint ?? new CultMeshRouteHint(CultMeshLocalityKind.InProcess, "CultCache indexed collection");
return new CultMeshCollectionHandle<TDocument>(
resolvedId,
() => Task.FromResult<IReadOnlyList<TDocument>>(Optional(cache.GetByIndex<TDocument>(alias, value))),
() => cache.Watch<TDocument>()
.Where(change => MatchesIndex(cache, alias, value, change))
.Select(change => ToCollectionChange(change, descriptor.SchemaId)),
sourceList,
route);
}
/// <summary>
/// Creates a typed collection handle over all distributed CultNet database records assignable to the document type.
/// </summary>
public static CultMeshCollectionHandle<TDocument> Collection<TDocument>(
CultNetDatabase database,
string? collectionId = null,
IEnumerable<CultMeshProjectionSource>? sources = null,
CultMeshRouteHint? routeHint = null)
where TDocument : class
{
if (database == null) throw new ArgumentNullException(nameof(database));
var descriptor = CultDocumentRegistry.Shared.GetRequired<TDocument>();
var sourceList = sources?.ToArray()
?? new[] { ProjectionSource(CollectionId(collectionId, descriptor), descriptor.SchemaId, "CultNet database collection") };
var route = routeHint ?? new CultMeshRouteHint(CultMeshLocalityKind.Automatic, "CultNet database collection");
return new CultMeshCollectionHandle<TDocument>(
CollectionId(collectionId, descriptor),
() => Task.FromResult<IReadOnlyList<TDocument>>(database.GetAll<TDocument>().ToArray()),
() => database.Watch<TDocument>().Select(ToCollectionChange),
sourceList,
route);
}
/// <summary>
/// Creates a typed collection handle over the distributed CultNet database record with the supplied CultName.
/// </summary>
public static CultMeshCollectionHandle<TDocument> CollectionByName<TDocument>(
CultNetDatabase database,
string name,
string? collectionId = null,
IEnumerable<CultMeshProjectionSource>? sources = null,
CultMeshRouteHint? routeHint = null)
where TDocument : class
{
if (database == null) throw new ArgumentNullException(nameof(database));
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value must be non-empty.", nameof(name));
var descriptor = CultDocumentRegistry.Shared.GetRequired<TDocument>();
var resolvedId = collectionId ?? $"{descriptor.SchemaId}:name:{name}";
var sourceList = sources?.ToArray()
?? new[] { ProjectionSource(resolvedId, descriptor.SchemaId, "CultNet database named collection") };
var route = routeHint ?? new CultMeshRouteHint(CultMeshLocalityKind.Automatic, "CultNet database named collection");
return new CultMeshCollectionHandle<TDocument>(
resolvedId,
() => Task.FromResult<IReadOnlyList<TDocument>>(Optional(database.GetByName<TDocument>(name))),
() => database.WatchByName<TDocument>(name).Select(ToCollectionChange),
sourceList,
route);
}
/// <summary>
/// Creates a typed collection handle over distributed CultNet database records matched by an indexed value.
/// </summary>
public static CultMeshCollectionHandle<TDocument> CollectionByIndex<TDocument>(
CultNetDatabase database,
string alias,
string value,
string? collectionId = null,
IEnumerable<CultMeshProjectionSource>? sources = null,
CultMeshRouteHint? routeHint = null)
where TDocument : class
{
if (database == null) throw new ArgumentNullException(nameof(database));
if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentException("Value must be non-empty.", nameof(alias));
if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("Value must be non-empty.", nameof(value));
var descriptor = CultDocumentRegistry.Shared.GetRequired<TDocument>();
var resolvedId = collectionId ?? $"{descriptor.SchemaId}:index:{alias}:{value}";
var sourceList = sources?.ToArray()
?? new[] { ProjectionSource(resolvedId, descriptor.SchemaId, "CultNet database indexed collection") };
var route = routeHint ?? new CultMeshRouteHint(CultMeshLocalityKind.Automatic, "CultNet database indexed collection");
return new CultMeshCollectionHandle<TDocument>(
resolvedId,
() => Task.FromResult<IReadOnlyList<TDocument>>(Optional(database.GetByIndex<TDocument>(alias, value))),
() => database.WatchByIndex<TDocument>(alias, value).Select(ToCollectionChange),
sourceList,
route);
}
/// <summary>
/// Creates a typed collection handle over all CultMesh node database records assignable to the document type.
/// </summary>
public static CultMeshCollectionHandle<TDocument> Collection<TDocument>(
CultMeshNode node,
string? collectionId = null,
IEnumerable<CultMeshProjectionSource>? sources = null,
CultMeshRouteHint? routeHint = null)
where TDocument : class
{
if (node == null) throw new ArgumentNullException(nameof(node));
return Collection<TDocument>(node.Database, collectionId, sources, routeHint);
}
/// <summary>
/// Creates a typed collection handle over the CultMesh node database record with the supplied CultName.
/// </summary>
public static CultMeshCollectionHandle<TDocument> CollectionByName<TDocument>(
CultMeshNode node,
string name,
string? collectionId = null,
IEnumerable<CultMeshProjectionSource>? sources = null,
CultMeshRouteHint? routeHint = null)
where TDocument : class
{
if (node == null) throw new ArgumentNullException(nameof(node));
return CollectionByName<TDocument>(node.Database, name, collectionId, sources, routeHint);
}
/// <summary>
/// Creates a typed collection handle over CultMesh node database records matched by an indexed value.
/// </summary>
public static CultMeshCollectionHandle<TDocument> CollectionByIndex<TDocument>(
CultMeshNode node,
string alias,
string value,
string? collectionId = null,
IEnumerable<CultMeshProjectionSource>? sources = null,
CultMeshRouteHint? routeHint = null)
where TDocument : class
{
if (node == null) throw new ArgumentNullException(nameof(node));
return CollectionByIndex<TDocument>(node.Database, alias, value, collectionId, sources, routeHint);
}
/// <summary>
/// Describes a typed live feed surface for tooling, UI, and diagnostics.
/// </summary>
@ -1639,6 +1848,86 @@ namespace GameCult.Mesh
: documentId!;
}
private static string CollectionId(string? collectionId, CultDocumentDescriptor descriptor)
{
return string.IsNullOrWhiteSpace(collectionId)
? descriptor.SchemaId
: collectionId!;
}
private static IReadOnlyList<TDocument> Optional<TDocument>(TDocument? document)
where TDocument : class
{
return document == null
? Array.Empty<TDocument>()
: new[] { document };
}
private static bool MatchesName<TDocument>(
CultCache cache,
string name,
CultCacheDocumentChange<TDocument> change)
where TDocument : class
{
var current = cache.GetByName<TDocument>(name);
return (change.Document != null && ReferenceEquals(current, change.Document)) ||
(change.PreviousDocument != null && ReferenceEquals(current, change.PreviousDocument));
}
private static bool MatchesIndex<TDocument>(
CultCache cache,
string alias,
string value,
CultCacheDocumentChange<TDocument> change)
where TDocument : class
{
var current = cache.GetByIndex<TDocument>(alias, value);
return (change.Document != null && ReferenceEquals(current, change.Document)) ||
(change.PreviousDocument != null && ReferenceEquals(current, change.PreviousDocument));
}
private static CultMeshCollectionChange<TDocument> ToCollectionChange<TDocument>(
CultCacheDocumentChange<TDocument> change,
string schemaId)
where TDocument : class
{
return new CultMeshCollectionChange<TDocument>(
change.Kind switch
{
CultCacheDocumentChangeKind.Added => CultMeshCollectionChangeKind.Added,
CultCacheDocumentChangeKind.Updated => CultMeshCollectionChangeKind.Updated,
CultCacheDocumentChangeKind.Removed => CultMeshCollectionChangeKind.Removed,
_ => CultMeshCollectionChangeKind.Updated
},
change.Key,
schemaId,
change.Document,
change.PreviousDocument);
}
private static CultMeshCollectionChange<TDocument> ToCollectionChange<TDocument>(
CultNetDatabaseChange<TDocument> change)
where TDocument : class
{
return new CultMeshCollectionChange<TDocument>(
change.Kind switch
{
CultNetDatabaseChangeKind.Added => CultMeshCollectionChangeKind.Added,
CultNetDatabaseChangeKind.Updated => CultMeshCollectionChangeKind.Updated,
CultNetDatabaseChangeKind.Removed => CultMeshCollectionChangeKind.Removed,
CultNetDatabaseChangeKind.Predicted => CultMeshCollectionChangeKind.Predicted,
CultNetDatabaseChangeKind.Reconciled => CultMeshCollectionChangeKind.Reconciled,
CultNetDatabaseChangeKind.SchemaMigrated => CultMeshCollectionChangeKind.SchemaMigrated,
CultNetDatabaseChangeKind.Rejected => CultMeshCollectionChangeKind.Rejected,
_ => CultMeshCollectionChangeKind.Updated
},
change.Key,
change.SchemaId,
change.Document,
change.PreviousDocument,
change.Message);
}
private static TDocument ReadRequired<TDocument>(CultCache cache, CultRecordKey key)
where TDocument : class
{

View file

@ -1155,6 +1155,182 @@ namespace GameCult.Mesh
}
}
/// <summary>
/// Describes how a CultMesh collection document changed.
/// </summary>
public enum CultMeshCollectionChangeKind
{
/// <summary>A document was added.</summary>
Added,
/// <summary>A document was updated.</summary>
Updated,
/// <summary>A document was removed.</summary>
Removed,
/// <summary>A local prediction was published before authority accepted it.</summary>
Predicted,
/// <summary>A predicted document was reconciled with authority.</summary>
Reconciled,
/// <summary>A document was accepted through schema migration.</summary>
SchemaMigrated,
/// <summary>A document change was rejected.</summary>
Rejected
}
/// <summary>
/// One typed document change observed through a CultMesh collection handle.
/// </summary>
public sealed class CultMeshCollectionChange<TDocument>
where TDocument : class
{
/// <summary>Creates a collection change.</summary>
public CultMeshCollectionChange(
CultMeshCollectionChangeKind kind,
CultRecordKey key,
string schemaId,
TDocument? document,
TDocument? previousDocument,
string? message = null)
{
Kind = kind;
Key = key;
SchemaId = schemaId ?? "";
Document = document;
PreviousDocument = previousDocument;
Message = message;
}
/// <summary>Gets the change kind.</summary>
public CultMeshCollectionChangeKind Kind { get; }
/// <summary>Gets the changed record key.</summary>
public CultRecordKey Key { get; }
/// <summary>Gets the schema id of the changed document.</summary>
public string SchemaId { get; }
/// <summary>Gets the current document, when present.</summary>
public TDocument? Document { get; }
/// <summary>Gets the previous document, when present.</summary>
public TDocument? PreviousDocument { get; }
/// <summary>Gets an optional rejection or diagnostic message.</summary>
public string? Message { get; }
}
/// <summary>
/// Typed reactive collection handle with schema-aware alias conversion.
/// </summary>
public sealed class CultMeshCollectionHandle<TDocument>
where TDocument : class
{
private static readonly CultDocumentDescriptor Descriptor =
CultDocumentRegistry.Shared.GetRequired<TDocument>();
private readonly Func<Task<IReadOnlyList<TDocument>>> _latest;
private readonly Func<Observable<CultMeshCollectionChange<TDocument>>> _watchChanges;
/// <summary>Creates a collection handle.</summary>
public CultMeshCollectionHandle(
string collectionId,
Func<Task<IReadOnlyList<TDocument>>> latest,
Func<Observable<CultMeshCollectionChange<TDocument>>> watchChanges,
IEnumerable<CultMeshProjectionSource>? sources = null,
CultMeshRouteHint? routeHint = null)
{
CollectionId = string.IsNullOrWhiteSpace(collectionId)
? throw new ArgumentException("Value must be non-empty.", nameof(collectionId))
: collectionId;
_latest = latest ?? throw new ArgumentNullException(nameof(latest));
_watchChanges = watchChanges ?? throw new ArgumentNullException(nameof(watchChanges));
Sources = sources?.ToArray() ?? Array.Empty<CultMeshProjectionSource>();
RouteHint = routeHint ?? CultMeshRouteHint.Automatic;
}
/// <summary>Gets the semantic collection id.</summary>
public string CollectionId { get; }
/// <summary>Gets the CLR document type presented by this collection.</summary>
public Type DocumentType => typeof(TDocument);
/// <summary>Gets the stable CultCache schema name.</summary>
public string SchemaName => Descriptor.SchemaName;
/// <summary>Gets the stable CultCache schema version.</summary>
public string SchemaVersion => Descriptor.SchemaVersion;
/// <summary>Gets the content-derived schema identifier.</summary>
public string SchemaId => Descriptor.SchemaId;
/// <summary>Gets the preferred or observed route for collection access.</summary>
public CultMeshRouteHint RouteHint { get; }
/// <summary>Gets typed state sources this collection handle depends on, when known.</summary>
public IReadOnlyList<CultMeshProjectionSource> Sources { get; }
/// <summary>Reads one coherent collection snapshot.</summary>
public Task<IReadOnlyList<TDocument>> LatestAsync()
{
return _latest();
}
/// <summary>Watches typed collection changes.</summary>
public Observable<CultMeshCollectionChange<TDocument>> WatchChanges()
{
return _watchChanges();
}
/// <summary>Subscribes to typed collection changes.</summary>
public IDisposable WatchChanges(Action<CultMeshCollectionChange<TDocument>> onNext)
{
if (onNext == null) throw new ArgumentNullException(nameof(onNext));
return WatchChanges().Subscribe(onNext);
}
/// <summary>Creates a same-schema alias presentation for another CLR document type.</summary>
public CultMeshCollectionHandle<TAlias> AsSchemaAlias<TAlias>() where TAlias : class
{
var aliasDescriptor = CultDocumentRegistry.Shared.GetRequired<TAlias>();
if (!string.Equals(Descriptor.SchemaName, aliasDescriptor.SchemaName, StringComparison.Ordinal) ||
!string.Equals(Descriptor.SchemaVersion, aliasDescriptor.SchemaVersion, StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"Document type {typeof(TAlias).FullName} uses schema '{aliasDescriptor.SchemaName}' " +
$"version '{aliasDescriptor.SchemaVersion}', but collection '{CollectionId}' exposes " +
$"schema '{SchemaName}' version '{SchemaVersion}'.");
}
return new CultMeshCollectionHandle<TAlias>(
CollectionId,
async () => (await LatestAsync().ConfigureAwait(false))
.Select(ConvertDocument<TDocument, TAlias>)
.ToArray(),
() => WatchChanges().Select(change => new CultMeshCollectionChange<TAlias>(
change.Kind,
change.Key,
change.SchemaId,
change.Document == null ? null : ConvertDocument<TDocument, TAlias>(change.Document),
change.PreviousDocument == null ? null : ConvertDocument<TDocument, TAlias>(change.PreviousDocument),
change.Message)),
Sources,
RouteHint);
}
private static TTarget ConvertDocument<TSource, TTarget>(TSource document)
where TSource : class
where TTarget : class
{
if (document == null) throw new ArgumentNullException(nameof(document));
if (document is TTarget alreadyTyped)
{
return alreadyTyped;
}
var payload = CultDocumentMessagePackSerialization.SerializeUntyped(document, typeof(TSource));
return (TTarget)CultDocumentMessagePackSerialization.DeserializeUntyped(typeof(TTarget), payload);
}
}
/// <summary>
/// Inspectable metadata for a typed live feed surface.
/// </summary>

View file

@ -176,6 +176,13 @@ portable machinery that makes it feel native.
`CultMeshDocumentHandle<TDocument>.AsSchemaAlias<TAlias>()`. Domain facades
can expose `Document<T>()`, `DocumentBySchema(...)`, `LatestAsync<T>()`, and
`Watch<T>()` without owning schema dictionaries or alias serializers.
- `CultMeshCollectionHandle<TDocument>` is the typed reactive edge for
multi-record state. It exposes one `LatestAsync()` collection snapshot and a
`WatchChanges()` stream while hiding whether the backing source is an
in-process `CultCache`, a distributed `CultNetDatabase`, or a `CultMeshNode`.
Collection handles can represent all records of a document type, a named
document view, or an indexed value view, and they support the same
same-schema CLR alias conversion as document handles.
- `CultMeshProjectionRecipe<TParameters, TResult>` names a reusable projection
from typed source state into derived state. It records source handles,
route hints, and projection execution, and can be exposed as a typed query
@ -273,6 +280,20 @@ var uiDocking = documents.Document<CurrentDockingUiDocument>();
var bySchema = documents.DocumentBySchema("gamecult.aetheria.station_refit.v1");
```
Typed collections use the same one-call style:
```csharp
var allies = CultMesh.CollectionByIndex<PlayerShipDocument>(
node,
"Faction",
"au");
var currentAllies = await allies.LatestAsync();
using var changes = allies.WatchChanges(change => UpdateRoster(change));
var uiAllies = allies.AsSchemaAlias<PlayerShipUiDocument>();
```
```ts
const queryVerse = verse.withRoute("shared-memory", statePath);
const commandVerse = verse

View file

@ -686,6 +686,33 @@ namespace GameCult.Networking
return Task.FromResult(_cache.Get<T>(key));
}
/// <summary>
/// Gets all documents assignable to the requested type.
/// </summary>
public IEnumerable<T> GetAll<T>() where T : class
{
ThrowIfDisposed();
return _cache.GetAll<T>();
}
/// <summary>
/// Gets a typed document by its CultName value.
/// </summary>
public T? GetByName<T>(string name) where T : class
{
ThrowIfDisposed();
return _cache.GetByName<T>(name);
}
/// <summary>
/// Gets a typed document by an indexed value.
/// </summary>
public T? GetByIndex<T>(string alias, string value) where T : class
{
ThrowIfDisposed();
return _cache.GetByIndex<T>(alias, value);
}
/// <summary>
/// Opens a reactive POCO presentation for one distributed CultCache document.
/// </summary>

View file

@ -771,6 +771,71 @@ public sealed class CultMeshStreamingTests
observed.Revision.Should().Be(2);
}
[Test]
public async Task CollectionHandle_ReadsAndWatchesDatabaseCollectionsByIndex()
{
var cache = new CultCache();
var database = new CultNetDatabase(cache);
var teamId = Guid.NewGuid().ToString("D");
var collection = CultMesh.CollectionByIndex<MeshIndexedPlayer>(
database,
"TeamId",
teamId);
var changes = new List<CultMeshCollectionChange<MeshIndexedPlayer>>();
using var subscription = collection.WatchChanges(change => changes.Add(change));
await database.PutAsync(
new CultRecordKey("player:one"),
new MeshIndexedPlayer
{
Name = "One",
TeamId = teamId,
Score = 10
});
await database.PutAsync(
new CultRecordKey("player:two"),
new MeshIndexedPlayer
{
Name = "Two",
TeamId = Guid.NewGuid().ToString("D"),
Score = 20
});
collection.CollectionId.Should().Contain(teamId);
collection.SchemaName.Should().Be("tests.mesh_indexed_player");
collection.RouteHint.Kind.Should().Be(CultMeshLocalityKind.Automatic);
collection.Sources.Should().ContainSingle().Which.SchemaId.Should().Be(collection.SchemaId);
var snapshot = await collection.LatestAsync();
snapshot.Should().ContainSingle().Which.Name.Should().Be("One");
changes.Should().ContainSingle();
changes[0].Kind.Should().Be(CultMeshCollectionChangeKind.Added);
changes[0].Document!.Name.Should().Be("One");
}
[Test]
public async Task CollectionHandle_SupportsSameSchemaAliases()
{
var cache = new CultCache();
await cache.UpsertAsync(
new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "collection-alias",
Revision = 4
},
new CultRecordHandle<MeshNoteDocument>(new CultRecordKey("mesh-note:collection")));
var collection = CultMesh.Collection<MeshNoteDocument>(cache);
var alias = collection.AsSchemaAlias<MeshNoteAliasDocument>();
alias.CollectionId.Should().Be(collection.CollectionId);
alias.SchemaName.Should().Be(collection.SchemaName);
var snapshot = await alias.LatestAsync();
snapshot.Should().ContainSingle().Which.Text.Should().Be("collection-alias");
}
[Test]
public async Task DocumentHandle_ReplacesThroughCultMeshNodeDatabase()
{
@ -1193,6 +1258,21 @@ public sealed class CultMeshStreamingTests
public int Health;
}
[CultDocument("tests.mesh_indexed_player", "tests.mesh_indexed_player.v1")]
[MessagePackObject(AllowPrivate = true)]
internal sealed class MeshIndexedPlayer
{
[Key(0)]
public string Name { get; set; } = string.Empty;
[Key(1)]
[CultIndex]
public string TeamId { get; set; } = string.Empty;
[Key(2)]
public int Score { get; set; }
}
[CultDocument("tests.mesh_note", "tests.mesh_note.v1")]
[MessagePackObject(AllowPrivate = true)]
internal sealed class MeshNoteDocument