Add CultMesh document catalogs

This commit is contained in:
Meta Vens 2026-06-27 15:01:23 +01:00
commit df079f2cab
4 changed files with 192 additions and 0 deletions

View file

@ -1120,6 +1120,22 @@ namespace GameCult.Mesh
return Document<TDocument>(node, key, verse.Context, documentId, sources, routeHint);
}
/// <summary>
/// Creates a schema-aware catalog over typed document handles.
/// </summary>
public static CultMeshDocumentCatalog Documents(IEnumerable<ICultMeshDocumentHandle> documents)
{
return new CultMeshDocumentCatalog(documents);
}
/// <summary>
/// Creates a schema-aware catalog over typed document handles.
/// </summary>
public static CultMeshDocumentCatalog Documents(params ICultMeshDocumentHandle[] documents)
{
return new CultMeshDocumentCatalog(documents);
}
/// <summary>
/// Describes a typed live feed surface for tooling, UI, and diagnostics.
/// </summary>

View file

@ -1031,6 +1031,130 @@ namespace GameCult.Mesh
}
}
/// <summary>
/// Schema-aware lookup table for typed CultMesh document handles.
/// </summary>
public sealed class CultMeshDocumentCatalog
{
private readonly IReadOnlyList<ICultMeshDocumentHandle> _documents;
private readonly IReadOnlyDictionary<Type, ICultMeshDocumentHandle> _documentsByType;
private readonly IReadOnlyDictionary<string, ICultMeshDocumentHandle> _documentsBySchema;
/// <summary>Creates a document catalog from known handles.</summary>
public CultMeshDocumentCatalog(IEnumerable<ICultMeshDocumentHandle> documents)
{
if (documents == null) throw new ArgumentNullException(nameof(documents));
var documentList = documents
.Where(document => document != null)
.ToArray();
var byType = new Dictionary<Type, ICultMeshDocumentHandle>();
var bySchema = new Dictionary<string, ICultMeshDocumentHandle>(StringComparer.Ordinal);
foreach (var document in documentList)
{
byType[document.DocumentType] = document;
if (!string.IsNullOrWhiteSpace(document.SchemaVersion))
bySchema[document.SchemaVersion] = document;
if (!string.IsNullOrWhiteSpace(document.SchemaName))
bySchema[document.SchemaName] = document;
}
_documents = documentList;
_documentsByType = byType;
_documentsBySchema = bySchema;
}
/// <summary>Gets the handles in this catalog.</summary>
public IReadOnlyList<ICultMeshDocumentHandle> Documents => _documents;
/// <summary>Looks up a document handle by shared schema name or schema version.</summary>
public bool TryGetDocumentBySchema(
string schema,
out ICultMeshDocumentHandle document)
{
if (!string.IsNullOrWhiteSpace(schema) &&
_documentsBySchema.TryGetValue(schema, out document!))
{
return true;
}
document = null!;
return false;
}
/// <summary>Looks up a document handle by shared schema name or schema version.</summary>
public ICultMeshDocumentHandle DocumentBySchema(string schema)
{
if (TryGetDocumentBySchema(schema, out var document))
return document;
throw new NotSupportedException(
$"CultMesh document catalog does not expose a document for schema '{schema}'.");
}
/// <summary>Looks up a typed document handle by CLR type or same-schema alias.</summary>
public bool TryGetDocument<TDocument>(
out CultMeshDocumentHandle<TDocument> document)
where TDocument : class
{
if (_documentsByType.TryGetValue(typeof(TDocument), out var untypedDocument) &&
untypedDocument is CultMeshDocumentHandle<TDocument> typedDocument)
{
document = typedDocument;
return true;
}
var descriptor = CultDocumentRegistry.Shared.GetRequired<TDocument>();
if (_documentsBySchema.TryGetValue(descriptor.SchemaVersion, out var schemaDocument) ||
_documentsBySchema.TryGetValue(descriptor.SchemaName, out schemaDocument!))
{
if (schemaDocument is CultMeshDocumentHandle<TDocument> schemaTypedDocument)
{
document = schemaTypedDocument;
return true;
}
document = schemaDocument.AsSchemaAlias<TDocument>();
return true;
}
document = null!;
return false;
}
/// <summary>Looks up a typed document handle by CLR type or same-schema alias.</summary>
public CultMeshDocumentHandle<TDocument> Document<TDocument>()
where TDocument : class
{
if (TryGetDocument<TDocument>(out var document))
return document;
throw new NotSupportedException(
$"CultMesh document catalog does not expose a document for {typeof(TDocument).FullName}.");
}
/// <summary>Reads one typed document snapshot by CLR type or same-schema alias.</summary>
public Task<TDocument> LatestAsync<TDocument>()
where TDocument : class
{
return Document<TDocument>().LatestAsync();
}
/// <summary>Watches one typed document by CLR type or same-schema alias.</summary>
public Observable<TDocument> Watch<TDocument>()
where TDocument : class
{
return Document<TDocument>().Watch();
}
/// <summary>Subscribes to one typed document by CLR type or same-schema alias.</summary>
public IDisposable Watch<TDocument>(Action<TDocument> onNext)
where TDocument : class
{
return Document<TDocument>().Watch(onNext);
}
}
/// <summary>
/// Inspectable metadata for a typed live feed surface.
/// </summary>

