From c911eb561d2e199fd8babe9548aa12e849e8e221 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 27 Jun 2026 14:30:59 +0100 Subject: [PATCH] Add CultMesh typed document handles --- src/GameCult.Mesh/CultMesh.cs | 791 +++++ src/GameCult.Mesh/CultMeshPrimitives.cs | 2775 +++++++++++++++++ src/GameCult.Mesh/docs/public-api.md | 267 ++ .../CultMeshStreamingTests.cs | 999 ++++++ 4 files changed, 4832 insertions(+) create mode 100644 src/GameCult.Mesh/CultMeshPrimitives.cs diff --git a/src/GameCult.Mesh/CultMesh.cs b/src/GameCult.Mesh/CultMesh.cs index 9cc40ca2..b4e93588 100644 --- a/src/GameCult.Mesh/CultMesh.cs +++ b/src/GameCult.Mesh/CultMesh.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; @@ -297,6 +298,796 @@ namespace GameCult.Mesh return new CultMeshStreamCatalog(); } + /// + /// Creates a managed Verse context for generated domain sugar. + /// + public static CultMeshVerse Verse( + string verseId, + string runtimeId, + CultMeshRouteHint? routeHint = null, + IEnumerable? claims = null) + { + return new CultMeshVerse(new CultMeshVerseContext(verseId, runtimeId, routeHint, claims)); + } + + /// + /// Creates a managed Verse context for generated domain sugar. + /// + public static Task ConnectVerseAsync( + string verseId, + string runtimeId, + CultMeshRouteHint? routeHint = null, + IEnumerable? claims = null) + { + return Task.FromResult(Verse(verseId, runtimeId, routeHint, claims)); + } + + /// + /// Starts a fluent typed operation context for one runtime. + /// + public static CultMeshOperationContextBuilder OperationContextFor(string runtimeId) + { + return new CultMeshOperationContextBuilder(runtimeId); + } + + /// + /// Starts a fluent typed query context for one runtime. + /// + public static CultMeshQueryContextBuilder QueryContextFor(string runtimeId) + { + return new CultMeshQueryContextBuilder(runtimeId); + } + + /// + /// Binds a typed operation handle to a Verse context. + /// + public static CultMeshBoundOperationHandle BindOperation( + CultMeshVerseContext context, + CultMeshOperationHandle operation) + { + return new CultMeshBoundOperationHandle(context, operation); + } + + /// + /// Binds a typed operation handle to a Verse. + /// + public static CultMeshBoundOperationHandle BindOperation( + CultMeshVerse verse, + CultMeshOperationHandle operation) + { + if (verse == null) throw new ArgumentNullException(nameof(verse)); + return BindOperation(verse.Context, operation); + } + + /// + /// Binds a typed query surface to a Verse context. + /// + public static CultMeshBoundQuerySurface BindQuery( + CultMeshVerseContext context, + CultMeshQuerySurface query) + { + return new CultMeshBoundQuerySurface(context, query); + } + + /// + /// Binds a typed query surface to a Verse. + /// + public static CultMeshBoundQuerySurface BindQuery( + CultMeshVerse verse, + CultMeshQuerySurface query) + { + if (verse == null) throw new ArgumentNullException(nameof(verse)); + return BindQuery(verse.Context, query); + } + + /// + /// Binds a typed live feed to a Verse context. + /// + public static CultMeshBoundLiveFeed BindLiveFeed( + CultMeshVerseContext context, + CultMeshLiveFeed feed) + { + return new CultMeshBoundLiveFeed(context, feed); + } + + /// + /// Binds a typed live feed to a Verse. + /// + public static CultMeshBoundLiveFeed BindLiveFeed( + CultMeshVerse verse, + CultMeshLiveFeed feed) + { + if (verse == null) throw new ArgumentNullException(nameof(verse)); + return BindLiveFeed(verse.Context, feed); + } + + /// + /// Binds a typed document live feed to a Verse context. + /// + public static CultMeshDocumentHandle BindDocument( + CultMeshVerseContext context, + CultMeshLiveFeed feed) + where TDocument : class + { + return new CultMeshDocumentHandle(BindLiveFeed(context, feed)); + } + + /// + /// Binds a typed document live feed to a Verse. + /// + public static CultMeshDocumentHandle BindDocument( + CultMeshVerse verse, + CultMeshLiveFeed feed) + where TDocument : class + { + if (verse == null) throw new ArgumentNullException(nameof(verse)); + return BindDocument(verse.Context, feed); + } + + /// + /// Binds a typed state pointer to a Verse context. + /// + public static CultMeshBoundStatePointer BindStatePointer( + CultMeshVerseContext context, + CultMeshStatePointer pointer) + { + return new CultMeshBoundStatePointer(context, pointer); + } + + /// + /// Binds a typed state pointer to a Verse. + /// + public static CultMeshBoundStatePointer BindStatePointer( + CultMeshVerse verse, + CultMeshStatePointer pointer) + { + if (verse == null) throw new ArgumentNullException(nameof(verse)); + return BindStatePointer(verse.Context, pointer); + } + + /// + /// Binds a mutable typed state pointer to a Verse context. + /// + public static CultMeshBoundMutableStatePointer BindMutableStatePointer( + CultMeshVerseContext context, + CultMeshMutableStatePointer pointer) + { + return new CultMeshBoundMutableStatePointer(context, pointer); + } + + /// + /// Binds a mutable typed state pointer to a Verse. + /// + public static CultMeshBoundMutableStatePointer BindMutableStatePointer( + CultMeshVerse verse, + CultMeshMutableStatePointer pointer) + { + if (verse == null) throw new ArgumentNullException(nameof(verse)); + return BindMutableStatePointer(verse.Context, pointer); + } + + /// + /// Creates a typed state pointer to Verse state. + /// + public static CultMeshStatePointer StatePointer( + string pointerId, + Func> resolve, + Func> watch, + CultMeshRouteHint? routeHint = null, + IEnumerable? sources = null) + { + return new CultMeshStatePointer( + pointerId, + resolve, + watch, + routeHint, + sources); + } + + /// + /// Creates a typed state pointer to Verse state that can resolve through a query context. + /// + public static CultMeshStatePointer StatePointer( + string pointerId, + Func> resolve, + Func> watch, + CultMeshRouteHint? routeHint = null, + IEnumerable? sources = null) + { + return new CultMeshStatePointer( + pointerId, + resolve, + watch, + routeHint, + sources); + } + + /// + /// Creates a mutable typed state pointer to Verse state. + /// + public static CultMeshMutableStatePointer MutableStatePointer( + string pointerId, + Func> resolve, + Func> watch, + Func replace, + CultMeshRouteHint? routeHint = null, + IEnumerable? sources = null) + { + return new CultMeshMutableStatePointer( + pointerId, + resolve, + watch, + replace, + routeHint, + sources); + } + + /// + /// Creates a mutable typed state pointer to Verse state that can operate through a query context. + /// + public static CultMeshMutableStatePointer MutableStatePointer( + string pointerId, + Func> resolve, + Func> watch, + Func replace, + CultMeshRouteHint? routeHint = null, + IEnumerable? sources = null) + { + return new CultMeshMutableStatePointer( + pointerId, + resolve, + watch, + replace, + routeHint, + sources); + } + + /// + /// Creates a typed projection source descriptor. + /// + public static CultMeshProjectionSource ProjectionSource( + string sourceId, + string? schemaId = null, + string? description = null) + { + return new CultMeshProjectionSource(sourceId, schemaId, description); + } + + /// + /// Creates a named state-reference resolver for portable UI/tool surfaces. + /// + public static CultMeshStateRefResolver StateRefResolver( + string resolverId, + Func resolve, + IEnumerable? sources = null, + CultMeshRouteHint? routeHint = null) + { + if (resolve == null) throw new ArgumentNullException(nameof(resolve)); + return new CultMeshStateRefResolver( + resolverId, + (stateRef, _context) => resolve(stateRef), + sources, + routeHint); + } + + /// + /// Creates a named state-reference resolver for portable UI/tool surfaces. + /// + public static CultMeshStateRefResolver StateRefResolver( + string resolverId, + Func resolve, + IEnumerable? sources = null, + CultMeshRouteHint? routeHint = null) + { + return new CultMeshStateRefResolver(resolverId, resolve, sources, routeHint); + } + + /// + /// Describes a state-reference resolver for catalogs and tools. + /// + public static CultMeshStateRefResolverDiagnostic DescribeStateRefResolver( + CultMeshStateRefResolver resolver) + { + if (resolver == null) throw new ArgumentNullException(nameof(resolver)); + return new CultMeshStateRefResolverDiagnostic( + resolver.ResolverId, + resolver.RouteHint, + resolver.Sources); + } + + /// + /// Creates a UI/tool binding descriptor from a component property to a typed state pointer. + /// + public static CultMeshStateBindingDescriptor StateBinding( + string targetProp, + CultMeshStatePointer pointer) + { + return CultMeshStateBindingDescriptor.FromPointer(targetProp, pointer); + } + + /// + /// Creates a UI/tool binding descriptor from a component property to a mutable typed state pointer. + /// + public static CultMeshStateBindingDescriptor StateBinding( + string targetProp, + CultMeshMutableStatePointer pointer) + { + if (pointer == null) throw new ArgumentNullException(nameof(pointer)); + return CultMeshStateBindingDescriptor.FromPointer(targetProp, pointer.AsStatePointer()); + } + + /// + /// Creates a UI/tool binding descriptor from a component property to a state pointer id. + /// + public static CultMeshStateBindingDescriptor StateBinding( + string targetProp, + string pointerId, + string? sourceId = null, + string? schemaId = null, + CultMeshRouteHint? routeHint = null) + { + return new CultMeshStateBindingDescriptor(targetProp, pointerId, sourceId, schemaId, routeHint); + } + + /// + /// Creates transport-friendly fields from a state binding descriptor. + /// + public static CultMeshStateBindingRecord StateBindingRecord(CultMeshStateBindingDescriptor? binding) + { + return CultMeshStateBindingRecord.FromBinding(binding); + } + + /// + /// Creates transport-friendly state binding fields directly. + /// + public static CultMeshStateBindingRecord StateBindingRecord( + string? targetProp, + string? pointerId, + string? sourceId = null, + string? schemaId = null, + string? routeKind = null, + string? routeDescription = null) + { + return new CultMeshStateBindingRecord( + targetProp, + pointerId, + sourceId, + schemaId, + routeKind, + routeDescription); + } + + /// + /// Creates a UI/tool command binding descriptor from a typed operation handle. + /// + public static CultMeshOperationBindingDescriptor OperationBinding( + CultMeshOperationHandle operation, + string? label = null, + string? schemaId = null, + CultMeshRouteHint? routeHint = null) + { + return CultMeshOperationBindingDescriptor.FromOperation(operation, label, schemaId, routeHint); + } + + /// + /// Creates a UI/tool command binding descriptor from a typed operation id. + /// + public static CultMeshOperationBindingDescriptor OperationBinding( + string operationId, + string? label = null, + string? schemaId = null, + CultMeshRouteHint? routeHint = null) + { + return new CultMeshOperationBindingDescriptor(operationId, label, schemaId, routeHint); + } + + /// + /// Creates transport-friendly fields from an operation binding descriptor. + /// + public static CultMeshOperationBindingRecord OperationBindingRecord( + CultMeshOperationBindingDescriptor? binding) + { + return CultMeshOperationBindingRecord.FromBinding(binding); + } + + /// + /// Creates transport-friendly operation binding fields directly. + /// + public static CultMeshOperationBindingRecord OperationBindingRecord( + string? operationId, + string? label = null, + string? schemaId = null, + string? routeKind = null, + string? routeDescription = null) + { + return new CultMeshOperationBindingRecord( + operationId, + label, + schemaId, + routeKind, + routeDescription); + } + + /// + /// Creates a concrete invocation descriptor from an advertised operation binding. + /// + public static CultMeshOperationInvocationDescriptor OperationInvocation( + CultMeshOperationBindingDescriptor binding, + string? idempotencyKey = null) + { + return CultMeshOperationInvocationDescriptor.FromBinding(binding, idempotencyKey); + } + + /// + /// Creates a concrete invocation descriptor from a typed operation handle. + /// + public static CultMeshOperationInvocationDescriptor OperationInvocation( + CultMeshOperationHandle operation, + string? schemaId = null, + CultMeshRouteHint? routeHint = null, + string? idempotencyKey = null) + { + return CultMeshOperationInvocationDescriptor.FromOperation( + operation, + schemaId, + routeHint, + idempotencyKey); + } + + /// + /// Creates a concrete invocation descriptor from a typed operation id. + /// + public static CultMeshOperationInvocationDescriptor OperationInvocation( + string operationId, + string? schemaId = null, + CultMeshRouteHint? routeHint = null, + string? idempotencyKey = null) + { + return new CultMeshOperationInvocationDescriptor(operationId, schemaId, routeHint, idempotencyKey); + } + + /// + /// Creates transport-friendly fields from a route hint. + /// + public static CultMeshRouteRecord RouteRecord(CultMeshRouteHint? routeHint) + { + return CultMeshRouteRecord.FromRoute(routeHint); + } + + /// + /// Creates transport-friendly route fields directly. + /// + public static CultMeshRouteRecord RouteRecord(string? kind, string? description = null) + { + return new CultMeshRouteRecord(kind, description); + } + + /// + /// Creates transport-friendly fields from a typed operation invocation. + /// + public static CultMeshOperationInvocationRecord OperationInvocationRecord( + CultMeshOperationInvocationDescriptor? invocation, + string? fallbackOperationId = null, + string? fallbackSchemaId = null, + CultMeshRouteHint? fallbackRouteHint = null, + string? fallbackIdempotencyKey = null) + { + return CultMeshOperationInvocationRecord.FromInvocation( + invocation, + fallbackOperationId, + fallbackSchemaId, + fallbackRouteHint, + fallbackIdempotencyKey); + } + + /// + /// Creates transport-friendly operation invocation fields directly. + /// + public static CultMeshOperationInvocationRecord OperationInvocationRecord( + string? operationId, + string? schemaId = null, + string? routeKind = null, + string? routeDescription = null, + string? idempotencyKey = null) + { + return new CultMeshOperationInvocationRecord( + operationId, + schemaId, + routeKind, + routeDescription, + idempotencyKey); + } + + /// + /// Creates an empty shared operation payload. + /// + public static CultMeshOperationPayload OperationPayload() + { + return CultMeshOperationPayload.Empty; + } + + /// + /// Creates a shared operation payload from string-compatible fields. + /// + public static CultMeshOperationPayload OperationPayload( + IEnumerable>? fields) + { + return CultMeshOperationPayload.FromStrings(fields); + } + + /// + /// Creates a shared operation payload from key/value pairs. + /// + public static CultMeshOperationPayload OperationPayload(params (string Key, string Value)[] fields) + { + var pairs = fields?.Select(field => new KeyValuePair(field.Key, field.Value)); + return CultMeshOperationPayload.FromStrings(pairs); + } + + /// + /// Creates a reusable projection recipe over typed source state. + /// + public static CultMeshProjectionRecipe ProjectionRecipe( + string projectionId, + IEnumerable sources, + Func> project, + CultMeshRouteHint? routeHint = null, + Func>? watch = null) + { + return new CultMeshProjectionRecipe( + projectionId, + sources, + project, + routeHint, + watch); + } + + /// + /// Creates a typed live feed surface for coherent client snapshots. + /// + public static CultMeshLiveFeed LiveFeed( + string feedId, + Func> snapshot, + Func>? watch = null, + IEnumerable? sources = null, + CultMeshRouteHint? routeHint = null) + { + return new CultMeshLiveFeed( + feedId, + snapshot, + watch, + sources, + routeHint); + } + + /// + /// Creates a typed document handle from snapshot/watch delegates and binds it to a Verse context. + /// + public static CultMeshDocumentHandle Document( + string documentId, + CultMeshVerseContext context, + Func> latest, + Func> watch, + IEnumerable? sources = null, + CultMeshRouteHint? routeHint = null) + where TDocument : class + { + if (latest == null) throw new ArgumentNullException(nameof(latest)); + if (watch == null) throw new ArgumentNullException(nameof(watch)); + + var feed = LiveFeed( + documentId, + (_parameters, queryContext) => latest(queryContext), + (_parameters, queryContext) => watch(queryContext), + sources, + routeHint); + return BindDocument(context, feed); + } + + /// + /// Creates a typed document handle from snapshot/watch delegates and binds it to a Verse. + /// + public static CultMeshDocumentHandle Document( + string documentId, + CultMeshVerse verse, + Func> latest, + Func> watch, + IEnumerable? sources = null, + CultMeshRouteHint? routeHint = null) + where TDocument : class + { + if (verse == null) throw new ArgumentNullException(nameof(verse)); + return Document(documentId, verse.Context, latest, watch, sources, routeHint); + } + + /// + /// Describes a typed live feed surface for tooling, UI, and diagnostics. + /// + public static CultMeshLiveFeedDiagnostic DescribeLiveFeed( + CultMeshLiveFeed feed) + { + if (feed == null) throw new ArgumentNullException(nameof(feed)); + return new CultMeshLiveFeedDiagnostic(feed.FeedId, feed.RouteHint, feed.Sources); + } + + /// + /// Describes a typed query surface for tooling, UI, and diagnostics. + /// + public static CultMeshQuerySurfaceDiagnostic DescribeQuerySurface( + CultMeshQuerySurface query) + { + if (query == null) throw new ArgumentNullException(nameof(query)); + return new CultMeshQuerySurfaceDiagnostic(query.QueryId, query.RouteHint, query.Sources); + } + + /// + /// Describes a typed operation handle for tooling, UI, and diagnostics. + /// + public static CultMeshOperationHandleDiagnostic DescribeOperationHandle( + CultMeshOperationHandle operation) + { + if (operation == null) throw new ArgumentNullException(nameof(operation)); + return new CultMeshOperationHandleDiagnostic(operation.OperationId); + } + + /// + /// Describes a typed state pointer for tooling, UI, and diagnostics. + /// + public static CultMeshStatePointerDiagnostic DescribeStatePointer( + CultMeshStatePointer pointer) + { + if (pointer == null) throw new ArgumentNullException(nameof(pointer)); + return new CultMeshStatePointerDiagnostic(pointer.PointerId, pointer.RouteHint, pointer.Sources); + } + + /// + /// Describes a mutable typed state pointer for tooling, UI, and diagnostics. + /// + public static CultMeshStatePointerDiagnostic DescribeStatePointer( + CultMeshMutableStatePointer pointer) + { + if (pointer == null) throw new ArgumentNullException(nameof(pointer)); + return new CultMeshStatePointerDiagnostic(pointer.PointerId, pointer.RouteHint, pointer.Sources); + } + + /// + /// Describes a native slice view for tooling, UI, and diagnostics. + /// + public static CultMeshNativeSliceViewDiagnostic DescribeNativeSliceView( + CultMeshNativeSliceViewDescriptor view) + { + return new CultMeshNativeSliceViewDiagnostic(view); + } + + /// + /// Describes a typed projection recipe for tooling, UI, and diagnostics. + /// + public static CultMeshProjectionRecipeDiagnostic DescribeProjectionRecipe( + CultMeshProjectionRecipe recipe) + { + if (recipe == null) throw new ArgumentNullException(nameof(recipe)); + return new CultMeshProjectionRecipeDiagnostic(recipe.ProjectionId, recipe.RouteHint, recipe.Sources); + } + + /// + /// Describes a typed query surface as one entry in a surface catalog. + /// + public static CultMeshSurfaceDiagnostic DescribeSurface( + CultMeshQuerySurface query) + { + if (query == null) throw new ArgumentNullException(nameof(query)); + return new CultMeshSurfaceDiagnostic(CultMeshSurfaceKind.Query, query.QueryId, query.RouteHint, query.Sources); + } + + /// + /// Describes a typed operation handle as one entry in a surface catalog. + /// + public static CultMeshSurfaceDiagnostic DescribeSurface( + CultMeshOperationHandle operation) + { + if (operation == null) throw new ArgumentNullException(nameof(operation)); + return new CultMeshSurfaceDiagnostic(CultMeshSurfaceKind.Operation, operation.OperationId); + } + + /// + /// Describes a typed projection recipe as one entry in a surface catalog. + /// + public static CultMeshSurfaceDiagnostic DescribeSurface( + CultMeshProjectionRecipe recipe) + { + if (recipe == null) throw new ArgumentNullException(nameof(recipe)); + return new CultMeshSurfaceDiagnostic(CultMeshSurfaceKind.ProjectionRecipe, recipe.ProjectionId, recipe.RouteHint, recipe.Sources); + } + + /// + /// Describes a typed live feed as one entry in a surface catalog. + /// + public static CultMeshSurfaceDiagnostic DescribeSurface( + CultMeshLiveFeed feed) + { + if (feed == null) throw new ArgumentNullException(nameof(feed)); + return new CultMeshSurfaceDiagnostic(CultMeshSurfaceKind.LiveFeed, feed.FeedId, feed.RouteHint, feed.Sources); + } + + /// + /// Describes a typed state pointer as one entry in a surface catalog. + /// + public static CultMeshSurfaceDiagnostic DescribeSurface( + CultMeshStatePointer pointer) + { + if (pointer == null) throw new ArgumentNullException(nameof(pointer)); + return new CultMeshSurfaceDiagnostic( + CultMeshSurfaceKind.StatePointer, + pointer.PointerId, + pointer.RouteHint, + pointer.Sources); + } + + /// + /// Describes a mutable typed state pointer as one entry in a surface catalog. + /// + public static CultMeshSurfaceDiagnostic DescribeSurface( + CultMeshMutableStatePointer pointer) + { + if (pointer == null) throw new ArgumentNullException(nameof(pointer)); + return new CultMeshSurfaceDiagnostic( + CultMeshSurfaceKind.StatePointer, + pointer.PointerId, + pointer.RouteHint, + pointer.Sources); + } + + /// + /// Describes a native slice view as one entry in a surface catalog. + /// + public static CultMeshSurfaceDiagnostic DescribeSurface( + CultMeshNativeSliceViewDescriptor view) + { + if (view == null) throw new ArgumentNullException(nameof(view)); + return new CultMeshSurfaceDiagnostic(CultMeshSurfaceKind.NativeSliceView, view.ViewId, view.Route); + } + + /// + /// Creates a typed surface catalog diagnostic for tooling, UI, and generated bindings. + /// + public static CultMeshSurfaceCatalogDiagnostic DescribeSurfaceCatalog( + string catalogId, + IEnumerable surfaces) + { + return new CultMeshSurfaceCatalogDiagnostic(catalogId, surfaces); + } + + /// + /// Creates a kind-indexed surface catalog diagnostic for generated bindings, UI, and tools. + /// + public static CultMeshSurfaceCatalogIndexDiagnostic DescribeSurfaceCatalogIndex( + CultMeshSurfaceCatalogDiagnostic catalog) + { + if (catalog == null) throw new ArgumentNullException(nameof(catalog)); + return catalog.IndexByKind(); + } + + /// + /// Creates a timer-backed query watcher for runtimes that do not yet have native reactive transport. + /// + public static Func> PollingQueryWatcher( + Func> sample, + CultMeshPollingWatchOptions? options = null) + { + if (sample == null) throw new ArgumentNullException(nameof(sample)); + var resolvedOptions = options ?? new CultMeshPollingWatchOptions(); + return (parameters, context) => R3.Observable.Create(observer => + { + var watcher = new CultMeshPollingWatcher( + sample, + parameters, + context, + resolvedOptions); + var subscription = watcher.Observable.Subscribe(observer); + return new CultMeshCompositeDisposable(subscription, watcher); + }); + } + /// /// Parses a rudp://host:port endpoint into its host/port parts. /// diff --git a/src/GameCult.Mesh/CultMeshPrimitives.cs b/src/GameCult.Mesh/CultMeshPrimitives.cs new file mode 100644 index 00000000..49906d1c --- /dev/null +++ b/src/GameCult.Mesh/CultMeshPrimitives.cs @@ -0,0 +1,2775 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using GameCult.Caching; +using GameCult.Caching.MessagePack; +using MessagePack; +using R3; + +namespace GameCult.Mesh +{ + /// + /// Describes the route CultMesh used or should prefer for a typed operation, query, pointer, or native view. + /// + public enum CultMeshLocalityKind + { + /// Let CultMesh choose the fastest valid route. + Automatic, + /// The target is in the same process. + InProcess, + /// The target is available through a shared CultCache slab or native shared memory view. + SharedMemory, + /// The target is reachable through local inter-process transport. + Ipc, + /// The target is reachable through a network Verse route. + Network, + /// The target is hosted in or exposed to a WebAssembly runtime. + Wasm + } + + /// + /// Names a CultMesh locality decision without exposing transport plumbing to application code. + /// + public sealed class CultMeshRouteHint + { + /// Creates a route hint. + public CultMeshRouteHint(CultMeshLocalityKind kind = CultMeshLocalityKind.Automatic, string? description = null) + { + Kind = kind; + Description = description; + } + + /// Gets the requested or observed locality kind. + public CultMeshLocalityKind Kind { get; } + + /// Gets optional human-facing route diagnostics. + public string? Description { get; } + + /// Gets the default automatic route hint. + public static CultMeshRouteHint Automatic { get; } = new(); + } + + /// + /// Flat, transport-friendly fields for a CultMesh route hint. + /// + public sealed class CultMeshRouteRecord + { + /// Creates flat route fields. + public CultMeshRouteRecord(string? kind = null, string? description = null) + { + Kind = kind ?? ""; + Description = description ?? ""; + } + + /// Gets the serialized route locality kind. + public string Kind { get; } + + /// Gets the serialized route description. + public string Description { get; } + + /// Creates flat route fields from a route hint. + public static CultMeshRouteRecord FromRoute(CultMeshRouteHint? routeHint) + { + var route = routeHint ?? CultMeshRouteHint.Automatic; + return new CultMeshRouteRecord(route.Kind.ToString(), route.Description); + } + + /// Rehydrates flat route fields into a route hint. + public CultMeshRouteHint ToRoute(CultMeshRouteHint? fallback = null) + { + var fallbackRoute = fallback ?? CultMeshRouteHint.Automatic; + return new CultMeshRouteHint( + ParseKind(Kind, fallbackRoute.Kind), + string.IsNullOrWhiteSpace(Description) ? fallbackRoute.Description : Description); + } + + private static CultMeshLocalityKind ParseKind(string? value, CultMeshLocalityKind fallback) + { + if (string.IsNullOrWhiteSpace(value)) + return fallback; + + if (Enum.TryParse(value, ignoreCase: true, out var parsed)) + return parsed; + + var normalized = value.Replace("-", "", StringComparison.Ordinal); + foreach (CultMeshLocalityKind candidate in Enum.GetValues(typeof(CultMeshLocalityKind))) + { + if (string.Equals(candidate.ToString(), normalized, StringComparison.OrdinalIgnoreCase)) + return candidate; + } + + return fallback; + } + } + + /// + /// Shared Verse invocation context used by generated domain sugar. + /// + public sealed class CultMeshVerseContext + { + /// Creates a Verse context. + public CultMeshVerseContext( + string verseId, + string runtimeId, + CultMeshRouteHint? routeHint = null, + IEnumerable? claims = null) + { + VerseId = RequireNonEmpty(verseId, nameof(verseId)); + RuntimeId = RequireNonEmpty(runtimeId, nameof(runtimeId)); + RouteHint = routeHint ?? CultMeshRouteHint.Automatic; + Claims = claims?.ToArray() ?? Array.Empty(); + } + + /// Gets the semantic Verse id. + public string VerseId { get; } + + /// Gets the runtime using the Verse. + public string RuntimeId { get; } + + /// Gets the preferred route for generated operations and queries. + public CultMeshRouteHint RouteHint { get; } + + /// Gets authority claims carried by generated operations. + public IReadOnlyList Claims { get; } + + /// Returns a copy with one additional authority claim. + public CultMeshVerseContext WithClaim(CultMeshAuthorityClaim claim) + { + if (claim == null) throw new ArgumentNullException(nameof(claim)); + return new CultMeshVerseContext(VerseId, RuntimeId, RouteHint, Claims.Concat(new[] { claim })); + } + + /// Returns a copy with the supplied route hint. + public CultMeshVerseContext WithRoute(CultMeshRouteHint routeHint) + { + if (routeHint == null) throw new ArgumentNullException(nameof(routeHint)); + return new CultMeshVerseContext(VerseId, RuntimeId, routeHint, Claims); + } + + /// Creates a typed operation context from the Verse context. + public CultMeshOperationContext OperationContext(string? idempotencyKey = null) + { + return new CultMeshOperationContext(RuntimeId, Claims, RouteHint, idempotencyKey); + } + + /// Creates a typed query context from the Verse context. + public CultMeshQueryContext QueryContext() + { + return new CultMeshQueryContext(RuntimeId, RouteHint); + } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Managed Verse handle that generated domain packages can bind to. + /// + public sealed class CultMeshVerse + { + /// Creates a managed Verse handle. + public CultMeshVerse(CultMeshVerseContext context) + { + Context = context ?? throw new ArgumentNullException(nameof(context)); + } + + /// Gets the Verse context shared by generated domain sugar. + public CultMeshVerseContext Context { get; } + + /// Gets the semantic Verse id. + public string VerseId => Context.VerseId; + + /// Gets the runtime using the Verse. + public string RuntimeId => Context.RuntimeId; + + /// Binds this Verse to a generated domain facade. + public TSchema Use(Func schemaFactory) + { + if (schemaFactory == null) throw new ArgumentNullException(nameof(schemaFactory)); + return schemaFactory(Context); + } + + /// Returns a Verse handle with one additional authority claim. + public CultMeshVerse WithClaim(CultMeshAuthorityClaim claim) + { + return new CultMeshVerse(Context.WithClaim(claim)); + } + + /// Returns a Verse handle with the supplied route hint. + public CultMeshVerse WithRoute(CultMeshRouteHint routeHint) + { + return new CultMeshVerse(Context.WithRoute(routeHint)); + } + + /// Creates a typed operation context from this Verse. + public CultMeshOperationContext OperationContext(string? idempotencyKey = null) + { + return Context.OperationContext(idempotencyKey); + } + + /// Creates a typed query context from this Verse. + public CultMeshQueryContext QueryContext() + { + return Context.QueryContext(); + } + + /// Binds an operation handle to this Verse. + public CultMeshBoundOperationHandle BindOperation( + CultMeshOperationHandle operation) + { + return new CultMeshBoundOperationHandle(Context, operation); + } + + /// Binds a query surface to this Verse. + public CultMeshBoundQuerySurface BindQuery( + CultMeshQuerySurface query) + { + return new CultMeshBoundQuerySurface(Context, query); + } + + /// Binds a live feed to this Verse. + public CultMeshBoundLiveFeed BindLiveFeed( + CultMeshLiveFeed feed) + { + return new CultMeshBoundLiveFeed(Context, feed); + } + + /// Binds a state pointer to this Verse. + public CultMeshBoundStatePointer BindStatePointer( + CultMeshStatePointer pointer) + { + return new CultMeshBoundStatePointer(Context, pointer); + } + + /// Binds a mutable state pointer to this Verse. + public CultMeshBoundMutableStatePointer BindMutableStatePointer( + CultMeshMutableStatePointer pointer) + { + return new CultMeshBoundMutableStatePointer(Context, pointer); + } + } + + /// + /// Describes a runtime authority claim attached to a typed operation. + /// + public sealed class CultMeshAuthorityClaim + { + /// Creates an authority claim. + public CultMeshAuthorityClaim(string role, string? shardId = null, string? leaseId = null) + { + Role = RequireNonEmpty(role, nameof(role)); + ShardId = shardId; + LeaseId = leaseId; + } + + /// Gets the role or claim kind being asserted. + public string Role { get; } + + /// Gets the optional shard covered by the claim. + public string? ShardId { get; } + + /// Gets the optional lease id backing the claim. + public string? LeaseId { get; } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Context supplied when invoking a typed CultMesh operation. + /// + public sealed class CultMeshOperationContext + { + /// Creates an operation context. + public CultMeshOperationContext( + string runtimeId, + IEnumerable? claims = null, + CultMeshRouteHint? routeHint = null, + string? idempotencyKey = null) + { + RuntimeId = RequireNonEmpty(runtimeId, nameof(runtimeId)); + Claims = claims?.ToArray() ?? Array.Empty(); + RouteHint = routeHint ?? CultMeshRouteHint.Automatic; + IdempotencyKey = idempotencyKey; + } + + /// Gets the runtime invoking the operation. + public string RuntimeId { get; } + + /// Gets authority claims attached to the operation. + public IReadOnlyList Claims { get; } + + /// Gets the preferred route for the operation. + public CultMeshRouteHint RouteHint { get; } + + /// Gets an optional operation idempotency key. + public string? IdempotencyKey { get; } + + /// Creates an operation context for a runtime. + public static CultMeshOperationContext ForRuntime(string runtimeId) + { + return new CultMeshOperationContext(runtimeId); + } + + /// Returns a copy with one additional authority claim. + public CultMeshOperationContext WithClaim(CultMeshAuthorityClaim claim) + { + if (claim == null) throw new ArgumentNullException(nameof(claim)); + return new CultMeshOperationContext(RuntimeId, Claims.Concat(new[] { claim }), RouteHint, IdempotencyKey); + } + + /// Returns a copy with the supplied route hint. + public CultMeshOperationContext WithRoute(CultMeshRouteHint routeHint) + { + if (routeHint == null) throw new ArgumentNullException(nameof(routeHint)); + return new CultMeshOperationContext(RuntimeId, Claims, routeHint, IdempotencyKey); + } + + /// Returns a copy with the supplied idempotency key. + public CultMeshOperationContext WithIdempotencyKey(string idempotencyKey) + { + return new CultMeshOperationContext( + RuntimeId, + Claims, + RouteHint, + RequireNonEmpty(idempotencyKey, nameof(idempotencyKey))); + } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Receipt returned by a typed CultMesh operation. + /// + public sealed class CultMeshOperationReceipt + { + /// Creates an operation receipt. + public CultMeshOperationReceipt( + string operationId, + bool accepted, + CultMeshRouteHint? route = null, + string? diagnostic = null) + { + OperationId = RequireNonEmpty(operationId, nameof(operationId)); + Accepted = accepted; + Route = route ?? CultMeshRouteHint.Automatic; + Diagnostic = diagnostic; + } + + /// Gets the semantic operation id. + public string OperationId { get; } + + /// Gets whether the operation was accepted by the target authority boundary. + public bool Accepted { get; } + + /// Gets the route used for the operation. + public CultMeshRouteHint Route { get; } + + /// Gets optional operation diagnostics. + public string? Diagnostic { get; } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Method-shaped handle for a typed CultMesh operation. + /// + public sealed class CultMeshOperationHandle + { + private readonly Func> _invoke; + + /// Creates an operation handle. + public CultMeshOperationHandle( + string operationId, + Func> invoke) + { + OperationId = RequireNonEmpty(operationId, nameof(operationId)); + _invoke = invoke ?? throw new ArgumentNullException(nameof(invoke)); + } + + /// Gets the semantic operation id. + public string OperationId { get; } + + /// Invokes the typed operation. + public Task InvokeAsync(TRequest request, CultMeshOperationContext context) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + return _invoke(request, context); + } + + /// Invokes the typed operation for one runtime using default context. + public Task InvokeAsync(TRequest request, string runtimeId) + { + return InvokeAsync(request, CultMeshOperationContext.ForRuntime(runtimeId)); + } + + /// Binds this operation to a Verse context. + public CultMeshBoundOperationHandle Bind(CultMeshVerseContext context) + { + return new CultMeshBoundOperationHandle(context, this); + } + + /// Binds this operation to a Verse. + public CultMeshBoundOperationHandle Bind(CultMeshVerse verse) + { + if (verse == null) throw new ArgumentNullException(nameof(verse)); + return Bind(verse.Context); + } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Operation handle pre-bound to a Verse so generated domain sugar can invoke without context plumbing. + /// + public sealed class CultMeshBoundOperationHandle + { + /// Creates a Verse-bound operation handle. + public CultMeshBoundOperationHandle( + CultMeshVerseContext context, + CultMeshOperationHandle operation) + { + Context = context ?? throw new ArgumentNullException(nameof(context)); + Operation = operation ?? throw new ArgumentNullException(nameof(operation)); + } + + /// Gets the bound Verse context. + public CultMeshVerseContext Context { get; } + + /// Gets the underlying operation handle. + public CultMeshOperationHandle Operation { get; } + + /// Gets the semantic operation id. + public string OperationId => Operation.OperationId; + + /// Invokes the operation through the bound Verse. + public Task InvokeAsync(TRequest request, string? idempotencyKey = null) + { + return Operation.InvokeAsync(request, Context.OperationContext(idempotencyKey)); + } + } + + /// + /// Context supplied when executing a typed CultMesh query surface. + /// + public sealed class CultMeshQueryContext + { + /// Creates a query context. + public CultMeshQueryContext(string runtimeId, CultMeshRouteHint? routeHint = null) + { + RuntimeId = RequireNonEmpty(runtimeId, nameof(runtimeId)); + RouteHint = routeHint ?? CultMeshRouteHint.Automatic; + } + + /// Gets the runtime executing the query. + public string RuntimeId { get; } + + /// Gets the preferred route for the query. + public CultMeshRouteHint RouteHint { get; } + + /// Creates a query context for a runtime. + public static CultMeshQueryContext ForRuntime(string runtimeId) + { + return new CultMeshQueryContext(runtimeId); + } + + /// Returns a copy with the supplied route hint. + public CultMeshQueryContext WithRoute(CultMeshRouteHint routeHint) + { + if (routeHint == null) throw new ArgumentNullException(nameof(routeHint)); + return new CultMeshQueryContext(RuntimeId, routeHint); + } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Fluent builder for typed operation context. This is sugar over , + /// intended for call sites that should read like native Verse operations instead of transport setup. + /// + public sealed class CultMeshOperationContextBuilder + { + private readonly string _runtimeId; + private readonly List _claims = new(); + private CultMeshRouteHint _routeHint = CultMeshRouteHint.Automatic; + private string? _idempotencyKey; + + /// Creates a builder for one runtime. + public CultMeshOperationContextBuilder(string runtimeId) + { + _runtimeId = RequireNonEmpty(runtimeId, nameof(runtimeId)); + } + + /// Adds an authority claim. + public CultMeshOperationContextBuilder Claim(string role, string? shardId = null, string? leaseId = null) + { + _claims.Add(new CultMeshAuthorityClaim(role, shardId, leaseId)); + return this; + } + + /// Adds an authority claim. + public CultMeshOperationContextBuilder Claim(CultMeshAuthorityClaim claim) + { + _claims.Add(claim ?? throw new ArgumentNullException(nameof(claim))); + return this; + } + + /// Adds many authority claims. + public CultMeshOperationContextBuilder Claims(IEnumerable claims) + { + if (claims == null) throw new ArgumentNullException(nameof(claims)); + _claims.AddRange(claims); + return this; + } + + /// Sets the preferred route hint. + public CultMeshOperationContextBuilder Route(CultMeshLocalityKind kind, string? description = null) + { + _routeHint = new CultMeshRouteHint(kind, description); + return this; + } + + /// Sets the operation idempotency key. + public CultMeshOperationContextBuilder Idempotency(string idempotencyKey) + { + _idempotencyKey = RequireNonEmpty(idempotencyKey, nameof(idempotencyKey)); + return this; + } + + /// Creates the immutable operation context. + public CultMeshOperationContext Build() + { + return new CultMeshOperationContext(_runtimeId, _claims, _routeHint, _idempotencyKey); + } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Fluent builder for typed query context. + /// + public sealed class CultMeshQueryContextBuilder + { + private readonly string _runtimeId; + private CultMeshRouteHint _routeHint = CultMeshRouteHint.Automatic; + + /// Creates a builder for one runtime. + public CultMeshQueryContextBuilder(string runtimeId) + { + _runtimeId = RequireNonEmpty(runtimeId, nameof(runtimeId)); + } + + /// Sets the preferred route hint. + public CultMeshQueryContextBuilder Route(CultMeshLocalityKind kind, string? description = null) + { + _routeHint = new CultMeshRouteHint(kind, description); + return this; + } + + /// Creates the immutable query context. + public CultMeshQueryContext Build() + { + return new CultMeshQueryContext(_runtimeId, _routeHint); + } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Typed derived-state query surface. + /// + public sealed class CultMeshQuerySurface + { + private readonly Func> _execute; + private readonly Func>? _watch; + + /// Creates a query surface. + public CultMeshQuerySurface( + string queryId, + Func> execute, + Func>? watch = null, + IEnumerable? sources = null, + CultMeshRouteHint? routeHint = null) + { + QueryId = RequireNonEmpty(queryId, nameof(queryId)); + _execute = execute ?? throw new ArgumentNullException(nameof(execute)); + _watch = watch; + Sources = sources?.ToArray() ?? Array.Empty(); + RouteHint = routeHint ?? CultMeshRouteHint.Automatic; + } + + /// Gets the semantic query id. + public string QueryId { get; } + + /// Gets the typed state sources this query depends on, when known. + public IReadOnlyList Sources { get; } + + /// Gets the preferred or observed route for the query. + public CultMeshRouteHint RouteHint { get; } + + /// Executes the query once. + public Task ExecuteAsync(TParameters parameters, CultMeshQueryContext context) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + return _execute(parameters, context); + } + + /// Executes the query once for one runtime using default context. + public Task ExecuteAsync(TParameters parameters, string runtimeId) + { + return ExecuteAsync(parameters, CultMeshQueryContext.ForRuntime(runtimeId)); + } + + /// Watches query results when the surface supports reactive execution. + public Observable Watch(TParameters parameters, CultMeshQueryContext context) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (_watch == null) + { + throw new NotSupportedException($"Query surface '{QueryId}' does not support reactive watches."); + } + + return _watch(parameters, context); + } + + /// Binds this query surface to a Verse context. + public CultMeshBoundQuerySurface Bind(CultMeshVerseContext context) + { + return new CultMeshBoundQuerySurface(context, this); + } + + /// Binds this query surface to a Verse. + public CultMeshBoundQuerySurface Bind(CultMeshVerse verse) + { + if (verse == null) throw new ArgumentNullException(nameof(verse)); + return Bind(verse.Context); + } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Query surface pre-bound to a Verse so generated domain sugar can execute without context plumbing. + /// + public sealed class CultMeshBoundQuerySurface + { + /// Creates a Verse-bound query surface. + public CultMeshBoundQuerySurface( + CultMeshVerseContext context, + CultMeshQuerySurface query) + { + Context = context ?? throw new ArgumentNullException(nameof(context)); + Query = query ?? throw new ArgumentNullException(nameof(query)); + } + + /// Gets the bound Verse context. + public CultMeshVerseContext Context { get; } + + /// Gets the underlying query surface. + public CultMeshQuerySurface Query { get; } + + /// Gets the semantic query id. + public string QueryId => Query.QueryId; + + /// Gets the typed state sources this query depends on, when known. + public IReadOnlyList Sources => Query.Sources; + + /// Gets the preferred or observed route for the query. + public CultMeshRouteHint RouteHint => Query.RouteHint; + + /// Executes the query through the bound Verse. + public Task ExecuteAsync(TParameters parameters) + { + return Query.ExecuteAsync(parameters, Context.QueryContext()); + } + + /// Watches query results through the bound Verse. + public Observable Watch(TParameters parameters) + { + return Query.Watch(parameters, Context.QueryContext()); + } + } + + /// + /// Typed live view surface for composed client snapshots. + /// + public sealed class CultMeshLiveFeed + { + private readonly Func> _snapshot; + private readonly Func>? _watch; + + /// Creates a live feed surface. + public CultMeshLiveFeed( + string feedId, + Func> snapshot, + Func>? watch = null, + IEnumerable? sources = null, + CultMeshRouteHint? routeHint = null) + { + FeedId = RequireNonEmpty(feedId, nameof(feedId)); + _snapshot = snapshot ?? throw new ArgumentNullException(nameof(snapshot)); + _watch = watch; + Sources = sources?.ToArray() ?? Array.Empty(); + RouteHint = routeHint ?? CultMeshRouteHint.Automatic; + } + + /// Gets the semantic feed id. + public string FeedId { get; } + + /// Gets the typed state sources this live feed depends on, when known. + public IReadOnlyList Sources { get; } + + /// Gets the preferred or observed route for the feed. + public CultMeshRouteHint RouteHint { get; } + + /// Reads one coherent feed snapshot. + public Task SnapshotAsync(TParameters parameters, CultMeshQueryContext context) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + return _snapshot(parameters, ResolveContext(context)); + } + + /// Reads one coherent feed snapshot for one runtime using default context. + public Task SnapshotAsync(TParameters parameters, string runtimeId) + { + return SnapshotAsync(parameters, CultMeshQueryContext.ForRuntime(runtimeId)); + } + + /// Watches coherent feed snapshots when the feed supports reactive execution. + public Observable Watch(TParameters parameters, CultMeshQueryContext context) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (_watch == null) + { + throw new NotSupportedException($"Live feed '{FeedId}' does not support reactive watches."); + } + + return _watch(parameters, ResolveContext(context)); + } + + /// Binds this live feed to a Verse context. + public CultMeshBoundLiveFeed Bind(CultMeshVerseContext context) + { + return new CultMeshBoundLiveFeed(context, this); + } + + /// Binds this live feed to a Verse. + public CultMeshBoundLiveFeed Bind(CultMeshVerse verse) + { + if (verse == null) throw new ArgumentNullException(nameof(verse)); + return Bind(verse.Context); + } + + private CultMeshQueryContext ResolveContext(CultMeshQueryContext context) + { + return context.RouteHint.Kind == CultMeshLocalityKind.Automatic && + RouteHint.Kind != CultMeshLocalityKind.Automatic + ? context.WithRoute(RouteHint) + : context; + } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Live feed pre-bound to a Verse so generated domain sugar can snapshot or watch without context plumbing. + /// + public sealed class CultMeshBoundLiveFeed + { + /// Creates a Verse-bound live feed. + public CultMeshBoundLiveFeed( + CultMeshVerseContext context, + CultMeshLiveFeed feed) + { + Context = context ?? throw new ArgumentNullException(nameof(context)); + Feed = feed ?? throw new ArgumentNullException(nameof(feed)); + } + + /// Gets the bound Verse context. + public CultMeshVerseContext Context { get; } + + /// Gets the underlying live feed. + public CultMeshLiveFeed Feed { get; } + + /// Gets the semantic feed id. + public string FeedId => Feed.FeedId; + + /// Gets the typed state sources this live feed depends on, when known. + public IReadOnlyList Sources => Feed.Sources; + + /// Gets the preferred or observed route for the feed. + public CultMeshRouteHint RouteHint => Feed.RouteHint; + + /// Reads one coherent feed snapshot through the bound Verse. + public Task SnapshotAsync(TParameters parameters) + { + return Feed.SnapshotAsync(parameters, Context.QueryContext()); + } + + /// Watches coherent feed snapshots through the bound Verse. + public Observable Watch(TParameters parameters) + { + return Feed.Watch(parameters, Context.QueryContext()); + } + } + + /// + /// Empty parameter object for document handles that expose one typed document as a live feed. + /// + public readonly struct CultMeshDocumentQueryParameters + { + /// Gets the singleton empty document query. + public static CultMeshDocumentQueryParameters Empty { get; } = new(); + } + + /// + /// Inspectable typed document handle exposed by CultMesh. + /// + public interface ICultMeshDocumentHandle + { + /// Gets the CLR document type presented by this handle. + Type DocumentType { get; } + + /// Gets the semantic document id. + string DocumentId { get; } + + /// Gets the stable CultCache schema name. + string SchemaName { get; } + + /// Gets the stable CultCache schema version. + string SchemaVersion { get; } + + /// Gets the preferred or observed route for document access. + CultMeshRouteHint RouteHint { get; } + + /// Gets typed state sources this document handle depends on, when known. + IReadOnlyList Sources { get; } + + /// Creates a same-schema alias presentation for another CLR document type. + CultMeshDocumentHandle AsSchemaAlias() where TAlias : class; + } + + /// + /// Typed reactive document handle with schema-aware alias conversion. + /// + public sealed class CultMeshDocumentHandle : ICultMeshDocumentHandle + where TDocument : class + { + private static readonly CultDocumentDescriptor Descriptor = + CultDocumentRegistry.Shared.GetRequired(); + + private readonly CultMeshBoundLiveFeed _feed; + + /// Creates a document handle from a Verse-bound live feed. + public CultMeshDocumentHandle(CultMeshBoundLiveFeed feed) + { + _feed = feed ?? throw new ArgumentNullException(nameof(feed)); + } + + /// Gets the semantic document id. + public string DocumentId => _feed.FeedId; + + /// Gets the CLR document type presented by this handle. + public Type DocumentType => typeof(TDocument); + + /// Gets the stable CultCache schema name. + public string SchemaName => Descriptor.SchemaName; + + /// Gets the stable CultCache schema version. + public string SchemaVersion => Descriptor.SchemaVersion; + + /// Gets the content-derived schema identifier. + public string SchemaId => Descriptor.SchemaId; + + /// Gets the preferred or observed route for document access. + public CultMeshRouteHint RouteHint => _feed.RouteHint; + + /// Gets typed state sources this document handle depends on, when known. + public IReadOnlyList Sources => _feed.Sources; + + /// Gets the bound Verse context. + public CultMeshVerseContext Context => _feed.Context; + + /// Gets the underlying live feed used by this document handle. + public CultMeshLiveFeed Feed => _feed.Feed; + + /// Reads one coherent document snapshot. + public Task LatestAsync() + { + return _feed.SnapshotAsync(CultMeshDocumentQueryParameters.Empty); + } + + /// Watches coherent document snapshots. + public Observable Watch() + { + return _feed.Watch(CultMeshDocumentQueryParameters.Empty); + } + + /// Subscribes to coherent document snapshots. + public IDisposable Watch(Action onNext) + { + if (onNext == null) throw new ArgumentNullException(nameof(onNext)); + return Watch().Subscribe(onNext); + } + + /// Creates a same-schema alias presentation for another CLR document type. + public CultMeshDocumentHandle AsSchemaAlias() where TAlias : class + { + var aliasDescriptor = CultDocumentRegistry.Shared.GetRequired(); + 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 handle '{DocumentId}' exposes " + + $"schema '{SchemaName}' version '{SchemaVersion}'."); + } + + var aliasFeed = new CultMeshLiveFeed( + DocumentId, + async (parameters, context) => ConvertDocument( + await Feed.SnapshotAsync(parameters, context).ConfigureAwait(false)), + (parameters, context) => Feed + .Watch(parameters, context) + .Select(ConvertDocument), + Sources, + RouteHint); + + return new CultMeshDocumentHandle(aliasFeed.Bind(Context)); + } + + private static TAlias ConvertDocument(TDocument document) where TAlias : class + { + if (document == null) throw new ArgumentNullException(nameof(document)); + var payload = CultDocumentMessagePackSerialization.SerializeUntyped(document, typeof(TDocument)); + return (TAlias)CultDocumentMessagePackSerialization.DeserializeUntyped(typeof(TAlias), payload); + } + } + + /// + /// Inspectable metadata for a typed live feed surface. + /// + public sealed class CultMeshLiveFeedDiagnostic + { + /// Creates live feed diagnostics. + public CultMeshLiveFeedDiagnostic( + string feedId, + CultMeshRouteHint? routeHint = null, + IEnumerable? sources = null) + { + FeedId = RequireNonEmpty(feedId, nameof(feedId)); + RouteHint = routeHint ?? CultMeshRouteHint.Automatic; + Sources = sources?.ToArray() ?? Array.Empty(); + } + + /// Gets the semantic feed id. + public string FeedId { get; } + + /// Gets the preferred or observed route for the feed. + public CultMeshRouteHint RouteHint { get; } + + /// Gets the typed state sources this live feed depends on, when known. + public IReadOnlyList Sources { get; } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Inspectable metadata for a typed query surface. + /// + public sealed class CultMeshQuerySurfaceDiagnostic + { + /// Creates query surface diagnostics. + public CultMeshQuerySurfaceDiagnostic( + string queryId, + CultMeshRouteHint? routeHint = null, + IEnumerable? sources = null) + { + QueryId = RequireNonEmpty(queryId, nameof(queryId)); + RouteHint = routeHint ?? CultMeshRouteHint.Automatic; + Sources = sources?.ToArray() ?? Array.Empty(); + } + + /// Gets the semantic query id. + public string QueryId { get; } + + /// Gets the preferred or observed route for the query. + public CultMeshRouteHint RouteHint { get; } + + /// Gets the typed state sources this query depends on, when known. + public IReadOnlyList Sources { get; } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Inspectable metadata for a typed operation handle. + /// + public sealed class CultMeshOperationHandleDiagnostic + { + /// Creates operation handle diagnostics. + public CultMeshOperationHandleDiagnostic(string operationId) + { + OperationId = RequireNonEmpty(operationId, nameof(operationId)); + } + + /// Gets the semantic operation id. + public string OperationId { get; } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Inspectable metadata for a typed state pointer. + /// + public sealed class CultMeshStatePointerDiagnostic + { + /// Creates state pointer diagnostics. + public CultMeshStatePointerDiagnostic( + string pointerId, + CultMeshRouteHint? routeHint = null, + IEnumerable? sources = null) + { + PointerId = RequireNonEmpty(pointerId, nameof(pointerId)); + RouteHint = routeHint ?? CultMeshRouteHint.Automatic; + Sources = sources?.ToArray() ?? Array.Empty(); + } + + /// Gets the semantic pointer id. + public string PointerId { get; } + + /// Gets the preferred or observed route for pointer resolution. + public CultMeshRouteHint RouteHint { get; } + + /// Gets typed state sources this pointer depends on, when known. + public IReadOnlyList Sources { get; } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Inspectable metadata for a typed projection recipe. + /// + public sealed class CultMeshProjectionRecipeDiagnostic + { + /// Creates projection recipe diagnostics. + public CultMeshProjectionRecipeDiagnostic( + string projectionId, + CultMeshRouteHint? routeHint = null, + IEnumerable? sources = null) + { + ProjectionId = RequireNonEmpty(projectionId, nameof(projectionId)); + RouteHint = routeHint ?? CultMeshRouteHint.Automatic; + Sources = sources?.ToArray() ?? Array.Empty(); + } + + /// Gets the semantic projection id. + public string ProjectionId { get; } + + /// Gets the preferred or observed route for the projection. + public CultMeshRouteHint RouteHint { get; } + + /// Gets the typed state sources this projection depends on, when known. + public IReadOnlyList Sources { get; } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Names the kind of typed surface described in a CultMesh surface catalog. + /// + public enum CultMeshSurfaceKind + { + /// A typed derived-state query surface. + Query, + + /// A reusable typed projection recipe. + ProjectionRecipe, + + /// A coherent live view feed. + LiveFeed, + + /// A typed operation handle. + Operation, + + /// A typed pointer to Verse state. + StatePointer, + + /// A native slice or slab view. + NativeSliceView + } + + /// + /// One inspectable typed surface advertised by a runtime. + /// + public sealed class CultMeshSurfaceDiagnostic + { + /// Creates a surface diagnostic. + public CultMeshSurfaceDiagnostic( + CultMeshSurfaceKind kind, + string surfaceId, + CultMeshRouteHint? routeHint = null, + IEnumerable? sources = null) + { + Kind = kind; + SurfaceId = RequireNonEmpty(surfaceId, nameof(surfaceId)); + RouteHint = routeHint ?? CultMeshRouteHint.Automatic; + Sources = sources?.ToArray() ?? Array.Empty(); + } + + /// Gets the kind of surface. + public CultMeshSurfaceKind Kind { get; } + + /// Gets the semantic surface id. + public string SurfaceId { get; } + + /// Gets the preferred or observed route for the surface. + public CultMeshRouteHint RouteHint { get; } + + /// Gets typed state sources this surface depends on, when known. + public IReadOnlyList Sources { get; } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Inspectable catalog of typed surfaces exposed by a runtime or generated binding. + /// + public sealed class CultMeshSurfaceCatalogDiagnostic + { + /// Creates a surface catalog diagnostic. + public CultMeshSurfaceCatalogDiagnostic( + string catalogId, + IEnumerable surfaces) + { + CatalogId = RequireNonEmpty(catalogId, nameof(catalogId)); + Surfaces = (surfaces ?? throw new ArgumentNullException(nameof(surfaces))).ToArray(); + } + + /// Gets the semantic catalog id. + public string CatalogId { get; } + + /// Gets the advertised typed surfaces. + public IReadOnlyList Surfaces { get; } + + /// Finds a surface by semantic id. + public CultMeshSurfaceDiagnostic? Find(string surfaceId) + { + if (string.IsNullOrWhiteSpace(surfaceId)) return null; + return Surfaces.FirstOrDefault(surface => string.Equals(surface.SurfaceId, surfaceId, StringComparison.Ordinal)); + } + + /// Finds surfaces by kind in catalog order. + public IReadOnlyList FindByKind(CultMeshSurfaceKind kind) + { + return Surfaces.Where(surface => surface.Kind == kind).ToArray(); + } + + /// Groups surfaces by kind for generated bindings and tooling. + public CultMeshSurfaceCatalogIndexDiagnostic IndexByKind() + { + return new CultMeshSurfaceCatalogIndexDiagnostic(this); + } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Kind-indexed view of a typed surface catalog for generated bindings, UI runtimes, and tools. + /// + public sealed class CultMeshSurfaceCatalogIndexDiagnostic + { + /// Creates a kind-indexed catalog diagnostic. + public CultMeshSurfaceCatalogIndexDiagnostic(CultMeshSurfaceCatalogDiagnostic catalog) + { + if (catalog == null) throw new ArgumentNullException(nameof(catalog)); + CatalogId = catalog.CatalogId; + Queries = catalog.FindByKind(CultMeshSurfaceKind.Query); + ProjectionRecipes = catalog.FindByKind(CultMeshSurfaceKind.ProjectionRecipe); + LiveFeeds = catalog.FindByKind(CultMeshSurfaceKind.LiveFeed); + Operations = catalog.FindByKind(CultMeshSurfaceKind.Operation); + StatePointers = catalog.FindByKind(CultMeshSurfaceKind.StatePointer); + NativeSliceViews = catalog.FindByKind(CultMeshSurfaceKind.NativeSliceView); + } + + /// Gets the semantic catalog id. + public string CatalogId { get; } + + /// Gets typed derived-state query surfaces. + public IReadOnlyList Queries { get; } + + /// Gets reusable typed projection recipes. + public IReadOnlyList ProjectionRecipes { get; } + + /// Gets coherent live view feeds. + public IReadOnlyList LiveFeeds { get; } + + /// Gets typed operation handles. + public IReadOnlyList Operations { get; } + + /// Gets typed pointers to Verse state. + public IReadOnlyList StatePointers { get; } + + /// Gets native slice or slab views. + public IReadOnlyList NativeSliceViews { get; } + } + + /// + /// Options for a polling watch fallback over a typed query or live feed snapshot. + /// + public sealed class CultMeshPollingWatchOptions + { + /// Creates polling watch options. + public CultMeshPollingWatchOptions( + TimeSpan? interval = null, + bool emitInitial = true, + IEqualityComparer? comparer = null) + { + Interval = interval ?? TimeSpan.FromMilliseconds(50); + if (Interval <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(interval), "Polling interval must be positive."); + } + + EmitInitial = emitInitial; + Comparer = comparer ?? EqualityComparer.Default; + } + + /// Gets the polling interval. + public TimeSpan Interval { get; } + + /// Gets whether the first sampled value should be emitted. + public bool EmitInitial { get; } + + /// Gets the equality comparer used to suppress unchanged samples. + public IEqualityComparer Comparer { get; } + } + + /// + /// Timer-backed fallback watcher for runtimes that do not yet have native reactive transport. + /// + internal sealed class CultMeshPollingWatcher : IDisposable + { + private readonly Func> _sample; + private readonly CultMeshPollingWatchOptions _options; + private readonly Subject _subject = new(); + private readonly Timer _timer; + private readonly object _gate = new(); + private readonly TParameters _parameters; + private readonly CultMeshQueryContext _context; + private TResult? _last; + private bool _hasLast; + private bool _running; + private bool _disposed; + + public CultMeshPollingWatcher( + Func> sample, + TParameters parameters, + CultMeshQueryContext context, + CultMeshPollingWatchOptions options) + { + _sample = sample ?? throw new ArgumentNullException(nameof(sample)); + _parameters = parameters; + _context = context ?? throw new ArgumentNullException(nameof(context)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _timer = new Timer(OnTimer, null, TimeSpan.Zero, _options.Interval); + } + + public Observable Observable => _subject; + + public void Dispose() + { + lock (_gate) + { + if (_disposed) + { + return; + } + + _disposed = true; + } + + _timer.Dispose(); + _subject.Dispose(); + } + + private void OnTimer(object? _) + { + lock (_gate) + { + if (_disposed || _running) + { + return; + } + + _running = true; + } + + _ = SampleAsync(); + } + + private async Task SampleAsync() + { + try + { + var next = await _sample(_parameters, _context).ConfigureAwait(false); + var shouldEmit = false; + lock (_gate) + { + if (_disposed) + { + return; + } + + shouldEmit = _hasLast + ? !_options.Comparer.Equals(_last!, next) + : _options.EmitInitial; + _last = next; + _hasLast = true; + } + + if (shouldEmit) + { + _subject.OnNext(next); + } + } + finally + { + lock (_gate) + { + _running = false; + } + } + } + } + + internal sealed class CultMeshCompositeDisposable : IDisposable + { + private readonly IDisposable[] _disposables; + private int _disposed; + + public CultMeshCompositeDisposable(params IDisposable[] disposables) + { + _disposables = disposables ?? throw new ArgumentNullException(nameof(disposables)); + } + + public void Dispose() + { + if (Interlocked.Exchange(ref _disposed, 1) != 0) + { + return; + } + + foreach (var disposable in _disposables) + { + disposable.Dispose(); + } + } + } + + /// + /// Typed pointer to Verse state that UI surfaces and tools can resolve and watch. + /// + public sealed class CultMeshStatePointer + { + private readonly Func> _resolve; + private readonly Func> _watch; + + /// Creates a state pointer. + public CultMeshStatePointer( + string pointerId, + Func> resolve, + Func> watch, + CultMeshRouteHint? routeHint = null, + IEnumerable? sources = null) + : this( + pointerId, + _ => (resolve ?? throw new ArgumentNullException(nameof(resolve)))(), + _ => (watch ?? throw new ArgumentNullException(nameof(watch)))(), + routeHint, + sources) + { + } + + /// Creates a state pointer that can resolve through a Verse query context. + public CultMeshStatePointer( + string pointerId, + Func> resolve, + Func> watch, + CultMeshRouteHint? routeHint = null, + IEnumerable? sources = null) + { + PointerId = RequireNonEmpty(pointerId, nameof(pointerId)); + _resolve = resolve ?? throw new ArgumentNullException(nameof(resolve)); + _watch = watch ?? throw new ArgumentNullException(nameof(watch)); + RouteHint = routeHint ?? CultMeshRouteHint.Automatic; + Sources = sources?.ToArray() ?? Array.Empty(); + } + + /// Gets the semantic pointer id. + public string PointerId { get; } + + /// Gets the preferred or observed route for pointer resolution. + public CultMeshRouteHint RouteHint { get; } + + /// Gets typed state sources this pointer depends on, when known. + public IReadOnlyList Sources { get; } + + /// Resolves the pointer once. + public Task ResolveAsync() + { + return ResolveAsync(CultMeshQueryContext.ForRuntime("local")); + } + + /// Resolves the pointer once through a query context. + public Task ResolveAsync(CultMeshQueryContext context) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + return _resolve(ResolveContext(context)); + } + + /// Resolves the pointer once for one runtime using default context. + public Task ResolveAsync(string runtimeId) + { + return ResolveAsync(CultMeshQueryContext.ForRuntime(runtimeId)); + } + + /// Watches resolved values for this pointer. + public Observable Watch() + { + return Watch(CultMeshQueryContext.ForRuntime("local")); + } + + /// Watches resolved values for this pointer through a query context. + public Observable Watch(CultMeshQueryContext context) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + return _watch(ResolveContext(context)); + } + + /// Binds this pointer to a Verse context. + public CultMeshBoundStatePointer Bind(CultMeshVerseContext context) + { + return new CultMeshBoundStatePointer(context, this); + } + + /// Binds this pointer to a Verse. + public CultMeshBoundStatePointer Bind(CultMeshVerse verse) + { + if (verse == null) throw new ArgumentNullException(nameof(verse)); + return Bind(verse.Context); + } + + private CultMeshQueryContext ResolveContext(CultMeshQueryContext context) + { + return context.RouteHint.Kind == CultMeshLocalityKind.Automatic && + RouteHint.Kind != CultMeshLocalityKind.Automatic + ? context.WithRoute(RouteHint) + : context; + } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// State pointer pre-bound to a Verse so UI/tool runtimes can resolve state without context plumbing. + /// + public sealed class CultMeshBoundStatePointer + { + /// Creates a Verse-bound state pointer. + public CultMeshBoundStatePointer( + CultMeshVerseContext context, + CultMeshStatePointer pointer) + { + Context = context ?? throw new ArgumentNullException(nameof(context)); + Pointer = pointer ?? throw new ArgumentNullException(nameof(pointer)); + } + + /// Gets the bound Verse context. + public CultMeshVerseContext Context { get; } + + /// Gets the underlying state pointer. + public CultMeshStatePointer Pointer { get; } + + /// Gets the semantic pointer id. + public string PointerId => Pointer.PointerId; + + /// Gets typed state sources this pointer depends on, when known. + public IReadOnlyList Sources => Pointer.Sources; + + /// Gets the preferred or observed route for pointer resolution. + public CultMeshRouteHint RouteHint => Pointer.RouteHint; + + /// Resolves the pointer through the bound Verse. + public Task ResolveAsync() + { + return Pointer.ResolveAsync(Context.QueryContext()); + } + + /// Watches the pointer through the bound Verse. + public Observable Watch() + { + return Pointer.Watch(Context.QueryContext()); + } + } + + /// + /// Typed pointer to Verse state that UI surfaces and tools can resolve, watch, and replace. + /// + public sealed class CultMeshMutableStatePointer + { + private readonly Func> _resolve; + private readonly Func> _watch; + private readonly Func _replace; + + /// Creates a mutable state pointer. + public CultMeshMutableStatePointer( + string pointerId, + Func> resolve, + Func> watch, + Func replace, + CultMeshRouteHint? routeHint = null, + IEnumerable? sources = null) + : this( + pointerId, + _ => (resolve ?? throw new ArgumentNullException(nameof(resolve)))(), + _ => (watch ?? throw new ArgumentNullException(nameof(watch)))(), + (_, value) => (replace ?? throw new ArgumentNullException(nameof(replace)))(value), + routeHint, + sources) + { + } + + /// Creates a mutable state pointer that can operate through a Verse query context. + public CultMeshMutableStatePointer( + string pointerId, + Func> resolve, + Func> watch, + Func replace, + CultMeshRouteHint? routeHint = null, + IEnumerable? sources = null) + { + PointerId = RequireNonEmpty(pointerId, nameof(pointerId)); + _resolve = resolve ?? throw new ArgumentNullException(nameof(resolve)); + _watch = watch ?? throw new ArgumentNullException(nameof(watch)); + _replace = replace ?? throw new ArgumentNullException(nameof(replace)); + RouteHint = routeHint ?? CultMeshRouteHint.Automatic; + Sources = sources?.ToArray() ?? Array.Empty(); + } + + /// Gets the semantic pointer id. + public string PointerId { get; } + + /// Gets the preferred or observed route for pointer access. + public CultMeshRouteHint RouteHint { get; } + + /// Gets typed state sources this pointer depends on, when known. + public IReadOnlyList Sources { get; } + + /// Resolves the pointer once. + public Task ResolveAsync() + { + return ResolveAsync(CultMeshQueryContext.ForRuntime("local")); + } + + /// Resolves the pointer once. + public Task ReadAsync() + { + return ResolveAsync(); + } + + /// Resolves the pointer once through a query context. + public Task ResolveAsync(CultMeshQueryContext context) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + return _resolve(ResolveContext(context)); + } + + /// Resolves the pointer once through a query context. + public Task ReadAsync(CultMeshQueryContext context) + { + return ResolveAsync(context); + } + + /// Resolves the pointer once for one runtime using default context. + public Task ResolveAsync(string runtimeId) + { + return ResolveAsync(CultMeshQueryContext.ForRuntime(runtimeId)); + } + + /// Resolves the pointer once for one runtime using default context. + public Task ReadAsync(string runtimeId) + { + return ResolveAsync(runtimeId); + } + + /// Replaces the pointed state value. + public Task ReplaceAsync(T value) + { + return ReplaceAsync(value, CultMeshQueryContext.ForRuntime("local")); + } + + /// Replaces the pointed state value through a query context. + public Task ReplaceAsync(T value, CultMeshQueryContext context) + { + if (value == null) throw new ArgumentNullException(nameof(value)); + if (context == null) throw new ArgumentNullException(nameof(context)); + return _replace(ResolveContext(context), value); + } + + /// Replaces the pointed state value for one runtime using default context. + public Task ReplaceAsync(T value, string runtimeId) + { + return ReplaceAsync(value, CultMeshQueryContext.ForRuntime(runtimeId)); + } + + /// Watches resolved values for this pointer. + public Observable Watch() + { + return Watch(CultMeshQueryContext.ForRuntime("local")); + } + + /// Watches resolved values for this pointer through a query context. + public Observable Watch(CultMeshQueryContext context) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + return _watch(ResolveContext(context)); + } + + /// Views this mutable pointer as a read/watch state pointer. + public CultMeshStatePointer AsStatePointer() + { + return new CultMeshStatePointer( + PointerId, + (Func>)ResolveAsync, + (Func>)Watch, + RouteHint, + Sources); + } + + /// Binds this pointer to a Verse context. + public CultMeshBoundMutableStatePointer Bind(CultMeshVerseContext context) + { + return new CultMeshBoundMutableStatePointer(context, this); + } + + /// Binds this pointer to a Verse. + public CultMeshBoundMutableStatePointer Bind(CultMeshVerse verse) + { + if (verse == null) throw new ArgumentNullException(nameof(verse)); + return Bind(verse.Context); + } + + private CultMeshQueryContext ResolveContext(CultMeshQueryContext context) + { + return context.RouteHint.Kind == CultMeshLocalityKind.Automatic && + RouteHint.Kind != CultMeshLocalityKind.Automatic + ? context.WithRoute(RouteHint) + : context; + } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Mutable state pointer pre-bound to a Verse so UI/tool runtimes can read, watch, and replace state without context plumbing. + /// + public sealed class CultMeshBoundMutableStatePointer + { + /// Creates a Verse-bound mutable state pointer. + public CultMeshBoundMutableStatePointer( + CultMeshVerseContext context, + CultMeshMutableStatePointer pointer) + { + Context = context ?? throw new ArgumentNullException(nameof(context)); + Pointer = pointer ?? throw new ArgumentNullException(nameof(pointer)); + } + + /// Gets the bound Verse context. + public CultMeshVerseContext Context { get; } + + /// Gets the underlying mutable state pointer. + public CultMeshMutableStatePointer Pointer { get; } + + /// Gets the semantic pointer id. + public string PointerId => Pointer.PointerId; + + /// Gets typed state sources this pointer depends on, when known. + public IReadOnlyList Sources => Pointer.Sources; + + /// Gets the preferred or observed route for pointer access. + public CultMeshRouteHint RouteHint => Pointer.RouteHint; + + /// Resolves the pointer through the bound Verse. + public Task ResolveAsync() + { + return Pointer.ResolveAsync(Context.QueryContext()); + } + + /// Resolves the pointer through the bound Verse. + public Task ReadAsync() + { + return ResolveAsync(); + } + + /// Replaces the pointer value through the bound Verse. + public Task ReplaceAsync(T value) + { + return Pointer.ReplaceAsync(value, Context.QueryContext()); + } + + /// Watches the pointer through the bound Verse. + public Observable Watch() + { + return Pointer.Watch(Context.QueryContext()); + } + + /// Views this mutable pointer as a read/watch state pointer bound to the same Verse. + public CultMeshBoundStatePointer AsStatePointer() + { + return new CultMeshBoundStatePointer(Context, Pointer.AsStatePointer()); + } + } + + /// + /// Describes one typed source consumed by a projection recipe. + /// + public sealed class CultMeshProjectionSource + { + /// Creates a projection source descriptor. + public CultMeshProjectionSource(string sourceId, string? schemaId = null, string? description = null) + { + SourceId = RequireNonEmpty(sourceId, nameof(sourceId)); + SchemaId = schemaId; + Description = description; + } + + /// Gets the semantic source id, such as a state pointer, document handle, or native view id. + public string SourceId { get; } + + /// Gets the optional schema id for the source. + public string? SchemaId { get; } + + /// Gets optional human-facing source diagnostics. + public string? Description { get; } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Resolves string state references embedded in portable UI/tool surfaces through a named CultMesh resolver. + /// + public sealed class CultMeshStateRefResolver + { + private readonly Func _resolve; + + /// Creates a state-reference resolver. + public CultMeshStateRefResolver( + string resolverId, + Func resolve, + IEnumerable? sources = null, + CultMeshRouteHint? routeHint = null) + { + ResolverId = RequireNonEmpty(resolverId, nameof(resolverId)); + _resolve = resolve ?? throw new ArgumentNullException(nameof(resolve)); + Sources = sources?.ToArray() ?? Array.Empty(); + RouteHint = routeHint ?? CultMeshRouteHint.Automatic; + } + + /// Gets the semantic resolver id. + public string ResolverId { get; } + + /// Gets the source state read by the resolver. + public IReadOnlyList Sources { get; } + + /// Gets the preferred route for resolving state refs. + public CultMeshRouteHint RouteHint { get; } + + /// Gets an empty resolver that always returns an empty string. + public static CultMeshStateRefResolver Empty { get; } = + new("cultmesh.state_refs.empty", (_stateRef, _context) => ""); + + /// Resolves a state ref using a local query context. + public string Resolve(string stateRef) + { + return Resolve(stateRef, CultMeshQueryContext.ForRuntime("local")); + } + + /// Resolves a state ref using a runtime id. + public string Resolve(string stateRef, string runtimeId) + { + return Resolve(stateRef, CultMeshQueryContext.ForRuntime(runtimeId)); + } + + /// Resolves a state ref using an explicit query context. + public string Resolve(string stateRef, CultMeshQueryContext context) + { + if (string.IsNullOrWhiteSpace(stateRef)) + return ""; + if (context == null) throw new ArgumentNullException(nameof(context)); + return _resolve(stateRef, ResolveContext(context)) ?? ""; + } + + /// Attempts to resolve a non-empty value. + public bool TryResolve(string stateRef, out string value) + { + value = Resolve(stateRef); + return !string.IsNullOrEmpty(value); + } + + /// Attempts to resolve a non-empty value. + public bool TryResolve(string stateRef, CultMeshQueryContext context, out string value) + { + value = Resolve(stateRef, context); + return !string.IsNullOrEmpty(value); + } + + /// Creates a resolver that falls back to another resolver when this resolver returns empty. + public CultMeshStateRefResolver Or(CultMeshStateRefResolver fallback) + { + if (fallback == null) throw new ArgumentNullException(nameof(fallback)); + return new CultMeshStateRefResolver( + ResolverId + "|" + fallback.ResolverId, + (stateRef, context) => + { + var value = Resolve(stateRef, context); + return string.IsNullOrEmpty(value) + ? fallback.Resolve(stateRef, context) + : value; + }, + Sources.Concat(fallback.Sources), + RouteHint.Kind == CultMeshLocalityKind.Automatic ? fallback.RouteHint : RouteHint); + } + + /// Returns the legacy function shape for existing surface renderers. + public Func AsFunc() + { + return Resolve; + } + + private CultMeshQueryContext ResolveContext(CultMeshQueryContext context) + { + if (context.RouteHint.Kind != CultMeshLocalityKind.Automatic || RouteHint.Kind == CultMeshLocalityKind.Automatic) + return context; + + return context.WithRoute(RouteHint); + } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Diagnostics for a named CultMesh state-reference resolver. + /// + public sealed class CultMeshStateRefResolverDiagnostic + { + /// Creates a state-reference resolver diagnostic. + public CultMeshStateRefResolverDiagnostic( + string resolverId, + CultMeshRouteHint routeHint, + IEnumerable? sources = null) + { + ResolverId = RequireNonEmpty(resolverId, nameof(resolverId)); + RouteHint = routeHint ?? CultMeshRouteHint.Automatic; + Sources = sources?.ToArray() ?? Array.Empty(); + } + + /// Gets the semantic resolver id. + public string ResolverId { get; } + + /// Gets the preferred route for resolving refs. + public CultMeshRouteHint RouteHint { get; } + + /// Gets source state used by the resolver. + public IReadOnlyList Sources { get; } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Describes one UI/tool surface property bound to a typed CultMesh state pointer. + /// + public sealed class CultMeshStateBindingDescriptor + { + /// Creates a state binding descriptor. + public CultMeshStateBindingDescriptor( + string targetProp, + string pointerId, + string? sourceId = null, + string? schemaId = null, + CultMeshRouteHint? routeHint = null) + { + TargetProp = RequireNonEmpty(targetProp, nameof(targetProp)); + PointerId = RequireNonEmpty(pointerId, nameof(pointerId)); + SourceId = sourceId; + SchemaId = schemaId; + RouteHint = routeHint ?? CultMeshRouteHint.Automatic; + } + + /// Gets the component property receiving the resolved state value. + public string TargetProp { get; } + + /// Gets the semantic CultMesh state pointer id. + public string PointerId { get; } + + /// Gets the optional source document, cache, native view, or daemon source id. + public string? SourceId { get; } + + /// Gets the optional schema id for the source state. + public string? SchemaId { get; } + + /// Gets the preferred route for resolving and watching the binding. + public CultMeshRouteHint RouteHint { get; } + + /// Creates a binding from an existing state pointer. + public static CultMeshStateBindingDescriptor FromPointer( + string targetProp, + CultMeshStatePointer pointer) + { + if (pointer == null) throw new ArgumentNullException(nameof(pointer)); + var source = pointer.Sources.FirstOrDefault(); + return new CultMeshStateBindingDescriptor( + targetProp, + pointer.PointerId, + source?.SourceId, + source?.SchemaId, + pointer.RouteHint); + } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Transport-friendly fields for a typed state binding descriptor. + /// + public sealed class CultMeshStateBindingRecord + { + /// Creates a state binding record. + public CultMeshStateBindingRecord( + string? targetProp = null, + string? pointerId = null, + string? sourceId = null, + string? schemaId = null, + string? routeKind = null, + string? routeDescription = null) + { + TargetProp = string.IsNullOrWhiteSpace(targetProp) ? "value" : targetProp!; + PointerId = pointerId ?? ""; + SourceId = sourceId ?? ""; + SchemaId = schemaId ?? ""; + RouteKind = routeKind ?? ""; + RouteDescription = routeDescription ?? ""; + } + + /// Gets the component property receiving the resolved state value. + public string TargetProp { get; } + + /// Gets the semantic CultMesh state pointer id. + public string PointerId { get; } + + /// Gets the source document, cache, native view, or daemon source id. + public string SourceId { get; } + + /// Gets the source schema id. + public string SchemaId { get; } + + /// Gets the flattened route kind. + public string RouteKind { get; } + + /// Gets the flattened route description. + public string RouteDescription { get; } + + /// Creates transport-friendly fields from a state binding descriptor. + public static CultMeshStateBindingRecord FromBinding(CultMeshStateBindingDescriptor? binding) + { + var route = CultMeshRouteRecord.FromRoute(binding?.RouteHint); + return new CultMeshStateBindingRecord( + binding?.TargetProp, + binding?.PointerId, + binding?.SourceId, + binding?.SchemaId, + route.Kind, + route.Description); + } + + /// Rehydrates this record as a state binding descriptor. + public CultMeshStateBindingDescriptor ToBinding( + CultMeshRouteHint? fallbackRouteHint = null, + string? fallbackTargetProp = null) + { + var targetProp = string.IsNullOrWhiteSpace(TargetProp) + ? (string.IsNullOrWhiteSpace(fallbackTargetProp) ? "value" : fallbackTargetProp!) + : TargetProp; + var pointerId = string.IsNullOrWhiteSpace(PointerId) + ? $"{targetProp}.unknown" + : PointerId; + var route = new CultMeshRouteRecord(RouteKind, RouteDescription) + .ToRoute(fallbackRouteHint ?? CultMeshRouteHint.Automatic); + + return new CultMeshStateBindingDescriptor( + targetProp, + pointerId, + SourceId, + SchemaId, + route); + } + } + + /// + /// Describes one UI/tool command bound to a typed CultMesh operation. + /// + public sealed class CultMeshOperationBindingDescriptor + { + /// Creates an operation binding descriptor. + public CultMeshOperationBindingDescriptor( + string operationId, + string? label = null, + string? schemaId = null, + CultMeshRouteHint? routeHint = null) + { + OperationId = RequireNonEmpty(operationId, nameof(operationId)); + Label = label ?? ""; + SchemaId = schemaId ?? ""; + RouteHint = routeHint ?? CultMeshRouteHint.Automatic; + } + + /// Gets the semantic CultMesh operation id. + public string OperationId { get; } + + /// Gets the human-facing command label. + public string Label { get; } + + /// Gets the optional request schema id for the operation. + public string SchemaId { get; } + + /// Gets the preferred route for invoking the operation. + public CultMeshRouteHint RouteHint { get; } + + /// Creates a binding from an existing typed operation handle. + public static CultMeshOperationBindingDescriptor FromOperation( + CultMeshOperationHandle operation, + string? label = null, + string? schemaId = null, + CultMeshRouteHint? routeHint = null) + { + if (operation == null) throw new ArgumentNullException(nameof(operation)); + return new CultMeshOperationBindingDescriptor( + operation.OperationId, + label, + schemaId, + routeHint); + } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Transport-friendly fields for a typed operation binding descriptor. + /// + public sealed class CultMeshOperationBindingRecord + { + /// Creates an operation binding record. + public CultMeshOperationBindingRecord( + string? operationId = null, + string? label = null, + string? schemaId = null, + string? routeKind = null, + string? routeDescription = null) + { + OperationId = operationId ?? ""; + Label = label ?? ""; + SchemaId = schemaId ?? ""; + RouteKind = routeKind ?? ""; + RouteDescription = routeDescription ?? ""; + } + + /// Gets the semantic CultMesh operation id. + public string OperationId { get; } + + /// Gets the human-facing command label. + public string Label { get; } + + /// Gets the optional request schema id for the operation. + public string SchemaId { get; } + + /// Gets the flattened route kind. + public string RouteKind { get; } + + /// Gets the flattened route description. + public string RouteDescription { get; } + + /// Creates transport-friendly fields from an operation binding descriptor. + public static CultMeshOperationBindingRecord FromBinding(CultMeshOperationBindingDescriptor? binding) + { + var route = CultMeshRouteRecord.FromRoute(binding?.RouteHint); + return new CultMeshOperationBindingRecord( + binding?.OperationId, + binding?.Label, + binding?.SchemaId, + route.Kind, + route.Description); + } + + /// Rehydrates this record as an operation binding descriptor. + public CultMeshOperationBindingDescriptor ToBinding( + CultMeshRouteHint? fallbackRouteHint = null, + string? fallbackOperationId = null) + { + var operationId = string.IsNullOrWhiteSpace(OperationId) + ? (fallbackOperationId ?? "") + : OperationId; + var route = new CultMeshRouteRecord(RouteKind, RouteDescription) + .ToRoute(fallbackRouteHint ?? CultMeshRouteHint.Automatic); + + return new CultMeshOperationBindingDescriptor( + operationId, + Label, + SchemaId, + route); + } + } + + /// + /// Describes one concrete invocation of a typed CultMesh operation. + /// + public sealed class CultMeshOperationInvocationDescriptor + { + /// Creates an operation invocation descriptor. + public CultMeshOperationInvocationDescriptor( + string operationId, + string? schemaId = null, + CultMeshRouteHint? routeHint = null, + string? idempotencyKey = null) + { + OperationId = RequireNonEmpty(operationId, nameof(operationId)); + SchemaId = schemaId ?? ""; + RouteHint = routeHint ?? CultMeshRouteHint.Automatic; + IdempotencyKey = idempotencyKey; + } + + /// Gets the semantic CultMesh operation id being invoked. + public string OperationId { get; } + + /// Gets the optional request schema id for the operation. + public string SchemaId { get; } + + /// Gets the preferred route for invoking the operation. + public CultMeshRouteHint RouteHint { get; } + + /// Gets an optional caller-provided idempotency key. + public string? IdempotencyKey { get; } + + /// Creates an invocation from an advertised operation binding. + public static CultMeshOperationInvocationDescriptor FromBinding( + CultMeshOperationBindingDescriptor binding, + string? idempotencyKey = null) + { + if (binding == null) throw new ArgumentNullException(nameof(binding)); + return new CultMeshOperationInvocationDescriptor( + binding.OperationId, + binding.SchemaId, + binding.RouteHint, + idempotencyKey); + } + + /// Creates an invocation from an existing typed operation handle. + public static CultMeshOperationInvocationDescriptor FromOperation( + CultMeshOperationHandle operation, + string? schemaId = null, + CultMeshRouteHint? routeHint = null, + string? idempotencyKey = null) + { + if (operation == null) throw new ArgumentNullException(nameof(operation)); + return new CultMeshOperationInvocationDescriptor( + operation.OperationId, + schemaId, + routeHint, + idempotencyKey); + } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Flat, transport-friendly fields for a concrete CultMesh operation invocation. + /// + [MessagePackObject] + public sealed class CultMeshOperationInvocationRecord + { + /// Creates flat operation invocation fields. + [SerializationConstructor] + public CultMeshOperationInvocationRecord( + string? operationId = null, + string? schemaId = null, + string? routeKind = null, + string? routeDescription = null, + string? idempotencyKey = null) + { + OperationId = operationId ?? ""; + SchemaId = schemaId ?? ""; + RouteKind = routeKind ?? ""; + RouteDescription = routeDescription ?? ""; + IdempotencyKey = idempotencyKey ?? ""; + } + + /// Gets the semantic CultMesh operation id. + [Key(0)] + public string OperationId { get; } + + /// Gets the optional request schema id. + [Key(1)] + public string SchemaId { get; } + + /// Gets the serialized route locality kind. + [Key(2)] + public string RouteKind { get; } + + /// Gets the serialized route description. + [Key(3)] + public string RouteDescription { get; } + + /// Gets the serialized idempotency key. + [Key(4)] + public string IdempotencyKey { get; } + + /// Creates flat fields from a typed invocation descriptor. + public static CultMeshOperationInvocationRecord FromInvocation( + CultMeshOperationInvocationDescriptor? invocation, + string? fallbackOperationId = null, + string? fallbackSchemaId = null, + CultMeshRouteHint? fallbackRouteHint = null, + string? fallbackIdempotencyKey = null) + { + var route = CultMeshRouteRecord.FromRoute(invocation?.RouteHint ?? fallbackRouteHint); + return new CultMeshOperationInvocationRecord( + string.IsNullOrWhiteSpace(invocation?.OperationId) ? fallbackOperationId : invocation!.OperationId, + string.IsNullOrWhiteSpace(invocation?.SchemaId) ? fallbackSchemaId : invocation!.SchemaId, + route.Kind, + route.Description, + string.IsNullOrWhiteSpace(invocation?.IdempotencyKey) ? fallbackIdempotencyKey : invocation!.IdempotencyKey); + } + + /// Rehydrates the flat fields into a typed invocation descriptor. + public CultMeshOperationInvocationDescriptor ToInvocation( + string? fallbackOperationId = null, + string? fallbackSchemaId = null, + CultMeshRouteHint? fallbackRouteHint = null, + string? fallbackIdempotencyKey = null) + { + var operationId = string.IsNullOrWhiteSpace(OperationId) ? fallbackOperationId : OperationId; + if (string.IsNullOrWhiteSpace(operationId)) + throw new InvalidOperationException("CultMesh operation invocation fields do not contain an operation id."); + + var route = new CultMeshRouteRecord(RouteKind, RouteDescription) + .ToRoute(fallbackRouteHint); + + return new CultMeshOperationInvocationDescriptor( + operationId!, + string.IsNullOrWhiteSpace(SchemaId) ? fallbackSchemaId : SchemaId, + route, + string.IsNullOrWhiteSpace(IdempotencyKey) ? fallbackIdempotencyKey : IdempotencyKey); + } + } + + /// + /// Shared payload value carried by one concrete CultMesh operation invocation. + /// + public sealed class CultMeshOperationPayload : IReadOnlyDictionary + { + private static readonly IReadOnlyDictionary EmptyFields = + new Dictionary(0, StringComparer.Ordinal); + + private readonly IReadOnlyDictionary _fields; + + /// Creates an empty operation payload. + public CultMeshOperationPayload() + : this(EmptyFields) + { + } + + /// Creates an operation payload from string-compatible fields. + public CultMeshOperationPayload(IEnumerable>? fields) + { + if (fields == null) + { + _fields = EmptyFields; + return; + } + + var copy = new Dictionary(StringComparer.Ordinal); + foreach (var field in fields) + { + if (string.IsNullOrWhiteSpace(field.Key)) + continue; + copy[field.Key] = field.Value ?? ""; + } + + _fields = copy; + } + + /// Gets the empty operation payload. + public static CultMeshOperationPayload Empty { get; } = new(); + + /// Gets the payload field names. + public IEnumerable Keys => _fields.Keys; + + /// Gets the payload field values. + public IEnumerable Values => _fields.Values; + + /// Gets the number of payload fields. + public int Count => _fields.Count; + + /// Gets a payload field by name. + public string this[string key] => _fields[key]; + + /// Gets whether the payload contains a field. + public bool ContainsKey(string key) + { + return _fields.ContainsKey(key); + } + + /// Tries to read a raw string field. + public bool TryGetValue(string key, out string value) + { + return _fields.TryGetValue(key, out value); + } + + /// Reads a raw string field with a default value. + public string GetString(string key, string defaultValue = "") + { + return _fields.TryGetValue(key, out var value) ? value ?? defaultValue : defaultValue; + } + + /// Reads an integer field using invariant parsing. + public int GetInt32(string key, int defaultValue = 0) + { + return int.TryParse(GetString(key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) + ? value + : defaultValue; + } + + /// Reads a floating point field using invariant parsing. + public double GetDouble(string key, double defaultValue = 0) + { + return double.TryParse(GetString(key), NumberStyles.Float, CultureInfo.InvariantCulture, out var value) + ? value + : defaultValue; + } + + /// Reads a boolean field using common CultMesh surface spellings. + public bool GetBoolean(string key, bool defaultValue = false) + { + var value = GetString(key); + if (string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) || + string.Equals(value, "1", StringComparison.Ordinal) || + string.Equals(value, "yes", StringComparison.OrdinalIgnoreCase) || + string.Equals(value, "on", StringComparison.OrdinalIgnoreCase)) + return true; + if (string.Equals(value, "false", StringComparison.OrdinalIgnoreCase) || + string.Equals(value, "0", StringComparison.Ordinal) || + string.Equals(value, "no", StringComparison.OrdinalIgnoreCase) || + string.Equals(value, "off", StringComparison.OrdinalIgnoreCase)) + return false; + return defaultValue; + } + + /// Returns a copy with one field overwritten. + public CultMeshOperationPayload With(string key, string value) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentException("Value must be non-empty.", nameof(key)); + var copy = new Dictionary(_fields, StringComparer.Ordinal) + { + [key] = value ?? "" + }; + return new CultMeshOperationPayload(copy); + } + + /// Copies payload fields into a mutable dictionary. + public Dictionary ToDictionary(IEqualityComparer? comparer = null) + { + var copy = new Dictionary(comparer ?? StringComparer.Ordinal); + foreach (var field in _fields) + copy[field.Key] = field.Value ?? ""; + return copy; + } + + /// Creates a payload from string-compatible fields. + public static CultMeshOperationPayload FromStrings(IEnumerable>? fields) + { + return new CultMeshOperationPayload(fields); + } + + /// + public IEnumerator> GetEnumerator() + { + return _fields.GetEnumerator(); + } + + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + /// + /// Names a reusable projection from typed source state into derived state. + /// + public sealed class CultMeshProjectionRecipe + { + private readonly Func> _project; + private readonly Func>? _watch; + + /// Creates a projection recipe. + public CultMeshProjectionRecipe( + string projectionId, + IEnumerable sources, + Func> project, + CultMeshRouteHint? routeHint = null, + Func>? watch = null) + { + ProjectionId = RequireNonEmpty(projectionId, nameof(projectionId)); + Sources = sources?.ToArray() ?? throw new ArgumentNullException(nameof(sources)); + _project = project ?? throw new ArgumentNullException(nameof(project)); + RouteHint = routeHint ?? CultMeshRouteHint.Automatic; + _watch = watch; + } + + /// Gets the semantic projection id. + public string ProjectionId { get; } + + /// Gets the typed state sources this projection depends on. + public IReadOnlyList Sources { get; } + + /// Gets the preferred or observed route for the projection. + public CultMeshRouteHint RouteHint { get; } + + /// Projects derived state once. + public Task ProjectAsync(TParameters parameters, CultMeshQueryContext context) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + return _project(parameters, ResolveContext(context)); + } + + /// Projects derived state once for one runtime using default context. + public Task ProjectAsync(TParameters parameters, string runtimeId) + { + return ProjectAsync(parameters, CultMeshQueryContext.ForRuntime(runtimeId)); + } + + /// Watches projection results when the recipe supports reactive execution. + public Observable Watch(TParameters parameters, CultMeshQueryContext context) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (_watch == null) + { + throw new NotSupportedException($"Projection recipe '{ProjectionId}' does not support reactive watches."); + } + + return _watch(parameters, ResolveContext(context)); + } + + /// Views this projection recipe as a typed query surface. + public CultMeshQuerySurface AsQuerySurface() + { + return new CultMeshQuerySurface( + ProjectionId, + ProjectAsync, + _watch == null ? null : Watch, + Sources, + RouteHint); + } + + private CultMeshQueryContext ResolveContext(CultMeshQueryContext context) + { + return context.RouteHint.Kind == CultMeshLocalityKind.Automatic && + RouteHint.Kind != CultMeshLocalityKind.Automatic + ? context.WithRoute(RouteHint) + : context; + } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Describes one column in a native CultMesh slice view. + /// + public sealed class CultMeshNativeSliceColumn + { + /// Creates a native slice column descriptor. + public CultMeshNativeSliceColumn(string name, string valueType, int elementSizeBytes) + { + Name = RequireNonEmpty(name, nameof(name)); + ValueType = RequireNonEmpty(valueType, nameof(valueType)); + if (elementSizeBytes <= 0) + { + throw new ArgumentOutOfRangeException(nameof(elementSizeBytes), "Element size must be positive."); + } + + ElementSizeBytes = elementSizeBytes; + } + + /// Gets the column name. + public string Name { get; } + + /// Gets the schema value type for each element. + public string ValueType { get; } + + /// Gets the size of one element in bytes. + public int ElementSizeBytes { get; } + + /// Creates a native slice column descriptor for an unmanaged CLR value type. + public static CultMeshNativeSliceColumn For(string name) where TValue : unmanaged + { + return new CultMeshNativeSliceColumn(name, typeof(TValue).FullName ?? typeof(TValue).Name, SizeOf()); + } + + private static int SizeOf() where TValue : unmanaged + { + return System.Runtime.InteropServices.Marshal.SizeOf(); + } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Describes a typed native slice view exposed by CultMesh. + /// + public sealed class CultMeshNativeSliceViewDescriptor + { + /// Creates a native slice view descriptor. + public CultMeshNativeSliceViewDescriptor( + string viewId, + string schemaId, + int rowCount, + IEnumerable columns, + CultMeshRouteHint? route = null, + string? nativeHandle = null) + { + ViewId = RequireNonEmpty(viewId, nameof(viewId)); + SchemaId = RequireNonEmpty(schemaId, nameof(schemaId)); + if (rowCount < 0) + { + throw new ArgumentOutOfRangeException(nameof(rowCount), "Row count cannot be negative."); + } + + RowCount = rowCount; + Columns = columns?.ToArray() ?? throw new ArgumentNullException(nameof(columns)); + Route = route ?? CultMeshRouteHint.Automatic; + NativeHandle = nativeHandle; + } + + /// Gets the semantic view id. + public string ViewId { get; } + + /// Gets the schema id for the rows in this view. + public string SchemaId { get; } + + /// Gets the number of rows in this view. + public int RowCount { get; } + + /// Gets column descriptors in view order. + public IReadOnlyList Columns { get; } + + /// Gets the route that exposes this view. + public CultMeshRouteHint Route { get; } + + /// Gets an optional runtime-specific native handle. + public string? NativeHandle { get; } + + /// Gets the estimated byte stride for one row when all columns are densely packed. + public int DenseRowStrideBytes => Columns.Sum(column => column.ElementSizeBytes); + + /// Finds a column descriptor by name. + public CultMeshNativeSliceColumn? FindColumn(string name) + { + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value must be non-empty.", nameof(name)); + return Columns.FirstOrDefault(column => string.Equals(column.Name, name, StringComparison.Ordinal)); + } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + /// + /// Inspectable metadata for a native slice view. + /// + public sealed class CultMeshNativeSliceViewDiagnostic + { + /// Creates native slice view diagnostics. + public CultMeshNativeSliceViewDiagnostic(CultMeshNativeSliceViewDescriptor view) + { + if (view == null) throw new ArgumentNullException(nameof(view)); + ViewId = view.ViewId; + SchemaId = view.SchemaId; + RowCount = view.RowCount; + Columns = view.Columns.ToArray(); + Route = view.Route; + NativeHandle = view.NativeHandle; + DenseRowStrideBytes = view.DenseRowStrideBytes; + } + + /// Gets the semantic view id. + public string ViewId { get; } + + /// Gets the schema id for rows in this view. + public string SchemaId { get; } + + /// Gets the row count advertised by this view. + public int RowCount { get; } + + /// Gets column descriptors in view order. + public IReadOnlyList Columns { get; } + + /// Gets the route that exposes this view. + public CultMeshRouteHint Route { get; } + + /// Gets an optional runtime-specific native handle. + public string? NativeHandle { get; } + + /// Gets the estimated byte stride for one densely packed row. + public int DenseRowStrideBytes { get; } + } +} diff --git a/src/GameCult.Mesh/docs/public-api.md b/src/GameCult.Mesh/docs/public-api.md index 6089d331..30044d6e 100644 --- a/src/GameCult.Mesh/docs/public-api.md +++ b/src/GameCult.Mesh/docs/public-api.md @@ -87,6 +87,273 @@ whether a peer card is authorized for a requested role and shard. This keeps peer exchange separate from authority instead of letting contact gossip mutate the world. +### Cross-Runtime Primitives + +These are the first shared primitives for the cozy typed Verse surface that +projects like Aetheria should build on instead of inventing daemon-specific +adapters: + +In CultMesh, a primitive can be more than a data shape. Useful state-access +patterns should graduate into shared managed affordances: typed handles, +generated binding metadata, route selection, authority context, watches, +diagnostics, and native-view descriptors that let each runtime use the same +semantic API. The domain package names the thing; CultMesh supplies the fast, +portable machinery that makes it feel native. + +- `CultMeshStatePointer` is a typed pointer to Verse state that UI surfaces, + tools, and clients can resolve or watch without string state-ref plumbing. + `CultMesh.StatePointer(...)` creates simple or context-aware pointers. + Pointers carry source descriptors and route hints, and + `CultMesh.DescribeStatePointer(...)` exposes that metadata for catalogs and + inspectors. Context-aware pointers can bind with `Bind(...)`, + `CultMesh.BindStatePointer(...)`, or `CultMeshVerse.BindStatePointer(...)`, + producing a Verse-bound handle that resolves and watches through the shared + Verse query context instead of forcing UI/tool code to rebuild route plumbing. +- `CultMeshMutableStatePointer` extends the same state pointer shape with a + typed `ReplaceAsync(...)` operation for authoritative document handles, + editor tools, and UI surfaces that intentionally write state. It keeps the + read/watch/replace trio in CultMesh instead of letting each domain package + invent private document wrappers. `CultMesh.MutableStatePointer(...)`, + `CultMesh.BindMutableStatePointer(...)`, and + `CultMeshVerse.BindMutableStatePointer(...)` preserve the same Verse context, + route hints, source diagnostics, and state-binding metadata as read-only + pointers. +- `CultMeshStateRefResolver` is the shared bridge for UI/tool surfaces that + still receive state references from authored Eve/CultUI documents. Resolvers + are named, composable, carry source descriptors and route hints, and expose + diagnostics through `CultMesh.DescribeStateRefResolver(...)`. Legacy + `Func` lowerers can use `AsFunc()` while generated surfaces + migrate to typed state pointers and binding descriptors. +- `CultMeshStateBindingDescriptor` and `CultMeshStateBindingRecord` are the + UI/tool state edge. Live surfaces bind a component prop to a typed state + pointer; persistence and transport boundaries use + `CultMesh.StateBindingRecord(...)` instead of local pointer-field copies or + route parsers. +- `CultMeshOperationHandle` is a method-shaped operation + handle. `CultMeshOperationContext`, `CultMeshAuthorityClaim`, and + `CultMeshOperationReceipt` carry runtime identity, authority metadata, + route hints, idempotency, acceptance, and diagnostics. + `CultMesh.DescribeOperationHandle(...)` exposes the operation id for surface + catalogs and generated tooling. `Bind(...)`, `CultMesh.BindOperation(...)`, + and `CultMeshVerse.BindOperation(...)` produce Verse-bound handles so + generated domain facades can invoke without re-threading context plumbing at + every call site. +- `CultMeshOperationBindingDescriptor`, `CultMeshOperationBindingRecord`, + `CultMeshOperationInvocationDescriptor`, `CultMeshOperationInvocationRecord`, + and `CultMeshOperationPayload` are the UI/tool operation edge. Live surfaces + carry typed operation identity and scalar payload reads; persistence and + transport boundaries use `CultMesh.OperationBindingRecord(...)`, + `CultMesh.OperationInvocationRecord(...)`, and + `CultMeshOperationPayload.ToDictionary()` instead of local route parsers or + ad-hoc payload maps. +- `CultMeshQuerySurface` is a typed derived-state query + surface. Query execution can be local, remote, shared-memory, IPC, or + WASM-routed while preserving the same semantic API. Query surfaces can also + carry source descriptors, route hints, and reactive watches so tools and UI + runtimes can inspect what state a generated query depends on, which route it + prefers, and subscribe without inventing polling glue. Query surfaces can be + bound with `Bind(...)`, `CultMesh.BindQuery(...)`, or + `CultMeshVerse.BindQuery(...)`. +- `CultMeshLiveFeed` is a typed live view surface for + composed client snapshots such as an RTS viewport plus selection, health, and + authority panels. It gives clients one `snapshot(...)` / `watch(...)` handle + while preserving source descriptors and route defaults. Live feeds can be + bound with `Bind(...)`, `CultMesh.BindLiveFeed(...)`, or + `CultMeshVerse.BindLiveFeed(...)`. +- `CultMeshDocumentHandle` is the typed reactive document edge for + CultCache/CultNet state. It exposes document metadata, one coherent + `LatestAsync()` read, and a `Watch()` stream without forcing callers through + transport or projection layers. Same-schema CLR aliases can be requested with + `AsSchemaAlias()`; CultMesh verifies the shared + `[CultDocument(schemaName, schemaVersion)]` identity and uses the shared + CultCache MessagePack codec for conversion. +- `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 + surface. Reactive projection recipes preserve their watcher when exposed as + query surfaces. When a caller supplies an automatic query context, the recipe + route becomes the default; explicit caller routes still win. +- `CultMeshNativeSliceViewDescriptor` and `CultMeshNativeSliceColumn` describe + typed native views for co-located runtimes such as Unity Burst jobs, Rust + slices, or browser typed arrays. `CultMesh.DescribeNativeSliceView(...)` + exposes the full view id, schema id, row count, columns, route, native handle, + and dense stride for tools and adapters. +- `CultMeshRouteHint`, `CultMeshRouteRecord`, and `CultMeshLocalityKind` name + locality choices without forcing application code to choose transport-specific + APIs. `CultMesh.RouteRecord(...)` is the shared way to flatten and rehydrate + route kind/description fields at persisted document, UI, tool, and transport + boundaries. + +These types are intentionally small handles and descriptors. Generated bindings +and runtime adapters should compose them into domain sugar such as +`verse.aetheria().entity(id).pilot().move(...)`, +`zone.objects().visibleTo(units).within(rect)`, and +`renderView.AsNativeArrays()`. + +The C# helpers already support the first layer of that shape: + +```csharp +var context = CultMeshOperationContext + .ForRuntime("unity-raven") + .WithClaim(new CultMeshAuthorityClaim("pilot-control", shardId: "zone:local-rts")) + .WithRoute(new CultMeshRouteHint(CultMeshLocalityKind.InProcess)); + +await moveOperation.InvokeAsync(moveRequest, context); +``` + +Generated facades should bind surfaces once to the correct Verse view: + +```csharp +var queryVerse = verse.WithRoute(new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory)); +var commandVerse = verse.WithRoute(new CultMeshRouteHint(CultMeshLocalityKind.Network)); + +var visibleObjects = queryVerse.BindQuery(objectsViewport); +var movePilot = commandVerse.BindOperation(setMoveVector); + +var objects = await visibleObjects.ExecuteAsync(viewportRequest); +await movePilot.InvokeAsync(moveRequest, idempotencyKey); +``` + +A single typed document can be surfaced the same way: + +```csharp +var cockpit = CultMesh.Document( + "aetheria.ship.cockpit.current", + verse, + context => cockpitStore.LatestAsync(context), + context => cockpitStore.Watch(context), + sources: new[] + { + CultMesh.ProjectionSource( + "pilot:cockpit", + schemaId: "gamecult.aetheria.cockpit_state.v1") + }); + +var current = await cockpit.LatestAsync(); +using var subscription = cockpit.Watch(next => RenderCockpit(next)); + +var daemonAlias = cockpit.AsSchemaAlias(); +``` + +```ts +const queryVerse = verse.withRoute("shared-memory", statePath); +const commandVerse = verse + .withRoute("network", endpoint) + .withClaim("commander-control", { shardId: "aetheria.local" }); + +const visibleObjects = CultMesh.bindQuery(queryVerse, queries.objectsViewport); +const movePilot = CultMesh.bindOperation(commandVerse, operations.setMoveVector); + +const objects = await visibleObjects.execute(viewportRequest); +await movePilot.invoke({ actorEntityKey, directionX: 1, directionY: 0 }); +``` + +Native view descriptors can name unmanaged columns without hand-entered byte +sizes: + +```csharp +var view = new CultMeshNativeSliceViewDescriptor( + "aetheria.zone.render", + "gamecult.aetheria.render_body.v1", + rowCount, + new[] + { + CultMeshNativeSliceColumn.For("position"), + CultMeshNativeSliceColumn.For("velocity") + }, + new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory)); +``` + +Projection recipes give local projection code a shared shape before it becomes +runtime-specific glue: + +```csharp +var objectsViewport = CultMesh.ProjectionRecipe( + "aetheria.zone.objects.visible", + new[] + { + CultMesh.ProjectionSource("daemon:aetheria.frame.latest.v1", "gamecult.aetheria.daemon_frame.v1"), + CultMesh.ProjectionSource("daemon:aetheria.authority.policy.v1") + }, + (request, context) => ProjectObjectsAsync(request, context), + new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory)); + +var query = objectsViewport.AsQuerySurface(); +``` + +Calling `query.ExecuteAsync(request, "unity-raven")` inherits the recipe's +shared-memory route. Calling it with a `CultMeshQueryContext` that already names +`Network`, `Ipc`, `Wasm`, or another route uses the caller's route instead. +`query.Sources` and `query.RouteHint` preserve the recipe metadata for +diagnostics, tooling, and UI/MCP surfaces. If the projection supports reactive +execution, `query.Watch(...)` / `query.watch(...)` keeps the same projection +route resolution, so a browser map tab, Unity renderer, Eve surface, or MCP +tool can subscribe through the same typed query affordance it uses for one-shot +reads. + +`CultMesh.DescribeQuerySurface(...)` and +`CultMesh.DescribeProjectionRecipe(...)` expose the same metadata in a stable +diagnostic shape for generated clients, Eve/CultUI inspectors, MCP tools, and +surface catalogs. Tooling should ask CultMesh for these descriptors instead of +copying source lists and route hints by convention. +`CultMesh.DescribeSurface(...)` and `CultMesh.DescribeSurfaceCatalog(...)` +collect query surfaces, projection recipes, live feeds, operations, state +pointers, and native views into one inspectable catalog for a runtime or +generated binding package. State pointers are advertised by pointer id; native +slice views are advertised by view id and route while the full column layout +stays in `CultMeshNativeSliceViewDescriptor`. +Catalog diagnostics support exact id lookup and kind filtering so tools can +discover, for example, every operation or native view without local scan +conventions. `CultMesh.DescribeSurfaceCatalogIndex(...)` / TS +`CultMesh.surfaceCatalogIndex(...)` promotes that grouping into a shared +diagnostic shape with `queries`, `projectionRecipes`, `liveFeeds`, +`operations`, `statePointers`, and `nativeSliceViews`, so generated bindings +and tools can ask for the semantic bucket they need without copying catalog +scan logic. + +Live feeds compose several query surfaces into one client-facing snapshot +without making the UI own refresh loops: + +```csharp +var viewportFeed = CultMesh.LiveFeed( + "aetheria.rts.viewport.feed", + (request, context) => SnapshotViewportAsync(request, context), + (request, context) => WatchViewport(request, context), + new[] + { + CultMesh.ProjectionSource("daemon:aetheria.frame.latest.v1"), + CultMesh.ProjectionSource("daemon:aetheria.health.latest.v1") + }, + new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory)); + +var feedDiagnostics = CultMesh.DescribeLiveFeed(viewportFeed); +``` + +When a runtime has a snapshot path before it has native reactive transport, +`CultMesh.PollingQueryWatcher(...)` provides a disposable watch adapter: + +```csharp +var feed = CultMesh.LiveFeed( + "aetheria.rts.viewport.feed", + SnapshotViewportAsync, + CultMesh.PollingQueryWatcher( + SnapshotViewportAsync, + new CultMeshPollingWatchOptions( + TimeSpan.FromMilliseconds(50)))); +``` + +The polling adapter is intentionally a bridge primitive: application UI code +still receives a typed `Watch(...)` handle, and the sampling loop can later be +replaced by shared-memory, IPC, network, or WASM-native reactivity without +changing renderer call sites. + +The staged roadmap for growing these handles into the cross-runtime developer +experience lives in `docs/cross-runtime-primitives-roadmap.md`. When a project +discovers a reusable state-ref, operation, query, projection, authority, watch, +surface-binding, schema-generation, routing, diagnostic, or native-slab +abstraction, it should graduate into that roadmap and then into the shared +CultMesh/CultLib layer instead of remaining a private bridge. + ### RUDP Helpers `CultMesh.ParseRudpEndpoint(...)`, `CreateRudpServer(...)`, diff --git a/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs b/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs index 4278d621..7985144a 100644 --- a/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs +++ b/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using FluentAssertions; using GameCult.Caching; +using MessagePack; using NUnit.Framework; using R3; @@ -10,6 +13,833 @@ namespace GameCult.Mesh.Tests; public sealed class CultMeshStreamingTests { + [Test] + public async Task TypedOperationHandle_CarriesAuthorityAndRouteContext() + { + var handle = new CultMeshOperationHandle( + "aetheria.entity.pilot.move", + (request, context) => Task.FromResult(new CultMeshOperationReceipt( + "aetheria.entity.pilot.move", + accepted: request.EntityId == 7 && + context.Claims.Count == 1 && + context.Claims[0].Role == "pilot-control" && + context.RouteHint.Kind == CultMeshLocalityKind.InProcess, + context.RouteHint))); + + var receipt = await handle.InvokeAsync( + new MeshMoveRequest(7, 1, -1), + CultMeshOperationContext + .ForRuntime("unity-raven") + .WithClaim(new CultMeshAuthorityClaim("pilot-control", shardId: "zone:local-rts")) + .WithRoute(new CultMeshRouteHint(CultMeshLocalityKind.InProcess, "co-located daemon")) + .WithIdempotencyKey("move-7-001")); + + receipt.Accepted.Should().BeTrue(); + receipt.Route.Kind.Should().Be(CultMeshLocalityKind.InProcess); + + var diagnostic = CultMesh.DescribeOperationHandle(handle); + diagnostic.OperationId.Should().Be("aetheria.entity.pilot.move"); + } + + [Test] + public void ContextBuilders_MakeTypedHandleCallsReadLikeVerseOperations() + { + var operationContext = CultMesh.OperationContextFor("unity-raven") + .Claim("pilot-control", shardId: "zone:local-rts", leaseId: "lease:raven") + .Route(CultMeshLocalityKind.SharedMemory, "co-located daemon") + .Idempotency("move-7-001") + .Build(); + + operationContext.RuntimeId.Should().Be("unity-raven"); + operationContext.Claims.Should().ContainSingle(); + operationContext.Claims[0].Role.Should().Be("pilot-control"); + operationContext.Claims[0].ShardId.Should().Be("zone:local-rts"); + operationContext.Claims[0].LeaseId.Should().Be("lease:raven"); + operationContext.RouteHint.Kind.Should().Be(CultMeshLocalityKind.SharedMemory); + operationContext.IdempotencyKey.Should().Be("move-7-001"); + + var queryContext = CultMesh.QueryContextFor("browser-starfire") + .Route(CultMeshLocalityKind.Wasm, "browser-local projection") + .Build(); + + queryContext.RuntimeId.Should().Be("browser-starfire"); + queryContext.RouteHint.Kind.Should().Be(CultMeshLocalityKind.Wasm); + queryContext.RouteHint.Description.Should().Be("browser-local projection"); + } + + [Test] + public async Task VerseContext_LetsGeneratedDomainSugarUseSharedTypedContexts() + { + var verse = await CultMesh.ConnectVerseAsync( + "starbridge", + "unity-raven", + new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "co-located Verse"), + new[] { new CultMeshAuthorityClaim("pilot-control", shardId: "zone:raven", leaseId: "lease:raven") }); + var commandVerse = verse.WithRoute(new CultMeshRouteHint(CultMeshLocalityKind.Network, "remote command route")); + + var aetheria = commandVerse.Use(context => new MeshAetheriaDomain(context)); + var receipt = await aetheria.Entity(7).Pilot.MoveAsync(new MeshVec2(1, 0), "move:raven:1"); + var viewport = await aetheria.Zone("zone:raven").Objects.VisibleWithinAsync(new MeshViewportRequest(-16, 16)); + + verse.VerseId.Should().Be("starbridge"); + verse.RuntimeId.Should().Be("unity-raven"); + verse.QueryContext().RouteHint.Kind.Should().Be(CultMeshLocalityKind.SharedMemory); + commandVerse.OperationContext("move:raven:2").RouteHint.Kind.Should().Be(CultMeshLocalityKind.Network); + commandVerse.OperationContext("move:raven:2").IdempotencyKey.Should().Be("move:raven:2"); + receipt.OperationId.Should().Be("aetheria.entity.pilot.move"); + receipt.Accepted.Should().BeTrue(); + receipt.Route.Kind.Should().Be(CultMeshLocalityKind.Network); + viewport.Should().Equal("unity-raven:zone:raven:-16:16:Network"); + } + + [Test] + public async Task QuerySurfaceAndStatePointer_ExposeTypedReactiveState() + { + var query = new CultMeshQuerySurface( + "aetheria.zone.objects.visible", + (request, context) => Task.FromResult(new[] { $"{context.RuntimeId}:{request.MinX}:{request.MaxX}" })); + + var queryResult = await query.ExecuteAsync( + new MeshViewportRequest(-8, 8), + CultMeshQueryContext.ForRuntime("browser-starfire")); + + queryResult.Should().Equal("browser-starfire:-8:8"); + + var queryDiagnostic = CultMesh.DescribeQuerySurface(query); + queryDiagnostic.QueryId.Should().Be("aetheria.zone.objects.visible"); + queryDiagnostic.RouteHint.Kind.Should().Be(CultMeshLocalityKind.Automatic); + queryDiagnostic.Sources.Should().BeEmpty(); + + var subject = new Subject(); + var pointer = CultMesh.StatePointer( + "aetheria.selection.current", + () => Task.FromResult("entity:station:0"), + () => subject, + new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "co-located selection cache"), + new[] + { + CultMesh.ProjectionSource( + "daemon:aetheria.selection.current.v1", + schemaId: "gamecult.aetheria.selection.v1") + }); + + string observed = null!; + using var subscription = pointer.Watch().Subscribe(value => observed = value); + + (await pointer.ResolveAsync()).Should().Be("entity:station:0"); + subject.OnNext("entity:pawn:7"); + observed.Should().Be("entity:pawn:7"); + + var pointerDiagnostic = CultMesh.DescribeStatePointer(pointer); + pointerDiagnostic.PointerId.Should().Be("aetheria.selection.current"); + pointerDiagnostic.RouteHint.Kind.Should().Be(CultMeshLocalityKind.SharedMemory); + pointerDiagnostic.Sources.Should().ContainSingle().Which.SchemaId.Should().Be("gamecult.aetheria.selection.v1"); + } + + [Test] + public async Task StatePointer_CanBindToVerseContextForUiAndTools() + { + var subject = new Subject(); + var pointer = CultMesh.StatePointer( + "aetheria.daemon.frame.latest", + context => Task.FromResult($"{context.RuntimeId}:{context.RouteHint.Kind}"), + context => subject.Select(value => $"{context.RuntimeId}:{value}:{context.RouteHint.Kind}"), + new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "co-located daemon frame"), + new[] + { + CultMesh.ProjectionSource( + "daemon:aetheria.frame.latest.v1", + schemaId: "gamecult.aetheria.daemon_frame.v1") + }); + + var verse = CultMesh.Verse( + "aetheria.local", + "bifrost-tool", + new CultMeshRouteHint(CultMeshLocalityKind.Ipc, "tool bridge")); + var bound = verse.BindStatePointer(pointer); + + (await bound.ResolveAsync()).Should().Be("bifrost-tool:Ipc"); + + string observed = null!; + using var subscription = bound.Watch().Subscribe(value => observed = value); + subject.OnNext("frame:12"); + + bound.PointerId.Should().Be("aetheria.daemon.frame.latest"); + bound.Sources.Should().ContainSingle().Which.SchemaId.Should().Be("gamecult.aetheria.daemon_frame.v1"); + observed.Should().Be("bifrost-tool:frame:12:Ipc"); + } + + [Test] + public async Task MutableStatePointer_HoistsReadWatchReplaceDocumentHandles() + { + var subject = new Subject(); + var stored = "frame:0"; + var observedContexts = new List(); + var pointer = CultMesh.MutableStatePointer( + "aetheria.daemon.frame.latest", + context => + { + observedContexts.Add($"read:{context.RuntimeId}:{context.RouteHint.Kind}"); + return Task.FromResult(stored); + }, + context => subject.Select(value => $"{context.RuntimeId}:{value}:{context.RouteHint.Kind}"), + (context, value) => + { + observedContexts.Add($"replace:{context.RuntimeId}:{context.RouteHint.Kind}"); + stored = value; + subject.OnNext(value); + return Task.CompletedTask; + }, + new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "co-located daemon frame"), + new[] + { + CultMesh.ProjectionSource( + "daemon:aetheria.frame.latest.v1", + schemaId: "gamecult.aetheria.daemon_frame.v1") + }); + + var verse = CultMesh.Verse( + "aetheria.local", + "unity-raven", + new CultMeshRouteHint(CultMeshLocalityKind.Ipc, "tool bridge")); + var bound = verse.BindMutableStatePointer(pointer); + + string observed = null!; + using var subscription = bound.Watch().Subscribe(value => observed = value); + + (await bound.ReadAsync()).Should().Be("frame:0"); + await bound.ReplaceAsync("frame:1"); + + stored.Should().Be("frame:1"); + observed.Should().Be("unity-raven:frame:1:Ipc"); + observedContexts.Should().Equal("read:unity-raven:Ipc", "replace:unity-raven:Ipc"); + + var diagnostic = CultMesh.DescribeStatePointer(pointer); + diagnostic.PointerId.Should().Be("aetheria.daemon.frame.latest"); + diagnostic.RouteHint.Kind.Should().Be(CultMeshLocalityKind.SharedMemory); + + var binding = CultMesh.StateBinding("frame", pointer); + binding.PointerId.Should().Be("aetheria.daemon.frame.latest"); + binding.SchemaId.Should().Be("gamecult.aetheria.daemon_frame.v1"); + } + + [Test] + public void StateBindingDescriptor_BindsUiPropsToTypedStatePointers() + { + var pointer = CultMesh.StatePointer( + "aetheria.selection.current", + () => Task.FromResult("entity:station:0"), + () => new Subject(), + new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "co-located selection cache"), + new[] + { + CultMesh.ProjectionSource( + "daemon:aetheria.selection.current.v1", + schemaId: "gamecult.aetheria.selection.v1") + }); + + var binding = CultMesh.StateBinding("value", pointer); + var explicitBinding = CultMesh.StateBinding( + "label", + "aetheria.selection.label", + sourceId: "daemon:aetheria.selection.label.v1", + schemaId: "gamecult.aetheria.selection_label.v1", + routeHint: new CultMeshRouteHint(CultMeshLocalityKind.Ipc, "tool bridge")); + + binding.TargetProp.Should().Be("value"); + binding.PointerId.Should().Be("aetheria.selection.current"); + binding.SourceId.Should().Be("daemon:aetheria.selection.current.v1"); + binding.SchemaId.Should().Be("gamecult.aetheria.selection.v1"); + binding.RouteHint.Kind.Should().Be(CultMeshLocalityKind.SharedMemory); + binding.RouteHint.Description.Should().Be("co-located selection cache"); + + explicitBinding.TargetProp.Should().Be("label"); + explicitBinding.PointerId.Should().Be("aetheria.selection.label"); + explicitBinding.SourceId.Should().Be("daemon:aetheria.selection.label.v1"); + explicitBinding.SchemaId.Should().Be("gamecult.aetheria.selection_label.v1"); + explicitBinding.RouteHint.Kind.Should().Be(CultMeshLocalityKind.Ipc); + } + + [Test] + public void StateBindingRecord_FlattensAndRehydratesPointerBindingFields() + { + var binding = CultMesh.StateBinding( + "status", + "aetheria.current.status", + sourceId: "daemon:aetheria.frame.latest.v1", + schemaId: "gamecult.aetheria.daemon_frame.v1", + routeHint: new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "co-located frame slab")); + + var record = CultMesh.StateBindingRecord(binding); + record.TargetProp.Should().Be("status"); + record.PointerId.Should().Be("aetheria.current.status"); + record.SourceId.Should().Be("daemon:aetheria.frame.latest.v1"); + record.SchemaId.Should().Be("gamecult.aetheria.daemon_frame.v1"); + record.RouteKind.Should().Be(nameof(CultMeshLocalityKind.SharedMemory)); + record.RouteDescription.Should().Be("co-located frame slab"); + + var fromTypescript = CultMesh.StateBindingRecord( + "value", + "aetheria.selection.current", + "daemon:aetheria.selection.current.v1", + "gamecult.aetheria.selection.v1", + "shared-memory", + "browser-adjacent cache").ToBinding(); + + fromTypescript.TargetProp.Should().Be("value"); + fromTypescript.PointerId.Should().Be("aetheria.selection.current"); + fromTypescript.SourceId.Should().Be("daemon:aetheria.selection.current.v1"); + fromTypescript.SchemaId.Should().Be("gamecult.aetheria.selection.v1"); + fromTypescript.RouteHint.Kind.Should().Be(CultMeshLocalityKind.SharedMemory); + fromTypescript.RouteHint.Description.Should().Be("browser-adjacent cache"); + } + + [Test] + public void OperationBindingDescriptor_BindsUiCommandsToTypedOperations() + { + var operation = new CultMeshOperationHandle( + "aetheria.entity.pilot.move", + (_request, _context) => Task.FromResult(new CultMeshOperationReceipt("aetheria.entity.pilot.move", true))); + + var binding = CultMesh.OperationBinding( + operation, + label: "Move", + schemaId: "gamecult.aetheria.pilot_move.v1", + routeHint: new CultMeshRouteHint(CultMeshLocalityKind.Network, "remote Verse peer")); + var explicitBinding = CultMesh.OperationBinding( + "aetheria.surface.refresh", + label: "Refresh"); + + binding.OperationId.Should().Be("aetheria.entity.pilot.move"); + binding.Label.Should().Be("Move"); + binding.SchemaId.Should().Be("gamecult.aetheria.pilot_move.v1"); + binding.RouteHint.Kind.Should().Be(CultMeshLocalityKind.Network); + binding.RouteHint.Description.Should().Be("remote Verse peer"); + + explicitBinding.OperationId.Should().Be("aetheria.surface.refresh"); + explicitBinding.Label.Should().Be("Refresh"); + explicitBinding.SchemaId.Should().Be(""); + explicitBinding.RouteHint.Kind.Should().Be(CultMeshLocalityKind.Automatic); + } + + [Test] + public void OperationBindingRecord_FlattensAndRehydratesSurfaceCommandFields() + { + var binding = CultMesh.OperationBinding( + "aetheria.entity.pilot.move", + label: "Move", + schemaId: "gamecult.aetheria.pilot_move.v1", + routeHint: new CultMeshRouteHint(CultMeshLocalityKind.Network, "remote Verse peer")); + + var record = CultMesh.OperationBindingRecord(binding); + record.OperationId.Should().Be("aetheria.entity.pilot.move"); + record.Label.Should().Be("Move"); + record.SchemaId.Should().Be("gamecult.aetheria.pilot_move.v1"); + record.RouteKind.Should().Be(nameof(CultMeshLocalityKind.Network)); + record.RouteDescription.Should().Be("remote Verse peer"); + + var fromTypescript = CultMesh.OperationBindingRecord( + "aetheria.surface.refresh", + "Refresh", + "gamecult.aetheria.refresh.v1", + "in-process", + "embedded UI runtime").ToBinding(); + + fromTypescript.OperationId.Should().Be("aetheria.surface.refresh"); + fromTypescript.Label.Should().Be("Refresh"); + fromTypescript.SchemaId.Should().Be("gamecult.aetheria.refresh.v1"); + fromTypescript.RouteHint.Kind.Should().Be(CultMeshLocalityKind.InProcess); + fromTypescript.RouteHint.Description.Should().Be("embedded UI runtime"); + } + + [Test] + public void OperationInvocationDescriptor_CarriesTypedOperationIdentityThroughUiRequests() + { + var binding = CultMesh.OperationBinding( + "aetheria.entity.pilot.move", + label: "Move", + schemaId: "gamecult.aetheria.pilot_move.v1", + routeHint: new CultMeshRouteHint(CultMeshLocalityKind.Ipc, "local Verse node")); + + var invocation = CultMesh.OperationInvocation(binding, idempotencyKey: "move:42"); + var explicitInvocation = CultMesh.OperationInvocation( + "aetheria.surface.refresh", + routeHint: new CultMeshRouteHint(CultMeshLocalityKind.Network, "remote Verse peer")); + + invocation.OperationId.Should().Be("aetheria.entity.pilot.move"); + invocation.SchemaId.Should().Be("gamecult.aetheria.pilot_move.v1"); + invocation.RouteHint.Kind.Should().Be(CultMeshLocalityKind.Ipc); + invocation.RouteHint.Description.Should().Be("local Verse node"); + invocation.IdempotencyKey.Should().Be("move:42"); + + explicitInvocation.OperationId.Should().Be("aetheria.surface.refresh"); + explicitInvocation.SchemaId.Should().Be(""); + explicitInvocation.RouteHint.Kind.Should().Be(CultMeshLocalityKind.Network); + explicitInvocation.IdempotencyKey.Should().BeNull(); + } + + [Test] + public void OperationPayload_ReadsCommonSurfaceScalarFields() + { + var payload = CultMesh.OperationPayload( + ("value", "42.5"), + ("tierIndex", "3"), + ("enabled", "on"), + ("name", "Starfire")); + var updated = payload.With("enabled", "false"); + + payload.GetString("name").Should().Be("Starfire"); + payload.GetString("missing", "fallback").Should().Be("fallback"); + payload.GetInt32("tierIndex", -1).Should().Be(3); + payload.GetInt32("missing", -1).Should().Be(-1); + payload.GetDouble("value", -1).Should().Be(42.5); + payload.GetBoolean("enabled").Should().BeTrue(); + updated.GetBoolean("enabled", true).Should().BeFalse(); + updated.GetString("name").Should().Be("Starfire"); + payload.GetBoolean("missing", true).Should().BeTrue(); + payload.Should().ContainKey("value"); + payload.Count.Should().Be(4); + } + + [Test] + public void OperationInvocationRecord_FlattensAndRehydratesTransportFields() + { + var invocation = CultMesh.OperationInvocation( + "aetheria.entity.pilot.move", + "gamecult.aetheria.pilot_move.v1", + new CultMeshRouteHint(CultMeshLocalityKind.Ipc, "local Verse node"), + "move:42"); + + var record = CultMesh.OperationInvocationRecord(invocation); + var restored = record.ToInvocation( + fallbackOperationId: "fallback.operation", + fallbackSchemaId: "fallback.schema", + fallbackRouteHint: new CultMeshRouteHint(CultMeshLocalityKind.Network, "fallback route")); + var fallback = CultMesh.OperationInvocationRecord( + operationId: "", + schemaId: "", + routeKind: "not-a-route", + routeDescription: "", + idempotencyKey: "") + .ToInvocation( + fallbackOperationId: "fallback.operation", + fallbackSchemaId: "fallback.schema", + fallbackRouteHint: new CultMeshRouteHint(CultMeshLocalityKind.Network, "fallback route"), + fallbackIdempotencyKey: "fallback-key"); + var payloadFields = CultMesh.OperationPayload(("value", "Raven")).ToDictionary(); + + record.OperationId.Should().Be("aetheria.entity.pilot.move"); + record.SchemaId.Should().Be("gamecult.aetheria.pilot_move.v1"); + record.RouteKind.Should().Be("Ipc"); + record.RouteDescription.Should().Be("local Verse node"); + record.IdempotencyKey.Should().Be("move:42"); + restored.OperationId.Should().Be("aetheria.entity.pilot.move"); + restored.SchemaId.Should().Be("gamecult.aetheria.pilot_move.v1"); + restored.RouteHint.Kind.Should().Be(CultMeshLocalityKind.Ipc); + restored.IdempotencyKey.Should().Be("move:42"); + fallback.OperationId.Should().Be("fallback.operation"); + fallback.SchemaId.Should().Be("fallback.schema"); + fallback.RouteHint.Kind.Should().Be(CultMeshLocalityKind.Network); + fallback.RouteHint.Description.Should().Be("fallback route"); + fallback.IdempotencyKey.Should().Be("fallback-key"); + payloadFields["value"].Should().Be("Raven"); + } + + [Test] + public void RouteRecord_FlattensAndParsesCrossRuntimeRouteKinds() + { + var record = CultMesh.RouteRecord(new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "co-located slab")); + var restored = record.ToRoute(); + var fromTypescript = CultMesh.RouteRecord("in-process", "").ToRoute( + new CultMeshRouteHint(CultMeshLocalityKind.Network, "fallback route")); + var fallback = CultMesh.RouteRecord("not-real", "").ToRoute( + new CultMeshRouteHint(CultMeshLocalityKind.Ipc, "fallback route")); + + record.Kind.Should().Be("SharedMemory"); + record.Description.Should().Be("co-located slab"); + restored.Kind.Should().Be(CultMeshLocalityKind.SharedMemory); + restored.Description.Should().Be("co-located slab"); + fromTypescript.Kind.Should().Be(CultMeshLocalityKind.InProcess); + fromTypescript.Description.Should().Be("fallback route"); + fallback.Kind.Should().Be(CultMeshLocalityKind.Ipc); + fallback.Description.Should().Be("fallback route"); + } + + [Test] + public void StateRefResolver_ComposesNamedSurfaceResolution() + { + var route = new CultMeshRouteHint(CultMeshLocalityKind.InProcess, "embedded renderer"); + var daemon = CultMesh.StateRefResolver( + "aetheria.daemon.refs", + (stateRef, context) => stateRef == "aetheria.daemon/frame/id" + ? $"{context.RuntimeId}:{context.RouteHint.Kind}:42" + : "", + new[] + { + CultMesh.ProjectionSource( + "daemon:aetheria.frame.latest.v1", + schemaId: "gamecult.aetheria.daemon_frame.v1") + }, + route); + var itemStats = CultMesh.StateRefResolver( + "aetheria.item_stats.refs", + stateRef => stateRef == "aetheria.item_stats/laser/damage" ? "12.5" : ""); + var resolver = daemon.Or(itemStats); + + resolver.Resolve( + "aetheria.daemon/frame/id", + CultMesh.QueryContextFor("unity-raven") + .Route(CultMeshLocalityKind.Network, "remote peer") + .Build()) + .Should() + .Be("unity-raven:Network:42"); + resolver.Resolve("aetheria.item_stats/laser/damage").Should().Be("12.5"); + resolver.TryResolve("missing", out var missing).Should().BeFalse(); + missing.Should().Be(""); + resolver.AsFunc()("aetheria.item_stats/laser/damage").Should().Be("12.5"); + + var diagnostic = CultMesh.DescribeStateRefResolver(resolver); + diagnostic.ResolverId.Should().Be("aetheria.daemon.refs|aetheria.item_stats.refs"); + diagnostic.RouteHint.Kind.Should().Be(CultMeshLocalityKind.InProcess); + diagnostic.Sources.Should().ContainSingle(); + diagnostic.Sources[0].SchemaId.Should().Be("gamecult.aetheria.daemon_frame.v1"); + } + + [Test] + public async Task ProjectionRecipe_NamesSourcesAndCanBecomeQuerySurface() + { + var recipe = CultMesh.ProjectionRecipe( + "aetheria.zone.objects.visible", + new[] + { + CultMesh.ProjectionSource( + "daemon:aetheria.frame.latest.v1", + schemaId: "gamecult.aetheria.daemon_frame.v1", + description: "latest daemon frame"), + CultMesh.ProjectionSource( + "daemon:aetheria.authority.policy.v1", + schemaId: "gamecult.aetheria.authority_policy.v1") + }, + (request, context) => Task.FromResult(new[] + { + $"{context.RuntimeId}:{request.MinX}:{request.MaxX}:{context.RouteHint.Kind}" + }), + new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "co-located frame slab")); + + recipe.Sources.Should().HaveCount(2); + recipe.Sources[0].SourceId.Should().Be("daemon:aetheria.frame.latest.v1"); + recipe.RouteHint.Kind.Should().Be(CultMeshLocalityKind.SharedMemory); + + var recipeDiagnostic = CultMesh.DescribeProjectionRecipe(recipe); + recipeDiagnostic.ProjectionId.Should().Be("aetheria.zone.objects.visible"); + recipeDiagnostic.RouteHint.Description.Should().Be("co-located frame slab"); + recipeDiagnostic.Sources.Should().HaveCount(2); + recipeDiagnostic.Sources[0].SchemaId.Should().Be("gamecult.aetheria.daemon_frame.v1"); + recipeDiagnostic.Sources.Should().NotBeSameAs(recipe.Sources); + + var result = await recipe.ProjectAsync( + new MeshViewportRequest(-16, 16), + CultMesh.QueryContextFor("browser-starfire") + .Route(CultMeshLocalityKind.Wasm, "browser-local projection") + .Build()); + + result.Should().Equal("browser-starfire:-16:16:Wasm"); + + var query = recipe.AsQuerySurface(); + var queryResult = await query.ExecuteAsync( + new MeshViewportRequest(-4, 4), + "unity-raven"); + + query.QueryId.Should().Be(recipe.ProjectionId); + query.Sources.Should().HaveCount(2); + query.Sources[0].SchemaId.Should().Be("gamecult.aetheria.daemon_frame.v1"); + query.RouteHint.Kind.Should().Be(CultMeshLocalityKind.SharedMemory); + queryResult.Should().Equal("unity-raven:-4:4:SharedMemory"); + + var queryDiagnostic = CultMesh.DescribeQuerySurface(query); + queryDiagnostic.QueryId.Should().Be("aetheria.zone.objects.visible"); + queryDiagnostic.RouteHint.Kind.Should().Be(CultMeshLocalityKind.SharedMemory); + queryDiagnostic.Sources.Should().HaveCount(2); + queryDiagnostic.Sources.Should().NotBeSameAs(query.Sources); + } + + [Test] + public async Task LiveFeed_DescribesAndWatchesCoherentClientSnapshots() + { + var subject = new Subject(); + var feed = CultMesh.LiveFeed( + "aetheria.rts.viewport.feed", + (request, context) => Task.FromResult( + $"{context.RuntimeId}:{request.MinX}:{request.MaxX}:{context.RouteHint.Kind}"), + (_request, context) => subject.Select(value => $"{context.RuntimeId}:{value}:{context.RouteHint.Kind}"), + new[] + { + CultMesh.ProjectionSource( + "daemon:aetheria.frame.latest.v1", + schemaId: "gamecult.aetheria.daemon_frame.v1"), + CultMesh.ProjectionSource( + "daemon:aetheria.health.latest.v1", + schemaId: "gamecult.aetheria.daemon_health.v1") + }, + new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "co-located RTS cache")); + + feed.FeedId.Should().Be("aetheria.rts.viewport.feed"); + feed.Sources.Should().HaveCount(2); + feed.Sources[0].SchemaId.Should().Be("gamecult.aetheria.daemon_frame.v1"); + feed.RouteHint.Kind.Should().Be(CultMeshLocalityKind.SharedMemory); + + var diagnostic = CultMesh.DescribeLiveFeed(feed); + diagnostic.FeedId.Should().Be("aetheria.rts.viewport.feed"); + diagnostic.RouteHint.Description.Should().Be("co-located RTS cache"); + diagnostic.Sources.Should().HaveCount(2); + diagnostic.Sources[1].SourceId.Should().Be("daemon:aetheria.health.latest.v1"); + diagnostic.Sources.Should().NotBeSameAs(feed.Sources); + + var inherited = await feed.SnapshotAsync( + new MeshViewportRequest(-32, 32), + "browser-starfire"); + + inherited.Should().Be("browser-starfire:-32:32:SharedMemory"); + + var explicitRoute = await feed.SnapshotAsync( + new MeshViewportRequest(-32, 32), + CultMesh.QueryContextFor("unity-raven") + .Route(CultMeshLocalityKind.InProcess, "embedded Verse") + .Build()); + + explicitRoute.Should().Be("unity-raven:-32:32:InProcess"); + + string observed = null!; + using var subscription = feed + .Watch(new MeshViewportRequest(-1, 1), CultMeshQueryContext.ForRuntime("browser-starfire")) + .Subscribe(value => observed = value); + + subject.OnNext("frame:42"); + observed.Should().Be("browser-starfire:frame:42:SharedMemory"); + } + + [Test] + public async Task DocumentHandle_ExposesTypedSnapshotsAndSameSchemaAliases() + { + var subject = new Subject(); + var route = new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "co-located document slab"); + var sources = new[] + { + CultMesh.ProjectionSource( + "daemon:mesh.note.current", + schemaId: "tests.mesh_note.v1") + }; + var context = CultMesh.Verse("starbridge", "unity-pilot", route).Context; + var current = new MeshNoteDocument + { + Schema = "tests.mesh_note.v1", + Text = "primary", + Revision = 1 + }; + + var handle = CultMesh.Document( + "mesh.note.current", + context, + _ => Task.FromResult(current), + _ => subject, + sources, + route); + + handle.DocumentId.Should().Be("mesh.note.current"); + handle.DocumentType.Should().Be(typeof(MeshNoteDocument)); + handle.SchemaName.Should().Be("tests.mesh_note"); + handle.SchemaVersion.Should().Be("tests.mesh_note.v1"); + handle.RouteHint.Kind.Should().Be(CultMeshLocalityKind.SharedMemory); + handle.Sources.Should().ContainSingle().Which.SourceId.Should().Be("daemon:mesh.note.current"); + + var snapshot = await handle.LatestAsync(); + snapshot.Text.Should().Be("primary"); + + var alias = handle.AsSchemaAlias(); + var aliasSnapshot = await alias.LatestAsync(); + alias.DocumentId.Should().Be(handle.DocumentId); + alias.SchemaName.Should().Be(handle.SchemaName); + alias.SchemaVersion.Should().Be(handle.SchemaVersion); + aliasSnapshot.Text.Should().Be("primary"); + aliasSnapshot.Revision.Should().Be(1); + + MeshNoteAliasDocument observed = null!; + using var subscription = alias.Watch(value => observed = value); + subject.OnNext(new MeshNoteDocument + { + Schema = "tests.mesh_note.v1", + Text = "reactive", + Revision = 2 + }); + + observed.Text.Should().Be("reactive"); + observed.Revision.Should().Be(2); + } + + [Test] + public void DocumentHandle_RejectsAliasTypesWithDifferentSchemaIdentity() + { + var handle = CultMesh.Document( + "mesh.note.current", + CultMesh.Verse("starbridge", "unity-pilot"), + _ => Task.FromResult(new MeshNoteDocument + { + Schema = "tests.mesh_note.v1", + Text = "primary", + Revision = 1 + }), + _ => new Subject()); + + Action act = () => handle.AsSchemaAlias(); + + act.Should() + .Throw() + .WithMessage("*tests.mesh_other*tests.mesh_note*"); + } + + [Test] + public void SurfaceCatalog_DescribesTypedRuntimeSurfaces() + { + var route = new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "co-located frame slab"); + var sources = new[] + { + CultMesh.ProjectionSource( + "daemon:aetheria.frame.latest.v1", + schemaId: "gamecult.aetheria.daemon_frame.v1") + }; + var query = new CultMeshQuerySurface( + "aetheria.zone.objects.visible", + (_request, _context) => Task.FromResult(Array.Empty()), + sources: sources, + routeHint: route); + var feed = CultMesh.LiveFeed( + "aetheria.rts.viewport.feed", + (_request, _context) => Task.FromResult("frame"), + sources: sources, + routeHint: route); + var operation = new CultMeshOperationHandle( + "aetheria.entity.pilot.move", + (_request, _context) => Task.FromResult(new CultMeshOperationReceipt("aetheria.entity.pilot.move", true))); + var pointer = CultMesh.StatePointer( + "aetheria.selection.current", + async () => "entity:ship:1", + () => new Subject(), + route, + sources); + var nativeView = new CultMeshNativeSliceViewDescriptor( + "aetheria.zone.render", + "gamecult.aetheria.render_body.v1", + rowCount: 128, + new[] { CultMeshNativeSliceColumn.For("position") }, + route); + + var catalog = CultMesh.DescribeSurfaceCatalog( + "gamecult.aetheria.rts.surfaces.v1", + new[] + { + CultMesh.DescribeSurface(query), + CultMesh.DescribeSurface(feed), + CultMesh.DescribeSurface(operation), + CultMesh.DescribeSurface(pointer), + CultMesh.DescribeSurface(nativeView) + }); + + catalog.CatalogId.Should().Be("gamecult.aetheria.rts.surfaces.v1"); + catalog.Surfaces.Should().HaveCount(5); + catalog.Surfaces[0].Kind.Should().Be(CultMeshSurfaceKind.Query); + catalog.Surfaces[0].SurfaceId.Should().Be("aetheria.zone.objects.visible"); + catalog.Surfaces[0].RouteHint.Kind.Should().Be(CultMeshLocalityKind.SharedMemory); + catalog.Surfaces[0].Sources.Should().HaveCount(1); + catalog.Find("aetheria.rts.viewport.feed")!.Kind.Should().Be(CultMeshSurfaceKind.LiveFeed); + catalog.Find("aetheria.entity.pilot.move")!.Kind.Should().Be(CultMeshSurfaceKind.Operation); + catalog.Find("aetheria.selection.current")!.Kind.Should().Be(CultMeshSurfaceKind.StatePointer); + catalog.Find("aetheria.selection.current")!.RouteHint.Kind.Should().Be(CultMeshLocalityKind.SharedMemory); + catalog.Find("aetheria.selection.current")!.Sources.Should().ContainSingle(); + catalog.Find("aetheria.zone.render")!.Kind.Should().Be(CultMeshSurfaceKind.NativeSliceView); + catalog.Find("aetheria.zone.render")!.RouteHint.Kind.Should().Be(CultMeshLocalityKind.SharedMemory); + catalog.Find("missing").Should().BeNull(); + catalog.FindByKind(CultMeshSurfaceKind.Operation) + .Should() + .ContainSingle() + .Which + .SurfaceId + .Should() + .Be("aetheria.entity.pilot.move"); + catalog.FindByKind(CultMeshSurfaceKind.Query).Should().ContainSingle(); + + var index = CultMesh.DescribeSurfaceCatalogIndex(catalog); + index.CatalogId.Should().Be(catalog.CatalogId); + index.Queries.Should().ContainSingle().Which.SurfaceId.Should().Be("aetheria.zone.objects.visible"); + index.LiveFeeds.Should().ContainSingle().Which.SurfaceId.Should().Be("aetheria.rts.viewport.feed"); + index.Operations.Should().ContainSingle().Which.SurfaceId.Should().Be("aetheria.entity.pilot.move"); + index.StatePointers.Should().ContainSingle().Which.SurfaceId.Should().Be("aetheria.selection.current"); + index.NativeSliceViews.Should().ContainSingle().Which.SurfaceId.Should().Be("aetheria.zone.render"); + index.ProjectionRecipes.Should().BeEmpty(); + } + + [Test] + public async Task PollingQueryWatcher_TurnsSnapshotsIntoReactiveFeed() + { + var frameId = 0; + var observed = new List(); + var feed = CultMesh.LiveFeed( + "aetheria.rts.viewport.feed", + (_request, _context) => Task.FromResult(frameId), + CultMesh.PollingQueryWatcher( + (_request, _context) => Task.FromResult(frameId), + new CultMeshPollingWatchOptions(TimeSpan.FromMilliseconds(5)))); + + using (feed + .Watch(new MeshViewportRequest(-1, 1), CultMeshQueryContext.ForRuntime("browser-starfire")) + .Subscribe(value => observed.Add(value))) + { + await Task.Delay(35); + frameId = 1; + await Task.Delay(35); + frameId = 1; + await Task.Delay(25); + frameId = 2; + await Task.Delay(35); + } + + frameId = 3; + await Task.Delay(25); + + observed.Should().Equal(0, 1, 2); + } + + [Test] + public void NativeSliceDescriptor_DescribesSharedColumnsWithoutOpeningTransport() + { + var descriptor = new CultMeshNativeSliceViewDescriptor( + "aetheria.zone.render", + "gamecult.aetheria.render_body.v1", + rowCount: 128, + new[] + { + CultMeshNativeSliceColumn.For("position"), + CultMeshNativeSliceColumn.For("velocity") + }, + new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "CultCache slab"), + nativeHandle: "cultcache-slab:aetheria-zone-render"); + + descriptor.Route.Kind.Should().Be(CultMeshLocalityKind.SharedMemory); + descriptor.Columns.Should().HaveCount(2); + descriptor.Columns[0].Name.Should().Be("position"); + descriptor.DenseRowStrideBytes.Should().Be(16); + descriptor.FindColumn("velocity")!.ElementSizeBytes.Should().Be(8); + + var diagnostic = CultMesh.DescribeNativeSliceView(descriptor); + diagnostic.ViewId.Should().Be("aetheria.zone.render"); + diagnostic.SchemaId.Should().Be("gamecult.aetheria.render_body.v1"); + diagnostic.RowCount.Should().Be(128); + diagnostic.Route.Kind.Should().Be(CultMeshLocalityKind.SharedMemory); + diagnostic.NativeHandle.Should().Be("cultcache-slab:aetheria-zone-render"); + diagnostic.DenseRowStrideBytes.Should().Be(16); + diagnostic.Columns.Should().HaveCount(2); + diagnostic.Columns.Should().NotBeSameAs(descriptor.Columns); + } + [Test] public async Task ManagedDocument_Commits_Through_MeshDatabase_And_Watches_Networked_Updates() { @@ -221,4 +1051,173 @@ public sealed class CultMeshStreamingTests [MessagePack.Key(2)] public int Health; } + + [CultDocument("tests.mesh_note", "tests.mesh_note.v1")] + [MessagePackObject(AllowPrivate = true)] + internal sealed class MeshNoteDocument + { + [Key(0)] + public string Schema { get; set; } = string.Empty; + + [Key(1)] + public string Text { get; set; } = string.Empty; + + [Key(2)] + public int Revision { get; set; } + } + + [CultDocument("tests.mesh_note", "tests.mesh_note.v1")] + [MessagePackObject(AllowPrivate = true)] + internal sealed class MeshNoteAliasDocument + { + [Key(0)] + public string Schema { get; set; } = string.Empty; + + [Key(1)] + public string Text { get; set; } = string.Empty; + + [Key(2)] + public int Revision { get; set; } + } + + [CultDocument("tests.mesh_other", "tests.mesh_other.v1")] + [MessagePackObject(AllowPrivate = true)] + internal sealed class MeshOtherDocument + { + [Key(0)] + public string Schema { get; set; } = string.Empty; + + [Key(1)] + public string Text { get; set; } = string.Empty; + } +} + +public readonly struct MeshMoveRequest +{ + public MeshMoveRequest(int entityId, float x, float y) + { + EntityId = entityId; + X = x; + Y = y; + } + + public int EntityId { get; } + public float X { get; } + public float Y { get; } +} + +public readonly struct MeshViewportRequest +{ + public MeshViewportRequest(float minX, float maxX) + { + MinX = minX; + MaxX = maxX; + } + + public float MinX { get; } + public float MaxX { get; } +} + +public readonly struct MeshVec2 +{ + public MeshVec2(float x, float y) + { + X = x; + Y = y; + } + + public float X { get; } + public float Y { get; } +} + +public sealed class MeshAetheriaDomain +{ + private readonly CultMeshVerseContext _context; + + public MeshAetheriaDomain(CultMeshVerseContext context) + { + _context = context; + } + + public MeshEntityFacade Entity(int entityId) + { + return new MeshEntityFacade(_context, entityId); + } + + public MeshZoneFacade Zone(string zoneId) + { + return new MeshZoneFacade(_context, zoneId); + } +} + +public sealed class MeshEntityFacade +{ + public MeshEntityFacade(CultMeshVerseContext context, int entityId) + { + Pilot = new MeshPilotFacade(context, entityId); + } + + public MeshPilotFacade Pilot { get; } +} + +public sealed class MeshPilotFacade +{ + private readonly int _entityId; + private readonly CultMeshBoundOperationHandle _move; + + public MeshPilotFacade(CultMeshVerseContext context, int entityId) + { + _entityId = entityId; + _move = CultMesh.BindOperation( + context, + new CultMeshOperationHandle( + "aetheria.entity.pilot.move", + (request, context) => Task.FromResult(new CultMeshOperationReceipt( + "aetheria.entity.pilot.move", + accepted: request.EntityId == _entityId && + Math.Abs(request.X) > 0 && + context.Claims.Any(claim => claim.Role == "pilot-control"), + context.RouteHint)))); + } + + public Task MoveAsync(MeshVec2 direction, string idempotencyKey) + { + return _move.InvokeAsync( + new MeshMoveRequest(_entityId, direction.X, direction.Y), + idempotencyKey); + } +} + +public sealed class MeshZoneFacade +{ + public MeshZoneFacade(CultMeshVerseContext context, string zoneId) + { + Objects = new MeshObjectsFacade(context, zoneId); + } + + public MeshObjectsFacade Objects { get; } +} + +public sealed class MeshObjectsFacade +{ + private readonly string _zoneId; + private readonly CultMeshBoundQuerySurface _visibleObjects; + + public MeshObjectsFacade(CultMeshVerseContext context, string zoneId) + { + _zoneId = zoneId; + _visibleObjects = CultMesh.BindQuery( + context, + new CultMeshQuerySurface( + "aetheria.zone.objects.visible", + (parameters, context) => Task.FromResult(new[] + { + $"{context.RuntimeId}:{_zoneId}:{parameters.MinX}:{parameters.MaxX}:{context.RouteHint.Kind}" + }))); + } + + public Task VisibleWithinAsync(MeshViewportRequest request) + { + return _visibleObjects.ExecuteAsync(request); + } }