From 7bb6d7da8c65222c614dbee51973fb40124e043b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 27 Jun 2026 18:23:26 +0100 Subject: [PATCH] Add CultMesh multi-document latest helpers --- src/GameCult.Mesh/CultMesh.cs | 49 +++++++++++++++++++ .../CultMeshStreamingTests.cs | 31 ++++++++++++ 2 files changed, 80 insertions(+) diff --git a/src/GameCult.Mesh/CultMesh.cs b/src/GameCult.Mesh/CultMesh.cs index ecc79c49..45694e6a 100644 --- a/src/GameCult.Mesh/CultMesh.cs +++ b/src/GameCult.Mesh/CultMesh.cs @@ -1675,6 +1675,55 @@ namespace GameCult.Mesh return new CultMeshDocumentCatalog(documents); } + /// + /// Reads two typed document handles and projects them into one caller-defined view. + /// + public static async Task LatestAsync( + CultMeshDocumentHandle first, + CultMeshDocumentHandle second, + Func project) + where TFirst : class + where TSecond : class + { + if (first == null) throw new ArgumentNullException(nameof(first)); + if (second == null) throw new ArgumentNullException(nameof(second)); + if (project == null) throw new ArgumentNullException(nameof(project)); + + var firstTask = first.LatestAsync(); + var secondTask = second.LatestAsync(); + await Task.WhenAll(firstTask, secondTask).ConfigureAwait(false); + return project( + await firstTask.ConfigureAwait(false), + await secondTask.ConfigureAwait(false)); + } + + /// + /// Reads three typed document handles and projects them into one caller-defined view. + /// + public static async Task LatestAsync( + CultMeshDocumentHandle first, + CultMeshDocumentHandle second, + CultMeshDocumentHandle third, + Func project) + where TFirst : class + where TSecond : class + where TThird : class + { + if (first == null) throw new ArgumentNullException(nameof(first)); + if (second == null) throw new ArgumentNullException(nameof(second)); + if (third == null) throw new ArgumentNullException(nameof(third)); + if (project == null) throw new ArgumentNullException(nameof(project)); + + var firstTask = first.LatestAsync(); + var secondTask = second.LatestAsync(); + var thirdTask = third.LatestAsync(); + await Task.WhenAll(firstTask, secondTask, thirdTask).ConfigureAwait(false); + return project( + await firstTask.ConfigureAwait(false), + await secondTask.ConfigureAwait(false), + await thirdTask.ConfigureAwait(false)); + } + /// /// Creates a typed collection handle over all local CultCache records assignable to the document type. /// diff --git a/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs b/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs index 356b5dec..6f8fae18 100644 --- a/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs +++ b/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs @@ -736,6 +736,37 @@ public sealed class CultMeshStreamingTests catalog.Latest().Text.Should().Be("catalog-primary"); } + [Test] + public async Task DocumentHelpers_ProjectLatestSnapshotsFromMultipleHandles() + { + var primary = CultMesh.Document( + "mesh.note.primary", + CultMesh.Verse("starbridge", "unity-pilot"), + _ => Task.FromResult(new MeshNoteDocument + { + Schema = "tests.mesh_note.v1", + Text = "alpha", + Revision = 3 + }), + _ => new Subject()); + var secondary = CultMesh.Document( + "mesh.note.secondary", + CultMesh.Verse("starbridge", "unity-pilot"), + _ => Task.FromResult(new MeshOtherDocument + { + Schema = "tests.mesh_other.v1", + Text = "beta" + }), + _ => new Subject()); + + var projected = await CultMesh.LatestAsync( + primary, + secondary, + (first, second) => $"{first.Text}:{first.Revision}:{second.Text}"); + + projected.Should().Be("alpha:3:beta"); + } + [Test] public async Task DocumentHandle_ReadsWatchesAndReplacesCultCacheRecords() {