View file

@ -170,6 +170,12 @@ portable machinery that makes it feel native.
with `AsSchemaAlias<TAlias>()`; CultMesh verifies the shared
`[CultDocument(schemaName, schemaVersion)]` identity and uses the shared
CultCache MessagePack codec for read/watch/replace conversion.
- `CultMeshDocumentCatalog` is the schema-aware lookup edge for a set of
document handles. It indexes handles by CLR document type, schema name, and
schema version, and resolves same-schema CLR aliases by delegating to
`CultMeshDocumentHandle<TDocument>.AsSchemaAlias<TAlias>()`. Domain facades
can expose `Document<T>()`, `DocumentBySchema(...)`, `LatestAsync<T>()`, and
`Watch<T>()` without owning schema dictionaries or alias serializers.
- `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
@ -254,6 +260,19 @@ using var subscription = cockpit.Watch(next => RenderCockpit(next));
var daemonAlias = cockpit.AsSchemaAlias<DaemonCockpitState>();
```
A domain facade can collect handles into one schema-aware catalog:
```csharp
var documents = CultMesh.Documents(
currentDocking,
stationRefit,
zoneContacts);
var refit = await documents.LatestAsync<StationRefitDocument>();
var uiDocking = documents.Document<CurrentDockingUiDocument>();
var bySchema = documents.DocumentBySchema("gamecult.aetheria.station_refit.v1");
```
```ts
const queryVerse = verse.withRoute("shared-memory", statePath);
const commandVerse = verse

View file

@ -698,6 +698,39 @@ public sealed class CultMeshStreamingTests
.WithMessage("*tests.mesh_other*tests.mesh_note*");
}
[Test]
public async Task DocumentCatalog_IndexesHandlesByTypeAndSchemaAlias()
{
var current = new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "catalog-primary",
Revision = 3
};
var handle = CultMesh.Document(
"mesh.note.current",
CultMesh.Verse("starbridge", "unity-pilot"),
_ => Task.FromResult(current),
_ => new Subject<MeshNoteDocument>());
var catalog = CultMesh.Documents(handle);
catalog.Documents.Should().ContainSingle().Which.Should().BeSameAs(handle);
catalog.TryGetDocument<MeshNoteDocument>(out var exact).Should().BeTrue();
exact.Should().BeSameAs(handle);
catalog.TryGetDocumentBySchema("tests.mesh_note.v1", out var byVersion).Should().BeTrue();
byVersion.Should().BeSameAs(handle);
catalog.DocumentBySchema("tests.mesh_note").Should().BeSameAs(handle);
catalog.TryGetDocument<MeshNoteAliasDocument>(out var alias).Should().BeTrue();
alias.DocumentId.Should().Be(handle.DocumentId);
alias.SchemaName.Should().Be(handle.SchemaName);
alias.SchemaVersion.Should().Be(handle.SchemaVersion);
var aliasSnapshot = await catalog.LatestAsync<MeshNoteAliasDocument>();
aliasSnapshot.Text.Should().Be("catalog-primary");
aliasSnapshot.Revision.Should().Be(3);
}
[Test]
public async Task DocumentHandle_ReadsWatchesAndReplacesCultCacheRecords()
{