From df079f2cab5e9e5ff31fe16b46560ba41f65b637 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 27 Jun 2026 15:01:23 +0100 Subject: [PATCH] Add CultMesh document catalogs --- src/GameCult.Mesh/CultMesh.cs | 16 +++ src/GameCult.Mesh/CultMeshPrimitives.cs | 124 ++++++++++++++++++ src/GameCult.Mesh/docs/public-api.md | 19 +++ .../CultMeshStreamingTests.cs | 33 +++++ 4 files changed, 192 insertions(+) diff --git a/src/GameCult.Mesh/CultMesh.cs b/src/GameCult.Mesh/CultMesh.cs index cfde04af..7742b8ac 100644 --- a/src/GameCult.Mesh/CultMesh.cs +++ b/src/GameCult.Mesh/CultMesh.cs @@ -1120,6 +1120,22 @@ namespace GameCult.Mesh return Document(node, key, verse.Context, documentId, sources, routeHint); } + /// + /// Creates a schema-aware catalog over typed document handles. + /// + public static CultMeshDocumentCatalog Documents(IEnumerable documents) + { + return new CultMeshDocumentCatalog(documents); + } + + /// + /// Creates a schema-aware catalog over typed document handles. + /// + public static CultMeshDocumentCatalog Documents(params ICultMeshDocumentHandle[] documents) + { + return new CultMeshDocumentCatalog(documents); + } + /// /// Describes a typed live feed surface for tooling, UI, and diagnostics. /// diff --git a/src/GameCult.Mesh/CultMeshPrimitives.cs b/src/GameCult.Mesh/CultMeshPrimitives.cs index 70c451ea..22624d86 100644 --- a/src/GameCult.Mesh/CultMeshPrimitives.cs +++ b/src/GameCult.Mesh/CultMeshPrimitives.cs @@ -1031,6 +1031,130 @@ namespace GameCult.Mesh } } + /// + /// Schema-aware lookup table for typed CultMesh document handles. + /// + public sealed class CultMeshDocumentCatalog + { + private readonly IReadOnlyList _documents; + private readonly IReadOnlyDictionary _documentsByType; + private readonly IReadOnlyDictionary _documentsBySchema; + + /// Creates a document catalog from known handles. + public CultMeshDocumentCatalog(IEnumerable documents) + { + if (documents == null) throw new ArgumentNullException(nameof(documents)); + + var documentList = documents + .Where(document => document != null) + .ToArray(); + var byType = new Dictionary(); + var bySchema = new Dictionary(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; + } + + /// Gets the handles in this catalog. + public IReadOnlyList Documents => _documents; + + /// Looks up a document handle by shared schema name or schema version. + public bool TryGetDocumentBySchema( + string schema, + out ICultMeshDocumentHandle document) + { + if (!string.IsNullOrWhiteSpace(schema) && + _documentsBySchema.TryGetValue(schema, out document!)) + { + return true; + } + + document = null!; + return false; + } + + /// Looks up a document handle by shared schema name or schema version. + 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}'."); + } + + /// Looks up a typed document handle by CLR type or same-schema alias. + public bool TryGetDocument( + out CultMeshDocumentHandle document) + where TDocument : class + { + if (_documentsByType.TryGetValue(typeof(TDocument), out var untypedDocument) && + untypedDocument is CultMeshDocumentHandle typedDocument) + { + document = typedDocument; + return true; + } + + var descriptor = CultDocumentRegistry.Shared.GetRequired(); + if (_documentsBySchema.TryGetValue(descriptor.SchemaVersion, out var schemaDocument) || + _documentsBySchema.TryGetValue(descriptor.SchemaName, out schemaDocument!)) + { + if (schemaDocument is CultMeshDocumentHandle schemaTypedDocument) + { + document = schemaTypedDocument; + return true; + } + + document = schemaDocument.AsSchemaAlias(); + return true; + } + + document = null!; + return false; + } + + /// Looks up a typed document handle by CLR type or same-schema alias. + public CultMeshDocumentHandle Document() + where TDocument : class + { + if (TryGetDocument(out var document)) + return document; + + throw new NotSupportedException( + $"CultMesh document catalog does not expose a document for {typeof(TDocument).FullName}."); + } + + /// Reads one typed document snapshot by CLR type or same-schema alias. + public Task LatestAsync() + where TDocument : class + { + return Document().LatestAsync(); + } + + /// Watches one typed document by CLR type or same-schema alias. + public Observable Watch() + where TDocument : class + { + return Document().Watch(); + } + + /// Subscribes to one typed document by CLR type or same-schema alias. + public IDisposable Watch(Action onNext) + where TDocument : class + { + return Document().Watch(onNext); + } + } + /// /// Inspectable metadata for a typed live feed surface. /// diff --git a/src/GameCult.Mesh/docs/public-api.md b/src/GameCult.Mesh/docs/public-api.md index 507b82d7..e789f8d6 100644 --- a/src/GameCult.Mesh/docs/public-api.md +++ b/src/GameCult.Mesh/docs/public-api.md @@ -170,6 +170,12 @@ portable machinery that makes it feel native. with `AsSchemaAlias()`; 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.AsSchemaAlias()`. Domain facades + can expose `Document()`, `DocumentBySchema(...)`, `LatestAsync()`, and + `Watch()` without owning schema dictionaries or alias serializers. - `CultMeshProjectionRecipe` 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(); ``` +A domain facade can collect handles into one schema-aware catalog: + +```csharp +var documents = CultMesh.Documents( + currentDocking, + stationRefit, + zoneContacts); + +var refit = await documents.LatestAsync(); +var uiDocking = documents.Document(); +var bySchema = documents.DocumentBySchema("gamecult.aetheria.station_refit.v1"); +``` + ```ts const queryVerse = verse.withRoute("shared-memory", statePath); const commandVerse = verse diff --git a/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs b/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs index 1324561c..eb902889 100644 --- a/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs +++ b/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs @@ -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()); + var catalog = CultMesh.Documents(handle); + + catalog.Documents.Should().ContainSingle().Which.Should().BeSameAs(handle); + catalog.TryGetDocument(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(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(); + aliasSnapshot.Text.Should().Be("catalog-primary"); + aliasSnapshot.Revision.Should().Be(3); + } + [Test] public async Task DocumentHandle_ReadsWatchesAndReplacesCultCacheRecords() {