CultLib/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs
Meta Vens 2e12e9eebb Mesh single-file helpers: delete the second reader, the older catalog layout and the lock-free replace
Deletes CultMesh's ReadSingleFileSnapshot, HasOlderCatalogLayout, ReadLegacySchemaCatalogEntry,
WriteFileAtomically and the File.Exists gate, ReadStore's entry-reader delegate, and the three tests
of the older catalog layout. This commit does not build: the Mesh read and write sites still name
the deleted reader; the next commit routes them through the CultCache single-file store.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 01:59:13 +02:00

4161 lines
164 KiB
C#

using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using GameCult.Caching;
using GameCult.Caching.MessagePack;
using GameCult.Networking;
using MessagePack;
using NUnit.Framework;
using R3;
namespace GameCult.Mesh.Tests;
public sealed class CultMeshStreamingTests
{
[Test]
public async Task TypedOperationHandle_CarriesAuthorityAndRouteContext()
{
var handle = new CultMeshOperationHandle<MeshMoveRequest, CultMeshOperationReceipt>(
"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<MeshViewportRequest, string[]>(
"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<string>();
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<string>();
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<string>();
var stored = "frame:0";
var observedContexts = new List<string>();
var pointer = CultMesh.MutableStatePointer(
"aetheria.daemon.frame.latest",
context =>
{
observedContexts.Add($"read:{context.RuntimeId}:{context.RouteHint.Kind}");
return Task.FromResult<string?>(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<string>(),
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 StateBindingRecord_PreservesEditableFieldIdentityAndAuthority()
{
var binding = CultMesh.StateBinding(
"value",
"ghostlight.session-zero.composer",
sourceId: "session-zero:draft-7",
schemaId: "session_zero.v1",
routeHint: new CultMeshRouteHint(CultMeshLocalityKind.Network, "Ghostlight provider"),
bindingName: "composer.message",
documentId: "session-zero:draft-7",
fieldPath: "channels.private.messages.pending",
valueKind: "string",
accessMode: "local-draft",
authority: "SessionZeroKernel",
writeCommand: "edit.value");
var record = CultMesh.StateBindingRecord(binding);
record.BindingName.Should().Be("composer.message");
record.FieldPath.Should().Be("channels.private.messages.pending");
record.AccessMode.Should().Be("local-draft");
var restored = record.ToBinding();
restored.BindingName.Should().Be(binding.BindingName);
restored.DocumentId.Should().Be(binding.DocumentId);
restored.FieldPath.Should().Be(binding.FieldPath);
restored.ValueKind.Should().Be(binding.ValueKind);
restored.AccessMode.Should().Be(binding.AccessMode);
restored.Authority.Should().Be(binding.Authority);
restored.WriteCommand.Should().Be(binding.WriteCommand);
}
[Test]
public void OperationBindingDescriptor_BindsUiCommandsToTypedOperations()
{
var operation = new CultMeshOperationHandle<MeshMoveRequest, CultMeshOperationReceipt>(
"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 OperationInvocationRecord_ReadsLegacyStringAndWritesStructuredRecord()
{
var legacyPayload = MessagePackSerializer.Serialize(
"aetheria.entity.pilot.move",
CultDocumentMessagePackSerialization.Options);
var restored = MessagePackSerializer.Deserialize<CultMeshOperationInvocationRecord>(
legacyPayload,
CultDocumentMessagePackSerialization.Options);
var currentPayload = MessagePackSerializer.Serialize(
restored,
CultDocumentMessagePackSerialization.Options);
var reader = new MessagePackReader(currentPayload);
restored.OperationId.Should().Be("aetheria.entity.pilot.move");
restored.SchemaId.Should().BeEmpty();
restored.RouteKind.Should().BeEmpty();
restored.IdempotencyKey.Should().BeEmpty();
reader.ReadArrayHeader().Should().Be(5);
reader.ReadString().Should().Be("aetheria.entity.pilot.move");
}
[Test]
public void RouteRecord_ReadsLegacyStringAndWritesStructuredRecord()
{
var legacyPayload = MessagePackSerializer.Serialize(
"SharedMemory",
CultDocumentMessagePackSerialization.Options);
var restored = MessagePackSerializer.Deserialize<CultMeshRouteRecord>(
legacyPayload,
CultDocumentMessagePackSerialization.Options);
var currentPayload = MessagePackSerializer.Serialize(
restored,
CultDocumentMessagePackSerialization.Options);
var reader = new MessagePackReader(currentPayload);
restored.Kind.Should().Be("SharedMemory");
restored.Description.Should().BeEmpty();
reader.ReadArrayHeader().Should().Be(2);
reader.ReadString().Should().Be("SharedMemory");
}
[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<MeshViewportRequest, string[]>(
"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<string>();
var feed = CultMesh.LiveFeed<MeshViewportRequest, string>(
"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_ExposesTypedSnapshotsAndWatches()
{
var subject = new Subject<MeshNoteDocument>();
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");
handle.Latest().Text.Should().Be("primary");
MeshNoteDocument observed = null!;
using var subscription = handle.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 async Task DocumentHandle_Field_ProjectsTypedReactiveStateWithoutRepublishingSurfaceTrees()
{
var subject = new Subject<MeshNoteDocument>();
var route = new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "co-located document slab");
var current = new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "initial",
Revision = 1
};
var handle = CultMesh.Document(
"mesh.note.current",
CultMesh.Verse("starbridge", "unity-pilot", route),
_ => Task.FromResult(current),
_ => subject,
routeHint: route);
var text = handle.Field(value => value.Text);
text.PointerId.Should().Be("mesh.note.current#Text");
text.RouteHint.Kind.Should().Be(CultMeshLocalityKind.SharedMemory);
(await text.ResolveAsync()).Should().Be("initial");
string observed = null!;
using var subscription = text.Watch().Subscribe(value => observed = value);
subject.OnNext(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "reactive",
Revision = 2
});
observed.Should().Be("reactive");
}
[Test]
public async Task DocumentCatalog_IndexesHandlesByTypeAndSchema()
{
var current = new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "catalog-primary",
Revision = 3
};
var handle = CultMesh.Document(
"mesh.note.current",
CultMesh.Verse("starbridge", "unity-pilot"),
_ => Task.FromResult(current),
_ => new Subject<MeshNoteDocument>());
var catalog = CultMesh.Documents(handle);
catalog.Documents.Should().ContainSingle().Which.Should().BeSameAs(handle);
catalog.TryGetDocument<MeshNoteDocument>(out var exact).Should().BeTrue();
exact.Should().BeSameAs(handle);
catalog.TryGetDocumentBySchema("tests.mesh_note.v1", out var byVersion).Should().BeTrue();
byVersion.Should().BeSameAs(handle);
catalog.DocumentBySchema("tests.mesh_note").Should().BeSameAs(handle);
catalog.DocumentBySchema(CultDocumentRegistry.Shared.GetRequired<MeshNoteDocument>().SchemaId)
.Should()
.BeSameAs(handle);
catalog.TryGetDocument<MeshOtherDocument>(out _).Should().BeFalse();
var snapshot = await catalog.LatestAsync<MeshNoteDocument>();
snapshot.Text.Should().Be("catalog-primary");
snapshot.Revision.Should().Be(3);
catalog.Latest<MeshNoteDocument>().Text.Should().Be("catalog-primary");
}
[Test]
public async Task DocumentHelpers_ProjectLatestSnapshotsFromMultipleHandles()
{
var primary = CultMesh.Document(
"mesh.note.primary",
CultMesh.Verse("starbridge", "unity-pilot"),
_ => Task.FromResult(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "alpha",
Revision = 3
}),
_ => new Subject<MeshNoteDocument>());
var secondary = CultMesh.Document(
"mesh.note.secondary",
CultMesh.Verse("starbridge", "unity-pilot"),
_ => Task.FromResult(new MeshOtherDocument
{
Schema = "tests.mesh_other.v1",
Text = "beta"
}),
_ => new Subject<MeshOtherDocument>());
var projected = await CultMesh.LatestAsync(
primary,
secondary,
(first, second) => $"{first.Text}:{first.Revision}:{second.Text}");
projected.Should().Be("alpha:3:beta");
}
[Test]
public async Task DocumentHandle_ReadsWatchesAndReplacesCultCacheRecords()
{
var cache = new CultCache();
var key = new CultRecordKey("mesh-note:cache");
await cache.UpsertAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "cache-initial",
Revision = 1
}, new CultRecordHandle<MeshNoteDocument>(key));
var handle = CultMesh.Document<MeshNoteDocument>(
cache,
key,
CultMesh.Verse("starbridge", "unity-pilot"));
handle.CanReplace.Should().BeTrue();
handle.CanSubmitPrediction.Should().BeFalse();
handle.DocumentId.Should().Be(key.Value);
handle.Sources.Should().ContainSingle().Which.SchemaId.Should().Be(handle.SchemaId);
var snapshot = await handle.LatestAsync();
snapshot.Text.Should().Be("cache-initial");
MeshNoteDocument observed = null!;
using var subscription = handle.Watch(value => observed = value);
await handle.ReplaceAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "cache-replaced",
Revision = 2
});
cache.Get<MeshNoteDocument>(key)!.Text.Should().Be("cache-replaced");
observed.Text.Should().Be("cache-replaced");
observed.Revision.Should().Be(2);
var catalog = CultMesh.Documents(handle);
catalog.CanReplace<MeshNoteDocument>().Should().BeTrue();
await catalog.ReplaceAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "cache-catalog-replaced",
Revision = 3
});
cache.Get<MeshNoteDocument>(key)!.Text.Should().Be("cache-catalog-replaced");
Func<Task> act = () => handle.SubmitPredictionAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "cache-predicted",
Revision = 4
});
await act.Should().ThrowAsync<NotSupportedException>();
}
// A database serves only what its shards own. A schema has one CLR type (retire-schema-alias), so there is no second type
// that could reach a record around the shard that owns its schema.
[TestCase("tests.mesh_note.v1", true)]
[TestCase("tests.unrelated.v1", false)]
public async Task DocumentHandle_ReadsARecordOnlyWhenAShardOwnsItsSchema(string ownedSchemaId, bool served)
{
var cache = new CultCache();
var key = new CultRecordKey("note:one");
await cache.UpsertAsync(new MeshNoteDocument { Schema = "tests.mesh_note.v1", Text = "cached" }, new CultRecordHandle<MeshNoteDocument>(key));
var database = new CultNetDatabase(cache, new CultNetDatabaseOptions
{
RuntimeId = "reader",
Shards = new[] { new CultNetShardDescriptor("notes", "reader", epoch: 1, isPrimary: true, schemaIds: new[] { ownedSchemaId }) }
});
var handle = CultMesh.Document<MeshNoteDocument>(database, key, CultMesh.Verse("starbridge", "reader"));
if (served)
{
(await handle.LatestAsync()).Text.Should().Be("cached");
}
else
{
await FluentActions.Awaiting(() => handle.LatestAsync()).Should().ThrowAsync<KeyNotFoundException>();
}
}
[Test]
public async Task DocumentHandle_SubmitsPredictionsThroughCultNetDatabase()
{
var cache = new CultCache();
var schemaId = CultDocumentRegistry.Shared.GetRequired<MeshNoteDocument>().SchemaId;
var database = new CultNetDatabase(cache, new CultNetDatabaseOptions
{
RuntimeId = "pilot-a",
Shards = new[]
{
new CultNetShardDescriptor(
"pilot-inputs",
"server",
epoch: 1,
isPrimary: false,
schemaIds: new[] { schemaId },
keyPrefix: "input:")
},
ClientAuthorityScopes = new[]
{
new CultNetClientAuthorityScope(
"pilot-a",
schemaIds: new[] { schemaId },
keyPrefix: "input:pilot-a")
}
});
var key = new CultRecordKey("input:pilot-a:thermal");
var handle = CultMesh.Document<MeshNoteDocument>(
database,
key,
CultMesh.Verse("starbridge", "pilot-a"));
handle.CanReplace.Should().BeFalse();
handle.CanSubmitPrediction.Should().BeTrue();
var catalog = CultMesh.Documents(handle);
catalog.CanSubmitPrediction<MeshNoteDocument>().Should().BeTrue();
var predictionWriter = catalog.PredictionWriter<MeshNoteDocument>();
predictionWriter.Kind.Should().Be(CultMeshDocumentWriteKind.Prediction);
MeshNoteDocument observed = null!;
using var subscription = handle.Watch(value => observed = value);
await predictionWriter.WriteAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "predicted-thermal",
Revision = 5
});
cache.Get<MeshNoteDocument>(key)!.Text.Should().Be("predicted-thermal");
observed.Text.Should().Be("predicted-thermal");
observed.Revision.Should().Be(5);
(await handle.LatestAsync()).Text.Should().Be("predicted-thermal");
var updated = await handle.PredictionWriter().UpdateAsync(value => new MeshNoteDocument
{
Schema = value.Schema,
Text = "updated-as-prediction",
Revision = value.Revision + 1
});
updated.Text.Should().Be("updated-as-prediction");
cache.Get<MeshNoteDocument>(key)!.Revision.Should().Be(6);
}
[Test]
public async Task ReactiveDocument_CoalescesLocalEditsIntoOnePrediction()
{
var subject = new Subject<MeshNoteDocument>();
var current = new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "initial",
Revision = 1
};
var predictions = new List<MeshNoteDocument>();
var handle = CultMesh.Document(
"mesh.note.reactive",
CultMesh.Verse("starbridge", "pilot-a"),
_ => Task.FromResult(current),
_ => subject,
value =>
{
current = value;
subject.OnNext(value);
return Task.CompletedTask;
},
value =>
{
predictions.Add(value);
current = value;
subject.OnNext(value);
return Task.CompletedTask;
});
var catalog = CultMesh.Documents(handle);
using var reactive = await catalog.PredictionWriter<MeshNoteDocument>().ReactiveAsync(
new CultMeshReactiveDocumentOptions
{
FlushDelay = TimeSpan.FromMinutes(1)
});
reactive.Snapshot.Text.Should().Be("initial");
reactive.Update(document =>
{
document.Text = "first-local-edit";
document.Revision++;
});
reactive.Update(document =>
{
document.Text = "second-local-edit";
document.Revision++;
});
predictions.Should().BeEmpty();
await reactive.FlushAsync();
predictions.Should().ContainSingle();
predictions[0].Text.Should().Be("second-local-edit");
predictions[0].Revision.Should().Be(3);
current.Text.Should().Be("second-local-edit");
}
[Test]
public async Task ReactiveDocument_TracksCanonicalSnapshotDuringDirtyPrediction()
{
var subject = new Subject<MeshNoteDocument>();
var current = new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "initial",
Revision = 1
};
var handle = CultMesh.Document(
"mesh.note.reconciliation",
CultMesh.Verse("starbridge", "pilot-a"),
_ => Task.FromResult(current),
_ => subject,
value =>
{
current = value;
subject.OnNext(value);
return Task.CompletedTask;
});
using var reactive = await handle.AuthoritativeWriter().ReactiveAsync(
new CultMeshReactiveDocumentOptions
{
FlushDelay = TimeSpan.FromMinutes(1)
});
reactive.Update(document =>
{
document.Text = "local-prediction";
document.Revision = 2;
});
subject.OnNext(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "canonical-correction",
Revision = 7
});
reactive.Snapshot.Text.Should().Be("local-prediction");
reactive.Reconciliation.Should().NotBeNull();
reactive.Reconciliation!.Version.Should().Be(1);
reactive.Reconciliation!.Canonical.Text.Should().Be("canonical-correction");
reactive.Reconciliation.Predicted.Text.Should().Be("local-prediction");
reactive.Reconciliation.Delta.Should().ContainKey(nameof(MeshNoteDocument.Text));
reactive.Reconciliation.Delta[nameof(MeshNoteDocument.Text)].Should().Be("local-prediction");
reactive.Reconciliation.Delta.Should().ContainKey(nameof(MeshNoteDocument.Revision));
reactive.Reconciliation.Delta[nameof(MeshNoteDocument.Revision)].Should().Be(-5.0);
}
[Test]
public async Task ReactiveDocument_AutoFlushesOnceAfterDebounceWindow()
{
var subject = new Subject<MeshNoteDocument>();
var current = new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "initial",
Revision = 1
};
var predictions = new List<MeshNoteDocument>();
var handle = CultMesh.Document(
"mesh.note.reactive.debounce",
CultMesh.Verse("starbridge", "pilot-a"),
_ => Task.FromResult(current),
_ => subject,
value =>
{
current = value;
subject.OnNext(value);
return Task.CompletedTask;
},
value =>
{
predictions.Add(value);
current = value;
subject.OnNext(value);
return Task.CompletedTask;
});
using var reactive = await handle.PredictionWriter().ReactiveAsync(
new CultMeshReactiveDocumentOptions
{
FlushDelay = TimeSpan.FromMilliseconds(25)
});
reactive.Update(document =>
{
document.Text = "coalesced-one";
document.Revision++;
});
reactive.Update(document =>
{
document.Text = "coalesced-two";
document.Revision++;
});
await WaitForAsync(() => predictions.Count == 1);
predictions[0].Text.Should().Be("coalesced-two");
predictions[0].Revision.Should().Be(3);
reactive.IsDirty.Should().BeFalse();
}
[Test]
public async Task ReactiveDocument_CoalescesExplicitMemberUpdates()
{
var subject = new Subject<MeshNoteDocument>();
var current = new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "initial",
Revision = 1
};
var predictions = new List<MeshNoteDocument>();
var handle = CultMesh.Document(
"mesh.note.reactive.direct-members",
CultMesh.Verse("starbridge", "pilot-a"),
_ => Task.FromResult(current),
_ => subject,
value =>
{
current = value;
subject.OnNext(value);
return Task.CompletedTask;
},
value =>
{
predictions.Add(value);
current = value;
subject.OnNext(value);
return Task.CompletedTask;
});
using var reactive = await handle.PredictionWriter().ReactiveAsync(
new CultMeshReactiveDocumentOptions
{
FlushDelay = TimeSpan.FromMilliseconds(25)
});
reactive.Update(document => document.Text = "explicit-member-one");
reactive.Update(document =>
{
document.Text = "explicit-member-two";
document.Revision = 2;
});
await WaitForAsync(() => predictions.Count == 1);
predictions[0].Text.Should().Be("explicit-member-two");
predictions[0].Revision.Should().Be(2);
current.Text.Should().Be("explicit-member-two");
reactive.IsDirty.Should().BeFalse();
}
[Test]
public async Task ReactiveDocument_QueuesExplicitEditsWhileFlushIsInFlight()
{
var subject = new Subject<MeshNoteDocument>();
var current = new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "initial",
Revision = 1
};
var firstPredictionStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var releaseFirstPrediction = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var predictions = new List<MeshNoteDocument>();
var handle = CultMesh.Document(
"mesh.note.reactive.in-flight",
CultMesh.Verse("starbridge", "pilot-a"),
_ => Task.FromResult(current),
_ => subject,
value =>
{
current = value;
subject.OnNext(value);
return Task.CompletedTask;
},
async value =>
{
predictions.Add(value);
if (predictions.Count == 1)
{
firstPredictionStarted.SetResult();
await releaseFirstPrediction.Task.ConfigureAwait(false);
}
current = value;
subject.OnNext(value);
});
using var reactive = await handle.PredictionWriter().ReactiveAsync(
new CultMeshReactiveDocumentOptions
{
FlushDelay = TimeSpan.FromMinutes(1)
});
reactive.Update(document =>
{
document.Text = "first-in-flight";
document.Revision = 2;
});
var flushTask = reactive.FlushAsync();
await firstPredictionStarted.Task;
reactive.Update(document =>
{
document.Text = "second-after-flight-started";
document.Revision = 3;
});
releaseFirstPrediction.SetResult();
await flushTask;
predictions.Should().HaveCount(2);
predictions.Select(document => document.Text).Should().Equal(
"first-in-flight",
"second-after-flight-started");
current.Text.Should().Be("second-after-flight-started");
current.Revision.Should().Be(3);
reactive.IsDirty.Should().BeFalse();
}
[Test]
public async Task ReactiveDocument_UsesReplacementWhenPredictionIsUnavailable()
{
var subject = new Subject<MeshNoteDocument>();
var current = new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "initial",
Revision = 1
};
var replacements = new List<MeshNoteDocument>();
var handle = CultMesh.Document(
"mesh.note.reactive.replace",
CultMesh.Verse("starbridge", "daemon"),
_ => Task.FromResult(current),
_ => subject,
value =>
{
replacements.Add(value);
current = value;
subject.OnNext(value);
return Task.CompletedTask;
});
using var reactive = await handle.AuthoritativeWriter().ReactiveAsync(
new CultMeshReactiveDocumentOptions
{
FlushDelay = TimeSpan.FromMinutes(1)
});
reactive.Update(document =>
{
document.Text = "authoritative-replace";
document.Revision = 10;
});
await reactive.FlushAsync();
replacements.Should().ContainSingle();
replacements[0].Text.Should().Be("authoritative-replace");
current.Revision.Should().Be(10);
}
[Test]
public void ReactiveDocument_ReadOnlyHandleRejectsWriterSelection()
{
var current = new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "initial",
Revision = 1
};
var handle = CultMesh.Document(
"mesh.note.reactive.readonly",
CultMesh.Verse("starbridge", "observer"),
_ => Task.FromResult(current),
_ => new Subject<MeshNoteDocument>());
Action authoritative = () => handle.AuthoritativeWriter();
Action prediction = () => handle.PredictionWriter();
authoritative.Should().Throw<NotSupportedException>();
prediction.Should().Throw<NotSupportedException>();
}
[Test]
public async Task ObservedDocument_WatchesWithoutWriteCapabilityOrIdleTimer()
{
var subject = new Subject<MeshNoteDocument>();
var current = new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "initial",
Revision = 1
};
var handle = CultMesh.Document(
"mesh.note.observed.readonly",
CultMesh.Verse("starbridge", "observer"),
_ => Task.FromResult(current),
_ => subject);
using var observed = await handle.ObserveAsync();
observed.Current.Text.Should().Be("initial");
typeof(CultMeshObservedDocument<MeshNoteDocument>)
.GetFields(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)
.Should()
.NotContain(field => field.FieldType == typeof(System.Threading.Timer));
subject.OnNext(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "canonical-update",
Revision = 2
});
observed.Current.Text.Should().Be("canonical-update");
observed.Current.Revision.Should().Be(2);
}
// The older of two racing writes is parked in an earlier observer until the newer one has been applied.
[Test]
public async Task WatchRecordIgnoresStaleSequence()
{
var cache = new CultCache();
var key = new CultRecordKey("mesh-note:stale-sequence");
var handle = new CultRecordHandle<MeshNoteDocument>(key);
MeshNoteDocument Note(string text, int revision) =>
new() { Schema = "tests.mesh_note.v1", Text = text, Revision = revision };
await cache.UpsertAsync(Note("initial", 1), handle);
using var release = new System.Threading.ManualResetEventSlim();
using var parked = new System.Threading.ManualResetEventSlim();
using var park = cache.WatchRecord<MeshNoteDocument>(key).Subscribe(change =>
{
if (change.Document?.Revision != 2) return;
parked.Set();
release.Wait(TimeSpan.FromSeconds(30));
});
using var observed = await CultMesh.Document<MeshNoteDocument>(cache, key).ObserveAsync();
var older = Task.Run(() => cache.UpsertAsync(Note("older", 2), handle));
parked.Wait(TimeSpan.FromSeconds(5)).Should().BeTrue();
var newer = Task.Run(() => cache.UpsertAsync(Note("newer", 3), handle));
var newerFinished = newer.Wait(TimeSpan.FromSeconds(10));
release.Set();
newerFinished.Should().BeTrue("a writer waited on another thread's delivery");
older.Wait(TimeSpan.FromSeconds(10)).Should().BeTrue();
observed.Current.Text.Should().Be("newer");
cache.Dispose();
}
// A batch parks in an observer of another key before publishing its (older) target-key change; the mirror is
// created after a newer write landed, then the straggler is delivered.
[TestCase(false)]
[TestCase(true)]
public async Task ObserveAfterNewerSnapshotIgnoresStraggler(bool reactive)
{
var cache = new CultCache();
var key = new CultRecordKey("mesh-note:straggler");
var other = new CultRecordKey("mesh-note:straggler-other");
await cache.UpsertAsync(Note("initial", 1), new CultRecordHandle<MeshNoteDocument>(key));
using var release = new ManualResetEventSlim();
using var parked = new ManualResetEventSlim();
using var park = cache.WatchRecord<MeshNoteDocument>(other).Subscribe(_ =>
{
parked.Set();
release.Wait(TimeSpan.FromSeconds(30));
});
var batch = Task.Run(() => cache.Commit(stage =>
{
stage.Upsert(Note("other", 1), new CultRecordHandle<MeshNoteDocument>(other));
stage.Upsert(Note("older", 2), new CultRecordHandle<MeshNoteDocument>(key));
}));
parked.Wait(TimeSpan.FromSeconds(5)).Should().BeTrue();
await cache.UpsertAsync(Note("newer", 3), new CultRecordHandle<MeshNoteDocument>(key));
var (mirror, text, _) = await Mirror(cache, key, reactive);
release.Set();
batch.Wait(TimeSpan.FromSeconds(10)).Should().BeTrue();
text().Should().Be("newer");
mirror.Dispose();
cache.Dispose();
}
// The mirror's own delivery of the newer write is parked too, so only the refresh saw it before the straggler.
[TestCase(false)]
[TestCase(true)]
public async Task RefreshIgnoresStragglerOlderThanSnapshot(bool reactive)
{
var cache = new CultCache();
var key = new CultRecordKey("mesh-note:refresh-straggler");
var other = new CultRecordKey("mesh-note:refresh-straggler-other");
await cache.UpsertAsync(Note("initial", 1), new CultRecordHandle<MeshNoteDocument>(key));
using var releaseOlder = new ManualResetEventSlim();
using var olderParked = new ManualResetEventSlim();
using var releaseNewer = new ManualResetEventSlim();
using var newerParked = new ManualResetEventSlim();
using var parkOlder = cache.WatchRecord<MeshNoteDocument>(other).Subscribe(_ =>
{
olderParked.Set();
releaseOlder.Wait(TimeSpan.FromSeconds(30));
});
using var parkNewer = cache.WatchRecord<MeshNoteDocument>(key).Subscribe(change =>
{
if (change.Document?.Text != "newer") return;
newerParked.Set();
releaseNewer.Wait(TimeSpan.FromSeconds(30));
});
var (mirror, text, refresh) = await Mirror(cache, key, reactive);
var batch = Task.Run(() => cache.Commit(stage =>
{
stage.Upsert(Note("other", 1), new CultRecordHandle<MeshNoteDocument>(other));
stage.Upsert(Note("older", 2), new CultRecordHandle<MeshNoteDocument>(key));
}));
olderParked.Wait(TimeSpan.FromSeconds(5)).Should().BeTrue();
var newer = Task.Run(() => cache.UpsertAsync(Note("newer", 3), new CultRecordHandle<MeshNoteDocument>(key)));
newerParked.Wait(TimeSpan.FromSeconds(5)).Should().BeTrue();
await refresh();
releaseOlder.Set();
batch.Wait(TimeSpan.FromSeconds(10)).Should().BeTrue();
text().Should().Be("newer");
releaseNewer.Set();
newer.Wait(TimeSpan.FromSeconds(10)).Should().BeTrue();
text().Should().Be("newer");
mirror.Dispose();
cache.Dispose();
}
private static MeshNoteDocument Note(string text, int revision) =>
new() { Schema = "tests.mesh_note.v1", Text = text, Revision = revision };
private static async Task<(IDisposable Mirror, Func<string> Text, Func<Task> Refresh)> Mirror(
CultCache cache, CultRecordKey key, bool reactive)
{
var handle = CultMesh.Document<MeshNoteDocument>(cache, key);
if (reactive)
{
var mirror = await handle.AuthoritativeWriter().ReactiveAsync();
return (mirror, () => mirror.Snapshot.Text, () => mirror.RefreshAsync());
}
var observed = await handle.ObserveAsync();
return (observed, () => observed.Current.Text, () => observed.RefreshAsync());
}
[Test]
public async Task ReactiveDocument_DisposeSuppressesScheduledFlush()
{
var subject = new Subject<MeshNoteDocument>();
var current = new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "initial",
Revision = 1
};
var predictions = new List<MeshNoteDocument>();
var handle = CultMesh.Document(
"mesh.note.reactive.dispose",
CultMesh.Verse("starbridge", "pilot-a"),
_ => Task.FromResult(current),
_ => subject,
value =>
{
current = value;
subject.OnNext(value);
return Task.CompletedTask;
},
value =>
{
predictions.Add(value);
current = value;
subject.OnNext(value);
return Task.CompletedTask;
});
var reactive = await handle.PredictionWriter().ReactiveAsync(
new CultMeshReactiveDocumentOptions
{
FlushDelay = TimeSpan.FromMilliseconds(25)
});
reactive.Update(document => document.Text = "disposed-before-flush");
reactive.Dispose();
await Task.Delay(100);
predictions.Should().BeEmpty();
}
[Test]
public async Task ReactiveDocument_CanAdoptCanonicalSnapshotWhileDirty()
{
var subject = new Subject<MeshNoteDocument>();
var current = new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "initial",
Revision = 1
};
var handle = CultMesh.Document(
"mesh.note.reactive.adopt-canonical",
CultMesh.Verse("starbridge", "pilot-a"),
_ => Task.FromResult(current),
_ => subject,
value =>
{
current = value;
subject.OnNext(value);
return Task.CompletedTask;
});
using var reactive = await handle.AuthoritativeWriter().ReactiveAsync(
new CultMeshReactiveDocumentOptions
{
FlushDelay = TimeSpan.FromMinutes(1),
ReplaceDirtyCurrentOnCanonicalSnapshot = true
});
reactive.Update(document => document.Text = "local-prediction");
subject.OnNext(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "canonical-wins",
Revision = 9
});
reactive.Snapshot.Text.Should().Be("canonical-wins");
reactive.Snapshot.Revision.Should().Be(9);
reactive.Reconciliation.Should().BeNull();
}
[Test]
public async Task ReactiveDocument_RefreshClearsDirtyLocalState()
{
var current = new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "initial",
Revision = 1
};
var replacements = new List<MeshNoteDocument>();
var handle = CultMesh.Document(
"mesh.note.reactive.refresh",
CultMesh.Verse("starbridge", "pilot-a"),
_ => Task.FromResult(current),
_ => new Subject<MeshNoteDocument>(),
value =>
{
replacements.Add(value);
current = value;
return Task.CompletedTask;
});
using var reactive = await handle.AuthoritativeWriter().ReactiveAsync(
new CultMeshReactiveDocumentOptions
{
FlushDelay = TimeSpan.FromMinutes(1)
});
reactive.Update(document => document.Text = "local-dirty");
current = new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "fresh-canonical",
Revision = 5
};
await reactive.RefreshAsync();
await reactive.FlushAsync();
reactive.Snapshot.Text.Should().Be("fresh-canonical");
reactive.IsDirty.Should().BeFalse();
replacements.Should().BeEmpty();
}
[Test]
public async Task ReactiveDocument_SyncsAcrossRuntimeHandles()
{
var cache = new CultCache();
var schemaId = CultDocumentRegistry.Shared.GetRequired<MeshNoteDocument>().SchemaId;
var database = new CultNetDatabase(cache, new CultNetDatabaseOptions
{
RuntimeId = "pilot-a",
Shards = new[]
{
new CultNetShardDescriptor(
"pilot-inputs",
"server",
epoch: 1,
isPrimary: false,
schemaIds: new[] { schemaId },
keyPrefix: "input:")
},
ClientAuthorityScopes = new[]
{
new CultNetClientAuthorityScope(
"pilot-a",
schemaIds: new[] { schemaId },
keyPrefix: "input:pilot-a")
}
});
var key = new CultRecordKey("input:pilot-a:reactive");
await cache.UpsertAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "initial",
Revision = 1
}, new CultRecordHandle<MeshNoteDocument>(key));
var pilotHandle = CultMesh.Document<MeshNoteDocument>(
database,
key,
CultMesh.Verse("starbridge", "pilot-a"));
var commanderHandle = CultMesh.Document<MeshNoteDocument>(
database,
key,
CultMesh.Verse("starbridge", "commander-rts"));
var observedByCommander = new List<MeshNoteDocument>();
using var commanderSubscription = commanderHandle.Watch(observedByCommander.Add);
using var pilotReactive = await CultMesh
.Documents(pilotHandle)
.PredictionWriter<MeshNoteDocument>()
.ReactiveAsync(
new CultMeshReactiveDocumentOptions
{
FlushDelay = TimeSpan.FromMinutes(1)
});
pilotReactive.Update(document =>
{
document.Text = "pilot-predicted";
document.Revision = 2;
});
await pilotReactive.FlushAsync();
await WaitForAsync(() => observedByCommander.Any(document => document.Text == "pilot-predicted"));
commanderHandle.Context.RuntimeId.Should().Be("commander-rts");
observedByCommander.Last().Revision.Should().Be(2);
(await commanderHandle.LatestAsync()).Text.Should().Be("pilot-predicted");
}
[Test]
public async Task ReactiveDocument_TwoRuntimeManagedDocumentsSyncThroughDatabase()
{
var cache = new CultCache();
var schemaId = CultDocumentRegistry.Shared.GetRequired<MeshNoteDocument>().SchemaId;
var database = new CultNetDatabase(cache, new CultNetDatabaseOptions
{
RuntimeId = "pilot-a",
Shards = new[]
{
new CultNetShardDescriptor(
"pilot-inputs",
"server",
epoch: 1,
isPrimary: false,
schemaIds: new[] { schemaId },
keyPrefix: "input:")
},
ClientAuthorityScopes = new[]
{
new CultNetClientAuthorityScope(
"pilot-a",
schemaIds: new[] { schemaId },
keyPrefix: "input:pilot-a")
}
});
var key = new CultRecordKey("input:pilot-a:co-op");
await cache.UpsertAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "initial",
Revision = 1
}, new CultRecordHandle<MeshNoteDocument>(key));
var pilotHandle = CultMesh.Document<MeshNoteDocument>(
database,
key,
CultMesh.Verse("starbridge", "pilot-a"));
var commanderHandle = CultMesh.Document<MeshNoteDocument>(
database,
key,
CultMesh.Verse("starbridge", "commander-rts"));
MeshNoteDocument commanderSnapshot = null!;
using var commanderSubscription = commanderHandle.Watch(value => commanderSnapshot = value);
using var pilotReactive = await pilotHandle
.PredictionWriter()
.ReactiveAsync(new CultMeshReactiveDocumentOptions
{
FlushDelay = TimeSpan.FromMinutes(1)
});
pilotReactive.Update(document =>
{
document.Text = "pilot-first-touch";
document.Revision++;
});
pilotReactive.Update(document =>
{
document.Text = "pilot-coalesced-order";
document.Revision++;
});
await pilotReactive.FlushAsync();
await WaitForAsync(() => commanderSnapshot?.Text == "pilot-coalesced-order");
pilotReactive.IsDirty.Should().BeFalse();
commanderHandle.Context.RuntimeId.Should().Be("commander-rts");
commanderSnapshot.Revision.Should().Be(3);
cache.Get<MeshNoteDocument>(key)!.Text.Should().Be("pilot-coalesced-order");
}
[Test]
public async Task NodeDocument_OpensExplicitPredictionWriter()
{
var schemaId = CultDocumentRegistry.Shared.GetRequired<MeshNoteDocument>().SchemaId;
using var temp = new TemporaryDirectory();
using var node = await CultMesh.CreateNodeAsync(
Path.Combine(temp.Path, "node-reactive-alias.ccmp"),
new CultMeshNodeOptions
{
StartServer = false,
DatabaseOptions = new CultNetDatabaseOptions
{
RuntimeId = "pilot-a",
Shards = new[]
{
new CultNetShardDescriptor(
"pilot-inputs",
"server",
epoch: 1,
isPrimary: false,
schemaIds: new[] { schemaId },
keyPrefix: "input:")
},
ClientAuthorityScopes = new[]
{
new CultNetClientAuthorityScope(
"pilot-a",
schemaIds: new[] { schemaId },
keyPrefix: "input:pilot-a")
}
}
});
var key = new CultRecordKey("input:pilot-a:node-reactive");
await node.Cache.UpsertAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "node-initial",
Revision = 1
}, new CultRecordHandle<MeshNoteDocument>(key));
using var pilotReactive = await node
.Document<MeshNoteDocument>(key, CultMesh.Verse("starbridge", "pilot-a"))
.PredictionWriter()
.ReactiveAsync(new CultMeshReactiveDocumentOptions
{
FlushDelay = TimeSpan.FromMinutes(1)
});
var commanderHandle = node.Document<MeshNoteDocument>(
key.Value,
CultMesh.Verse("starbridge", "commander-rts"));
var observedByCommander = new List<MeshNoteDocument>();
using var commanderSubscription = commanderHandle.Watch(observedByCommander.Add);
pilotReactive.Update(document =>
{
document.Text = "node-one-call-prediction";
document.Revision = 2;
});
await pilotReactive.FlushAsync();
await WaitForAsync(() => observedByCommander.Any(document => document.Text == "node-one-call-prediction"));
pilotReactive.Document.DocumentId.Should().Be(key.Value);
pilotReactive.Document.Context.RuntimeId.Should().Be("pilot-a");
node.Cache.Get<MeshNoteDocument>(key)!.Text.Should().Be("node-one-call-prediction");
(await commanderHandle.LatestAsync()).Revision.Should().Be(2);
}
[Test]
public async Task SyncedSnapshotEndpointObservesCanonicalCorrectionAcrossRuntimeBoundary()
{
var sourceCache = new CultCache();
var registry = new CultNetDocumentRegistry(CultDocumentRegistry.Shared);
registry.Register(CultNetDocumentBinding.ForDocument<MeshNoteDocument>(sourceCache.Registry));
var key = new CultRecordKey("mesh-note:remote-correction");
await sourceCache.UpsertAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "initial",
Revision = 1
}, new CultRecordHandle<MeshNoteDocument>(key));
var endpoint = "cultnet://remote-correction.test:3080";
var surface = CultMesh.SnapshotEndpoint(
endpoint,
new CultMeshSnapshotEndpointOptions
{
Context = CultMesh.Verse("starbridge", "unity-pilot").Context,
DocumentRegistry = registry,
PollInterval = TimeSpan.FromMilliseconds(10),
Request = new CultMeshSnapshotRequestOptions
{
CreateClient = () => new MeshSnapshotSchemaClient(request =>
registry.CreateRawSnapshotResponse(sourceCache, request.MessageId, request))
}
});
using var temp = new TemporaryDirectory();
using var node = await CultMesh.CreateNodeAsync(
Path.Combine(temp.Path, "pilot-runtime.ccmp"),
new CultMeshNodeOptions
{
StartServer = false,
CacheOptions = new CultCacheOpenOptions
{
Registry = sourceCache.Registry
},
DatabaseOptions = new CultNetDatabaseOptions
{
DocumentRegistry = registry
}
});
var handle = surface.SyncTo(node).Document<MeshNoteDocument>(key.Value);
MeshNoteDocument observed = null!;
using var subscription = handle.Watch(value => observed = value);
await sourceCache.UpsertAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "remote-authority-correction",
Revision = 7
}, new CultRecordHandle<MeshNoteDocument>(key));
await WaitForAsync(() => observed?.Text == "remote-authority-correction");
observed.Revision.Should().Be(7);
node.Cache.Get<MeshNoteDocument>(key)!.Text.Should().Be("remote-authority-correction");
}
[Test]
public async Task ReactiveDocument_FreshRuntimeReconstructsFromSyncedSnapshotEndpoint()
{
var sourceCache = new CultCache();
var registry = new CultNetDocumentRegistry(CultDocumentRegistry.Shared);
registry.Register(CultNetDocumentBinding.ForDocument<MeshNoteDocument>(sourceCache.Registry));
var key = new CultRecordKey("mesh-note:runtime-reconnect");
await sourceCache.UpsertAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "before-crash",
Revision = 1
}, new CultRecordHandle<MeshNoteDocument>(key));
var endpoint = "cultnet://runtime-reconnect.test:3081";
var surface = CultMesh.SnapshotEndpoint(
endpoint,
new CultMeshSnapshotEndpointOptions
{
Context = CultMesh.Verse("starbridge", "unity-reconnected").Context,
DocumentRegistry = registry,
Request = new CultMeshSnapshotRequestOptions
{
CreateClient = () => new MeshSnapshotSchemaClient(request =>
registry.CreateRawSnapshotResponse(sourceCache, request.MessageId, request))
}
});
using var temp = new TemporaryDirectory();
using (var crashedRuntime = await CultMesh.CreateNodeAsync(
Path.Combine(temp.Path, "crashed-runtime.ccmp"),
new CultMeshNodeOptions
{
StartServer = false,
CacheOptions = new CultCacheOpenOptions
{
Registry = sourceCache.Registry
},
DatabaseOptions = new CultNetDatabaseOptions
{
DocumentRegistry = registry
}
}))
{
var stale = await surface.SyncTo(crashedRuntime).Document<MeshNoteDocument>(key.Value).LatestAsync();
stale.Text.Should().Be("before-crash");
}
await sourceCache.UpsertAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "after-reboot-authority",
Revision = 4
}, new CultRecordHandle<MeshNoteDocument>(key));
using var freshRuntime = await CultMesh.CreateNodeAsync(
Path.Combine(temp.Path, "fresh-runtime.ccmp"),
new CultMeshNodeOptions
{
StartServer = false,
CacheOptions = new CultCacheOpenOptions
{
Registry = sourceCache.Registry
},
DatabaseOptions = new CultNetDatabaseOptions
{
DocumentRegistry = registry
}
});
var reconstructed = await surface
.SyncTo(freshRuntime)
.Documents(CultMesh.SnapshotDocument<MeshNoteDocument>(key.Value))
.LatestAsync<MeshNoteDocument>();
reconstructed.Text.Should().Be("after-reboot-authority");
reconstructed.Revision.Should().Be(4);
freshRuntime.Cache.Get<MeshNoteDocument>(key)!.Text.Should().Be("after-reboot-authority");
}
[Test]
public async Task ReactiveDocument_ReadOnlySnapshotEndpointRefreshRecoversAfterRejectedLocalEdit()
{
var sourceCache = new CultCache();
var registry = new CultNetDocumentRegistry(CultDocumentRegistry.Shared);
registry.Register(CultNetDocumentBinding.ForDocument<MeshNoteDocument>(sourceCache.Registry));
var key = new CultRecordKey("mesh-note:readonly-refresh");
await sourceCache.UpsertAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "initial",
Revision = 1
}, new CultRecordHandle<MeshNoteDocument>(key));
var endpoint = "cultnet://readonly-refresh.test:3082";
var surface = CultMesh.SnapshotEndpoint(
endpoint,
new CultMeshSnapshotEndpointOptions
{
Context = CultMesh.Verse("starbridge", "observer").Context,
DocumentRegistry = registry,
Request = new CultMeshSnapshotRequestOptions
{
CreateClient = () => new MeshSnapshotSchemaClient(request =>
registry.CreateRawSnapshotResponse(sourceCache, request.MessageId, request))
}
});
using var temp = new TemporaryDirectory();
using var node = await CultMesh.CreateNodeAsync(
Path.Combine(temp.Path, "observer-runtime.ccmp"),
new CultMeshNodeOptions
{
StartServer = false,
CacheOptions = new CultCacheOpenOptions
{
Registry = sourceCache.Registry
},
DatabaseOptions = new CultNetDatabaseOptions
{
DocumentRegistry = registry
}
});
var readOnlyHandle = surface
.SyncTo(node)
.Document<MeshNoteDocument>(key.Value);
readOnlyHandle.CanReplace.Should().BeFalse();
readOnlyHandle.CanSubmitPrediction.Should().BeFalse();
await sourceCache.UpsertAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "fresh-authority",
Revision = 5
}, new CultRecordHandle<MeshNoteDocument>(key));
var refreshed = await readOnlyHandle.LatestAsync();
refreshed.Text.Should().Be("fresh-authority");
node.Cache.Get<MeshNoteDocument>(key)!.Revision.Should().Be(5);
}
[Test]
public async Task ReactiveDocument_SnapshotMutationCannotPublish()
{
var subject = new Subject<MeshNoteDocument>();
var current = new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "initial",
Revision = 1
};
var predictions = new List<MeshNoteDocument>();
var handle = CultMesh.Document(
"mesh.note.reactive.mark-dirty",
CultMesh.Verse("starbridge", "pilot-a"),
_ => Task.FromResult(current),
_ => subject,
value =>
{
current = value;
subject.OnNext(value);
return Task.CompletedTask;
},
value =>
{
predictions.Add(value);
current = value;
subject.OnNext(value);
return Task.CompletedTask;
});
using var reactive = await handle.PredictionWriter().ReactiveAsync(
new CultMeshReactiveDocumentOptions
{
FlushDelay = TimeSpan.FromMinutes(1)
});
var escapedSnapshot = reactive.Snapshot;
escapedSnapshot.Text = "must-not-publish";
escapedSnapshot.Revision = 12;
await reactive.FlushAsync();
predictions.Should().BeEmpty();
reactive.Snapshot.Text.Should().Be("initial");
reactive.IsDirty.Should().BeFalse();
}
[Test]
public async Task ReactiveDocument_IdleMirrorDoesNotPublishUntilExplicitUpdate()
{
var subject = new Subject<MeshNoteDocument>();
var current = new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "initial",
Revision = 1
};
var predictions = new List<MeshNoteDocument>();
var handle = CultMesh.Document(
"mesh.note.reactive.direct-member-auto",
CultMesh.Verse("starbridge", "pilot-a"),
_ => Task.FromResult(current),
_ => subject,
value =>
{
current = value;
subject.OnNext(value);
return Task.CompletedTask;
},
value =>
{
predictions.Add(value);
current = value;
subject.OnNext(value);
return Task.CompletedTask;
});
using var reactive = await handle.PredictionWriter().ReactiveAsync(
new CultMeshReactiveDocumentOptions
{
FlushDelay = TimeSpan.FromMilliseconds(10)
});
await Task.Delay(75);
predictions.Should().BeEmpty();
reactive.Update(document =>
{
document.Text = "explicit-member-edit";
document.Revision = 3;
});
await WaitForAsync(() => predictions.Count == 1);
predictions[0].Text.Should().Be("explicit-member-edit");
predictions[0].Revision.Should().Be(3);
reactive.IsDirty.Should().BeFalse();
}
[TestCase(1)]
[TestCase(100)]
[TestCase(1000)]
public async Task ReactiveDocument_SchedulingScalesWithChangedDocumentsOnly(int documentCount)
{
var clock = new BlockingCountClock();
var reactiveDocuments = new List<CultMeshReactiveDocument<MeshNoteDocument>>(documentCount);
try
{
for (var index = 0; index < documentCount; index++)
{
var subject = new Subject<MeshNoteDocument>();
var current = new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = $"initial-{index}",
Revision = 1
};
var handle = CultMesh.Document(
$"mesh.note.reactive.scale.{index}",
CultMesh.Verse("scale", "test"),
_ => Task.FromResult(current),
_ => subject,
value =>
{
current = value;
subject.OnNext(value);
return Task.CompletedTask;
});
reactiveDocuments.Add(await handle.AuthoritativeWriter().ReactiveAsync(
new CultMeshReactiveDocumentOptions
{
Clock = clock,
FlushDelay = TimeSpan.FromSeconds(1)
}));
}
clock.DelayCount.Should().Be(0, "idle reactive documents must not schedule background work");
var changedDocumentCount = Math.Max(1, documentCount / 100);
for (var index = 0; index < changedDocumentCount; index++)
{
reactiveDocuments[index].Update(document =>
{
document.Text = $"changed-{index}";
document.Revision++;
});
}
clock.DelayCount.Should().Be(
changedDocumentCount,
"flush scheduling must be proportional to explicitly changed documents");
reactiveDocuments.Count(document => document.IsDirty).Should().Be(changedDocumentCount);
}
finally
{
foreach (var reactiveDocument in reactiveDocuments)
reactiveDocument.Dispose();
}
}
[Test]
public async Task ReactiveDocument_ReplaceLocalFlushesWholeDocument()
{
var subject = new Subject<MeshNoteDocument>();
var current = new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "initial",
Revision = 1
};
var predictions = new List<MeshNoteDocument>();
var handle = CultMesh.Document(
"mesh.note.reactive.direct-member-flush",
CultMesh.Verse("starbridge", "pilot-a"),
_ => Task.FromResult(current),
_ => subject,
value =>
{
current = value;
subject.OnNext(value);
return Task.CompletedTask;
},
value =>
{
predictions.Add(value);
current = value;
subject.OnNext(value);
return Task.CompletedTask;
});
using var reactive = await handle.PredictionWriter().ReactiveAsync(
new CultMeshReactiveDocumentOptions
{
FlushDelay = TimeSpan.FromMinutes(1)
});
reactive.ReplaceLocal(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "whole-document-replacement",
Revision = 4
});
await reactive.FlushAsync();
predictions.Should().ContainSingle();
predictions[0].Text.Should().Be("whole-document-replacement");
predictions[0].Revision.Should().Be(4);
}
[Test]
public async Task ReactiveDocument_CleanFlushDoesNotWrite()
{
var current = new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "initial",
Revision = 1
};
var writes = 0;
var handle = CultMesh.Document(
"mesh.note.reactive.clean-flush",
CultMesh.Verse("starbridge", "pilot-a"),
_ => Task.FromResult(current),
_ => new Subject<MeshNoteDocument>(),
value =>
{
writes++;
current = value;
return Task.CompletedTask;
});
using var reactive = await handle.AuthoritativeWriter().ReactiveAsync();
await reactive.FlushAsync();
writes.Should().Be(0);
reactive.IsDirty.Should().BeFalse();
}
[Test]
public async Task ReactiveDocument_FlushesAgainWhenEditedDuringInFlightFlush()
{
var subject = new Subject<MeshNoteDocument>();
var current = new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "initial",
Revision = 1
};
var allowFirstWrite = new TaskCompletionSource<bool>();
var firstWriteStarted = new TaskCompletionSource<bool>();
var predictions = new List<MeshNoteDocument>();
var handle = CultMesh.Document(
"mesh.note.reactive.in-flight",
CultMesh.Verse("starbridge", "pilot-a"),
_ => Task.FromResult(current),
_ => subject,
value =>
{
current = value;
subject.OnNext(value);
return Task.CompletedTask;
},
async value =>
{
predictions.Add(value);
if (predictions.Count == 1)
{
firstWriteStarted.SetResult(true);
await allowFirstWrite.Task.ConfigureAwait(false);
}
current = value;
subject.OnNext(value);
});
using var reactive = await handle.PredictionWriter().ReactiveAsync(
new CultMeshReactiveDocumentOptions
{
FlushDelay = TimeSpan.FromMinutes(1)
});
reactive.Update(document =>
{
document.Text = "first";
document.Revision = 2;
});
var flush = reactive.FlushAsync();
await firstWriteStarted.Task;
reactive.Update(document =>
{
document.Text = "second";
document.Revision = 3;
});
allowFirstWrite.SetResult(true);
await flush;
predictions.Should().HaveCount(2);
predictions[0].Text.Should().Be("first");
predictions[1].Text.Should().Be("second");
current.Revision.Should().Be(3);
}
[Test]
public async Task ReactiveDocument_DebouncesBurstUpdatesIntoOnePrediction()
{
var subject = new Subject<MeshNoteDocument>();
var current = new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "initial",
Revision = 1
};
var predictions = new List<MeshNoteDocument>();
var handle = CultMesh.Document(
"mesh.note.reactive.burst",
CultMesh.Verse("starbridge", "pilot-a"),
_ => Task.FromResult(current),
_ => subject,
value =>
{
current = value;
subject.OnNext(value);
return Task.CompletedTask;
},
value =>
{
predictions.Add(value);
current = value;
subject.OnNext(value);
return Task.CompletedTask;
});
using var reactive = await handle.PredictionWriter().ReactiveAsync(
new CultMeshReactiveDocumentOptions
{
FlushDelay = TimeSpan.FromMilliseconds(35)
});
for (var i = 0; i < 5; i++)
{
reactive.Update(document =>
{
document.Text = $"burst-{i}";
document.Revision++;
});
}
await WaitForAsync(() => predictions.Count == 1);
predictions[0].Text.Should().Be("burst-4");
predictions[0].Revision.Should().Be(6);
reactive.IsDirty.Should().BeFalse();
}
[Test]
public async Task ReactiveDocument_ExplicitEditsDuringInFlightFlushQueueLatestPrediction()
{
var subject = new Subject<MeshNoteDocument>();
var current = new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "initial",
Revision = 1
};
var allowFirstWrite = new TaskCompletionSource<bool>();
var firstWriteStarted = new TaskCompletionSource<bool>();
var predictions = new List<MeshNoteDocument>();
var handle = CultMesh.Document(
"mesh.note.reactive.direct-in-flight",
CultMesh.Verse("starbridge", "pilot-a"),
_ => Task.FromResult(current),
_ => subject,
value =>
{
current = value;
subject.OnNext(value);
return Task.CompletedTask;
},
async value =>
{
predictions.Add(value);
if (predictions.Count == 1)
{
firstWriteStarted.SetResult(true);
await allowFirstWrite.Task.ConfigureAwait(false);
}
current = value;
subject.OnNext(value);
});
using var reactive = await handle.PredictionWriter().ReactiveAsync(
new CultMeshReactiveDocumentOptions
{
FlushDelay = TimeSpan.FromMinutes(1)
});
reactive.Update(document =>
{
document.Text = "first-explicit";
document.Revision = 2;
});
var flush = reactive.FlushAsync();
await firstWriteStarted.Task;
reactive.Update(document =>
{
document.Text = "second-explicit";
document.Revision = 3;
});
allowFirstWrite.SetResult(true);
await flush;
predictions.Should().HaveCount(2);
predictions[0].Text.Should().Be("first-explicit");
predictions[1].Text.Should().Be("second-explicit");
reactive.IsDirty.Should().BeFalse();
}
[Test]
public async Task ReactiveDocument_CanonicalEchoClearsOutstandingPredictionWithoutDelta()
{
var subject = new Subject<MeshNoteDocument>();
var current = new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "initial",
Revision = 1
};
var handle = CultMesh.Document(
"mesh.note.reactive.canonical-echo",
CultMesh.Verse("starbridge", "pilot-a"),
_ => Task.FromResult(current),
_ => subject,
value =>
{
current = value;
subject.OnNext(value);
return Task.CompletedTask;
},
value =>
{
current = value;
subject.OnNext(value);
return Task.CompletedTask;
});
using var reactive = await handle.PredictionWriter().ReactiveAsync(
new CultMeshReactiveDocumentOptions
{
FlushDelay = TimeSpan.FromMinutes(1)
});
reactive.Update(document =>
{
document.Text = "accepted";
document.Revision = 2;
});
await reactive.FlushAsync();
reactive.Snapshot.Text.Should().Be("accepted");
reactive.Reconciliation.Should().BeNull();
reactive.IsDirty.Should().BeFalse();
}
[Test]
public async Task DocumentHandle_ReadsSchemaPublicationsFromSingleFileStores()
{
using var directory = new TemporaryDirectory();
var filePath = Path.Combine(directory.Path, "publication.ccmp");
var key = new CultRecordKey("mesh-note:publication");
var writerCache = new CultCache();
writerCache.AddBackingStore(new SingleFileMessagePackBackingStore(filePath));
await writerCache.UpsertAsync(typeof(MeshPublicationNoteDocument), new MeshPublicationNoteDocument
{
Schema = "tests.mesh_publication_note.v1",
Text = "published",
Revision = 1
}, key);
writerCache.FlushAllBackingStores();
File.Exists(filePath).Should().BeTrue();
var directReadCache = new CultCache();
directReadCache.AddBackingStore(new SingleFileMessagePackBackingStore(filePath));
await directReadCache.PullAllBackingStoresAsync();
directReadCache.Get<MeshPublicationNoteDocument>(key)!.Text.Should().Be("published");
var handle = CultMesh.DocumentFromPublication<MeshPublicationNoteDocument>(
CultMeshDocumentPublicationSource.SingleFile(filePath),
key,
CultMesh.Verse("starbridge", "unity-pilot"),
new CultMeshStoreDocumentOptions
{
DocumentId = "daemon:tests.mesh_note.latest",
SourceId = "daemon:tests.mesh_note.latest.v1",
PollInterval = TimeSpan.FromMilliseconds(10)
});
var observed = new List<MeshPublicationNoteDocument>();
using var subscription = handle.Watch(value => observed.Add(value));
(await handle.LatestAsync()).Text.Should().Be("published");
handle.CanReplace.Should().BeFalse();
handle.CanSubmitPrediction.Should().BeFalse();
handle.DocumentId.Should().Be("daemon:tests.mesh_note.latest");
handle.RouteHint.Kind.Should().Be(CultMeshLocalityKind.SharedMemory);
handle.Sources.Should().ContainSingle().Which.SourceId.Should().Be("daemon:tests.mesh_note.latest.v1");
var republisher = new CultCache();
republisher.AddBackingStore(new SingleFileMessagePackBackingStore(filePath));
await republisher.UpsertAsync(typeof(MeshPublicationNoteDocument), new MeshPublicationNoteDocument
{
Schema = "tests.mesh_publication_note.v1",
Text = "republished",
Revision = 2
}, key);
republisher.FlushAllBackingStores();
await WaitForAsync(() => observed.Any(value => value.Text == "republished"));
(await handle.LatestAsync()).Revision.Should().Be(2);
}
[Test]
public async Task SingleFileDocumentHelpers_RoundTripTypedDocuments()
{
using var directory = new TemporaryDirectory();
var filePath = Path.Combine(directory.Path, "single-document.ccmp");
var key = new CultRecordKey("mesh-note:single-document");
CultMesh.WriteSingleFileDocument(
filePath,
key,
new MeshPublicationNoteDocument
{
Schema = "tests.mesh_publication_note.v1",
Text = "published-directly",
Revision = 3
},
storedAt: "2026-06-27T12:00:00.0000000Z");
var document = CultMesh.ReadSingleFileDocument<MeshPublicationNoteDocument>(filePath, key);
document.Text.Should().Be("published-directly");
document.Revision.Should().Be(3);
var cache = new CultCache();
cache.AddBackingStore(new SingleFileMessagePackBackingStore(filePath));
await cache.PullAllBackingStoresAsync();
cache.Get<MeshPublicationNoteDocument>(key)!.Text.Should().Be("published-directly");
}
[Test]
public async Task DocumentHandle_ReadsRemotePeerSnapshotsAsTypedDocuments()
{
var cache = new CultCache();
var key = new CultRecordKey("mesh-note:remote");
await cache.UpsertAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "remote-snapshot",
Revision = 9
}, new CultRecordHandle<MeshNoteDocument>(key));
var registry = new CultNetDocumentRegistry(CultDocumentRegistry.Shared)
.Register(CultNetDocumentBinding.ForDocument<MeshNoteDocument>(CultDocumentRegistry.Shared));
var requests = new List<CultNetSnapshotRequestMessage>();
var handle = CultMesh.DocumentFromPublication<MeshNoteDocument>(
CultMeshDocumentPublicationSource.PeerSnapshot(
() => new MeshSnapshotSchemaClient(request =>
{
requests.Add(request);
return registry.CreateRawSnapshotResponse(cache, request.MessageId, request);
}),
"cultnet://snapshot.test:3075"),
key,
CultMesh.Verse("starbridge", "unity-pilot"),
peerOptions: new CultMeshPeerSnapshotDocumentOptions
{
PollInterval = TimeSpan.FromMilliseconds(10),
MessageIdPrefix = "mesh-test-snapshot"
});
var catalog = CultMesh.Documents(handle);
var snapshot = await handle.LatestAsync();
snapshot.Text.Should().Be("remote-snapshot");
snapshot.Revision.Should().Be(9);
handle.DocumentType.Should().Be(typeof(MeshNoteDocument));
handle.RouteHint.Kind.Should().Be(CultMeshLocalityKind.Network);
handle.Sources.Should().ContainSingle().Which.SourceId.Should().Be(key.Value);
requests.Should().ContainSingle();
requests[0].SchemaIds.Should().ContainSingle().Which
.Should().Be(CultDocumentRegistry.Shared.GetRequired<MeshNoteDocument>().SchemaId);
requests[0].RecordKeys.Should().ContainSingle().Which.Should().Be(key.Value);
(await catalog.LatestAsync<MeshNoteDocument>()).Text.Should().Be("remote-snapshot");
var aliasSchemaHandle = CultMesh.DocumentFromPeerSnapshot<MeshNoteDocument>(
_ => Task.FromResult(new CultNetSnapshotResponseRawMessage
{
MessageId = "alias-schema",
Documents = new[]
{
new CultNetRawDocumentRecord
{
SchemaId = "remote.generated.mesh_note_alias.v99",
RecordKey = key.Value,
StoredAt = DateTimeOffset.UtcNow.ToString("O"),
PayloadEncoding = "messagepack",
Payload = CultDocumentMessagePackSerialization.SerializeUntyped(
new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "record-key-fallback",
Revision = 10
},
typeof(MeshNoteDocument))
}
}
}),
key.Value,
CultMesh.Verse("starbridge", "unity-pilot"));
(await aliasSchemaHandle.LatestAsync()).Text.Should().Be("record-key-fallback");
var mixedAliasSchemaHandle = CultMesh.DocumentFromPeerSnapshot<MeshNoteDocument>(
_ => Task.FromResult(new CultNetSnapshotResponseRawMessage
{
MessageId = "alias-schema-with-neighbor",
Documents = new[]
{
new CultNetRawDocumentRecord
{
SchemaId = CultDocumentRegistry.Shared.GetRequired<MeshNoteDocument>().SchemaId,
RecordKey = "mesh-note:other",
StoredAt = DateTimeOffset.UtcNow.ToString("O"),
PayloadEncoding = "messagepack",
Payload = CultDocumentMessagePackSerialization.SerializeUntyped(
new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "wrong-schema-neighbor",
Revision = 11
},
typeof(MeshNoteDocument))
},
new CultNetRawDocumentRecord
{
SchemaId = "remote.generated.mesh_note_alias.v100",
RecordKey = key.Value,
StoredAt = DateTimeOffset.UtcNow.ToString("O"),
PayloadEncoding = "messagepack",
Payload = CultDocumentMessagePackSerialization.SerializeUntyped(
new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "compatible-record-key",
Revision = 12
},
typeof(MeshNoteDocument))
}
}
}),
key.Value,
CultMesh.Verse("starbridge", "unity-pilot"));
(await mixedAliasSchemaHandle.LatestAsync()).Text.Should().Be("compatible-record-key");
// The recordKey match is exact (ordinal), not case-insensitive. A case-variant key must not
// win even when it appears first in the response.
var caseVariantKey = key.Value.ToUpperInvariant();
var caseVariantKeyHandle = CultMesh.DocumentFromPeerSnapshot<MeshNoteDocument>(
_ => Task.FromResult(new CultNetSnapshotResponseRawMessage
{
MessageId = "case-variant-key",
Documents = new[]
{
new CultNetRawDocumentRecord
{
SchemaId = CultDocumentRegistry.Shared.GetRequired<MeshNoteDocument>().SchemaId,
RecordKey = caseVariantKey,
StoredAt = DateTimeOffset.UtcNow.ToString("O"),
PayloadEncoding = "messagepack",
Payload = CultDocumentMessagePackSerialization.SerializeUntyped(
new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "wrong-case-key",
Revision = 13
},
typeof(MeshNoteDocument))
},
new CultNetRawDocumentRecord
{
SchemaId = CultDocumentRegistry.Shared.GetRequired<MeshNoteDocument>().SchemaId,
RecordKey = key.Value,
StoredAt = DateTimeOffset.UtcNow.ToString("O"),
PayloadEncoding = "messagepack",
Payload = CultDocumentMessagePackSerialization.SerializeUntyped(
new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "right-case-key",
Revision = 14
},
typeof(MeshNoteDocument))
}
}
}),
key.Value,
CultMesh.Verse("starbridge", "unity-pilot"));
(await caseVariantKeyHandle.LatestAsync()).Text.Should().Be("right-case-key");
}
// R-R (docs/cultnet-selection-cut.md, Self's rulings 2026-09-22) corrects the earlier commit-2
// ruling this test used to pin: an explicit empty recordKeys array is a real filter, not "no
// filter" that falls back to the type's own schema default. Discrepancy against R-R's prose,
// reported rather than silently resolved: at CultMesh's typed convenience layer this selection
// never reaches the wire at all. ResolveDefaultSelection now produces Keys=[] instead of Keys=null,
// and EnsureV0Compatible - CultMesh's own client-side door, landed under the earlier ruling R-F -
// already refuses an empty selection.keys before any request is sent ("Keys=[]/[\"\"] is refused
// rather than silently lowered to 'every key'", CultMeshSnapshots.cs:836-841). R-R's "recordKeys:
// [] answers empty" therefore only holds for the raw v0 wire protocol server, which any v0 peer
// can call directly without CultMesh's typed wrapper - see
// CultNetDocumentRegistry_RawSnapshot_ExplicitEmptyRecordKeys_AnswersEmpty in NetworkingTests.cs.
[Test]
public void FetchDocumentsAsync_WithExplicitEmptyRecordKeys_IsRefusedClientSide()
{
var registry = new CultNetDocumentRegistry(CultDocumentRegistry.Shared)
.Register(CultNetDocumentBinding.ForDocument<MeshNoteDocument>(CultDocumentRegistry.Shared));
var requests = new List<CultNetSnapshotRequestMessage>();
using var session = CultMesh.SnapshotSession(
"cultnet://empty-keys-default.test:3075",
new CultMeshSnapshotRequestOptions
{
CreateClient = () => new MeshSnapshotSchemaClient(request =>
{
requests.Add(request);
return registry.CreateRawSnapshotResponse(new CultCache(), request.MessageId, request);
})
},
registry);
Assert.ThrowsAsync<CultNetSelectionInvalidException>(
() => session.FetchDocumentsAsync<MeshNoteDocument>(recordKeys: Array.Empty<string>()));
requests.Should().BeEmpty();
}
[Test]
public async Task PublicationHelpers_BindConfiguredDocumentsIntoCatalog()
{
using var directory = new TemporaryDirectory();
var firstPath = Path.Combine(directory.Path, "first.ccmp");
var secondPath = Path.Combine(directory.Path, "second.ccmp");
var firstKey = new CultRecordKey("mesh-note:publication-catalog-first");
var secondKey = new CultRecordKey("mesh-note:publication-catalog-second");
var firstWriter = new CultCache();
firstWriter.AddBackingStore(new SingleFileMessagePackBackingStore(firstPath));
await firstWriter.UpsertAsync(typeof(MeshPublicationNoteDocument), new MeshPublicationNoteDocument
{
Schema = "tests.mesh_publication_note.v1",
Text = "first source",
Revision = 21
}, firstKey);
firstWriter.FlushAllBackingStores();
var secondWriter = new CultCache();
secondWriter.AddBackingStore(new SingleFileMessagePackBackingStore(secondPath));
await secondWriter.UpsertAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "second source",
Revision = 22
}, new CultRecordHandle<MeshNoteDocument>(secondKey));
secondWriter.FlushAllBackingStores();
var catalog = CultMesh.DocumentsFromPublication(
CultMeshDocumentPublicationSource.SingleFile(firstPath),
new ICultMeshPublicationDocumentBinding[]
{
CultMesh.PublicationDocument<MeshPublicationNoteDocument>(
firstKey,
documentId: "daemon:first"),
CultMesh.PublicationDocument<MeshNoteDocument>(
secondKey,
documentId: "daemon:second",
source: CultMeshDocumentPublicationSource.SingleFile(secondPath))
},
CultMesh.Verse("starbridge", "unity-catalog"),
new CultMeshStoreDocumentOptions
{
RouteHint = new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "publication catalog"),
PollInterval = TimeSpan.FromMilliseconds(10)
});
catalog.Documents.Select(document => document.DocumentId).Should().Equal("daemon:first", "daemon:second");
(await catalog.LatestAsync<MeshPublicationNoteDocument>()).Text.Should().Be("first source");
(await catalog.LatestAsync<MeshNoteDocument>()).Text.Should().Be("second source");
catalog.Document<MeshNoteDocument>().DocumentId.Should().Be("daemon:second");
catalog.Document<MeshPublicationNoteDocument>().RouteHint.Description.Should().Be("publication catalog");
catalog.Document<MeshNoteDocument>().Sources.Should().ContainSingle().Which.SourceId.Should().Be("daemon:second");
}
[Test]
public async Task PublicationHelpers_SyncConfiguredDocumentsIntoLocalCatalog()
{
using var directory = new TemporaryDirectory();
var firstPath = Path.Combine(directory.Path, "sync-first.ccmp");
var secondPath = Path.Combine(directory.Path, "sync-second.ccmp");
var firstKey = new CultRecordKey("mesh-note:publication-sync-catalog-first");
var secondKey = new CultRecordKey("mesh-note:publication-sync-catalog-second");
var firstWriter = new CultCache();
firstWriter.AddBackingStore(new SingleFileMessagePackBackingStore(firstPath));
await firstWriter.UpsertAsync(typeof(MeshPublicationNoteDocument), new MeshPublicationNoteDocument
{
Schema = "tests.mesh_publication_note.v1",
Text = "first synced source",
Revision = 31
}, firstKey);
firstWriter.FlushAllBackingStores();
var secondWriter = new CultCache();
secondWriter.AddBackingStore(new SingleFileMessagePackBackingStore(secondPath));
await secondWriter.UpsertAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "second synced source",
Revision = 32
}, new CultRecordHandle<MeshNoteDocument>(secondKey));
secondWriter.FlushAllBackingStores();
using var node = await CultMesh.CreateNodeAsync(
Path.Combine(directory.Path, "target.ccmp"),
new CultMeshNodeOptions
{
StartServer = false
});
var catalog = await CultMesh.SyncDocumentsFromPublicationAsync(
node,
CultMeshDocumentPublicationSource.SingleFile(firstPath),
new ICultMeshPublicationDocumentBinding[]
{
CultMesh.PublicationDocument<MeshPublicationNoteDocument>(
firstKey,
documentId: "local:first"),
CultMesh.PublicationDocument<MeshNoteDocument>(
secondKey,
documentId: "local:second",
source: CultMeshDocumentPublicationSource.SingleFile(secondPath))
},
CultMesh.Verse("starbridge", "unity-sync-catalog"),
new CultMeshStoreDocumentOptions
{
PollInterval = TimeSpan.FromMilliseconds(10)
});
catalog.Documents.Select(document => document.DocumentId).Should().Equal("local:first", "local:second");
(await catalog.LatestAsync<MeshPublicationNoteDocument>()).Text.Should().Be("first synced source");
(await catalog.LatestAsync<MeshNoteDocument>()).Text.Should().Be("second synced source");
node.Cache.Get<MeshPublicationNoteDocument>(firstKey)!.Revision.Should().Be(31);
(await node.Document<MeshNoteDocument>(secondKey, CultMesh.Verse("starbridge", "unity-sync-catalog"))
.LatestAsync()).Text.Should().Be("second synced source");
(await node.Document<MeshNoteDocument>(secondKey, CultMesh.Verse("starbridge", "unity-sync-catalog"))
.LatestAsync()).Revision.Should().Be(32);
}
[Test]
public async Task PublicationHelpers_SyncConfiguredPeerSourceIntoLocalNode()
{
var sourceCache = new CultCache();
var registry = new CultNetDocumentRegistry(CultDocumentRegistry.Shared);
registry.Register(CultNetDocumentBinding.ForDocument<MeshNoteDocument>(sourceCache.Registry));
var key = new CultRecordKey("mesh-note:publication-sync-alias");
await sourceCache.UpsertAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "publication-synced",
Revision = 51
}, new CultRecordHandle<MeshNoteDocument>(key));
var requests = new List<CultNetSnapshotRequestMessage>();
var verse = CultMesh.Verse("starbridge", "unity-support");
using var node = await CultMesh.CreateNodeAsync(
Path.Combine(Path.GetTempPath(), $"cultmesh-publication-sync-alias-{Guid.NewGuid():N}.ccmp"),
new CultMeshNodeOptions
{
StartServer = false,
CacheOptions = new CultCacheOpenOptions
{
Registry = sourceCache.Registry
},
DatabaseOptions = new CultNetDatabaseOptions
{
DocumentRegistry = registry
}
});
var synced = await CultMesh.SyncDocumentFromPublicationAsync<MeshNoteDocument>(
node,
CultMeshDocumentPublicationSource.PeerSnapshot(
() => new MeshSnapshotSchemaClient(request =>
{
requests.Add(request);
return registry.CreateRawSnapshotResponse(sourceCache, request.MessageId, request);
}),
"cultnet://publication-sync.test:3075"),
key,
verse,
peerOptions: new CultMeshPeerSnapshotDocumentOptions
{
MessageIdPrefix = "mesh-test-publication-sync",
PollInterval = TimeSpan.FromMilliseconds(10)
});
var local = await node.Document<MeshNoteDocument>(key, verse).LatestAsync();
synced.Text.Should().Be("publication-synced");
synced.Revision.Should().Be(51);
local.Text.Should().Be("publication-synced");
local.Revision.Should().Be(51);
requests.Should().ContainSingle().Which.RecordKeys.Should().Equal(key.Value);
}
[Test]
public async Task SnapshotHelpers_FetchApplyAndDecodeScopedSnapshots()
{
var sourceCache = new CultCache();
var targetCache = new CultCache();
var registry = new CultNetDocumentRegistry(CultDocumentRegistry.Shared);
registry.Register(CultNetDocumentBinding.ForDocument<MeshPublicationNoteDocument>(sourceCache.Registry));
var key = new CultRecordKey("mesh-note:snapshot-helper");
await sourceCache.UpsertAsync(new MeshPublicationNoteDocument
{
Schema = "tests.mesh_publication_note.v1",
Text = "snapshot-helper",
Revision = 11
}, new CultRecordHandle<MeshPublicationNoteDocument>(key));
var requests = new List<CultNetSnapshotRequestMessage>();
var endpoint = "cultnet://snapshot-helper.test:3075";
var options = new CultMeshSnapshotRequestOptions
{
Selection = new CultNetSelection { Keys = new[] { key.Value } },
ShardId = "primary",
ShardEpoch = 7,
MessageIdPrefix = "mesh-test-scoped-snapshot",
CreateClient = () => new MeshSnapshotSchemaClient(request =>
{
requests.Add(request);
return registry.CreateRawSnapshotResponse(sourceCache, request.MessageId, request);
})
};
var snapshot = await CultMesh.FetchSnapshotAsync(endpoint, options);
var decoded = await CultMesh.FetchSnapshotDocumentsAsync<MeshPublicationNoteDocument>(
endpoint,
options,
registry);
using var node = await CultMesh.CreateNodeAsync(
Path.Combine(Path.GetTempPath(), $"cultmesh-snapshot-helper-{Guid.NewGuid():N}.ccmp"),
new CultMeshNodeOptions
{
StartServer = false,
CacheOptions = new CultCacheOpenOptions
{
Registry = targetCache.Registry
},
DatabaseOptions = new CultNetDatabaseOptions
{
DocumentRegistry = registry
}
});
var applied = await CultMesh.ApplySnapshotAsync(node, endpoint, options);
snapshot.Documents.Should().ContainSingle().Which.RecordKey.Should().Be(key.Value);
decoded.Should().ContainSingle().Which.Text.Should().Be("snapshot-helper");
applied.Should().ContainSingle();
node.Cache.Get<MeshPublicationNoteDocument>(key)!.Revision.Should().Be(11);
requests.Should().HaveCount(3);
requests.Should().OnlyContain(request =>
request.RecordKeys!.SequenceEqual(new[] { key.Value }) &&
request.ShardId == "primary" &&
request.ShardEpoch == 7 &&
request.MessageId.StartsWith("mesh-test-scoped-snapshot:", StringComparison.Ordinal));
}
[Test]
public async Task SnapshotHelpers_SurfaceBackgroundClientFailureBeforeTimeout()
{
var options = new CultMeshSnapshotRequestOptions
{
ResponseTimeout = TimeSpan.FromSeconds(30),
Selection = new CultNetSelection { Keys = new[] { "eve:surface:test" } },
CreateClient = () => new FailedBackgroundSchemaClient(
new FormatException("malformed fragment"))
};
Func<Task> act = async () => await CultMesh.FetchSnapshotAsync(
"rudp://127.0.0.1:3075",
options);
var failure = await act.Should().ThrowAsync<InvalidOperationException>();
failure.Which.InnerException.Should().BeOfType<FormatException>();
failure.Which.Message.Should().Contain("127.0.0.1:3075");
}
[Test]
public async Task SnapshotHelpers_SurfaceBackgroundClientFailureWhileConnecting()
{
var options = new CultMeshSnapshotRequestOptions
{
ConnectTimeout = TimeSpan.FromSeconds(30),
CreateClient = () => new FailedBackgroundSchemaClient(
new FormatException("malformed handshake"),
connected: false)
};
Func<Task> act = async () => await CultMesh.FetchSnapshotAsync(
"rudp://127.0.0.1:3075",
options);
var failure = await act.Should().ThrowAsync<InvalidOperationException>();
failure.Which.InnerException.Should().BeOfType<FormatException>();
failure.Which.Message.Should().Contain("connecting");
}
[Test]
public async Task SnapshotHelpers_DecodeFromForeignSchemaId()
{
var key = new CultRecordKey("mesh-note:snapshot-alias-foreign-schema");
var snapshot = new CultNetSnapshotResponseRawMessage
{
MessageId = "foreign-schema",
Documents = new[]
{
new CultNetRawDocumentRecord
{
SchemaId = "runtime.generated.mesh-note.ui.42",
RecordKey = key.Value,
StoredAt = DateTimeOffset.UtcNow.ToString("O"),
PayloadEncoding = "messagepack",
Payload = CultDocumentMessagePackSerialization.SerializeUntyped(
new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "foreign-schema-alias",
Revision = 44
},
typeof(MeshNoteDocument))
}
}
};
var documents = await CultMesh.FetchSnapshotDocumentsAsync<MeshNoteDocument>(
"cultnet://foreign-schema.test:3075",
new CultMeshSnapshotRequestOptions
{
Selection = new CultNetSelection { Keys = new[] { key.Value } },
CreateClient = () => new MeshSnapshotSchemaClient(request =>
{
snapshot.MessageId = request.MessageId;
return snapshot;
})
},
new CultNetDocumentRegistry());
documents.Should().ContainSingle().Which.Text.Should().Be("foreign-schema-alias");
}
[Test]
public async Task SnapshotEndpoint_SyncsCompatibleForeignSchemaIdIntoLocalCache()
{
var key = new CultRecordKey("mesh-note:snapshot-sync-foreign-schema");
var snapshot = new CultNetSnapshotResponseRawMessage
{
MessageId = "foreign-schema-sync",
Documents = new[]
{
new CultNetRawDocumentRecord
{
SchemaId = "runtime.generated.mesh-note.ui.84",
RecordKey = key.Value,
StoredAt = DateTimeOffset.UtcNow.ToString("O"),
PayloadEncoding = "messagepack",
Payload = CultDocumentMessagePackSerialization.SerializeUntyped(
new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "foreign-schema-synced",
Revision = 45
},
typeof(MeshNoteDocument))
}
}
};
var cacheRegistry = CultMesh.CreateCultCacheDocumentRegistry(
typeof(MeshNoteDocument),
typeof(MeshNoteDocument));
var registry = CultMesh.CreateCultNetDocumentRegistry(
new[] { typeof(MeshNoteDocument), typeof(MeshNoteDocument) },
cacheRegistry);
var endpoint = CultMesh.SnapshotEndpoint(
"cultnet://foreign-schema.test:3075",
new CultMeshSnapshotEndpointOptions
{
DocumentRegistry = registry,
Request = new CultMeshSnapshotRequestOptions
{
CreateClient = () => new MeshSnapshotSchemaClient(request =>
{
snapshot.MessageId = request.MessageId;
return snapshot;
})
}
});
using var node = await CultMesh.CreateNodeAsync(
Path.Combine(Path.GetTempPath(), $"cultmesh-foreign-schema-sync-{Guid.NewGuid():N}.ccmp"),
new CultMeshNodeOptions
{
StartServer = false,
CacheOptions = new CultCacheOpenOptions
{
Registry = cacheRegistry
},
DatabaseOptions = new CultNetDatabaseOptions
{
DocumentRegistry = registry
}
});
var synced = await endpoint.SyncDocumentAsync<MeshNoteDocument>(node, key.Value);
synced.Text.Should().Be("foreign-schema-synced");
node.Cache.Get<MeshNoteDocument>(key)!.Text.Should().Be("foreign-schema-synced");
node.Cache.Get<MeshNoteDocument>(key)!.Revision.Should().Be(45);
}
[Test]
public async Task SnapshotFacade_SyncsRemoteDocumentIntoLocalNode()
{
var sourceCache = new CultCache();
var registry = new CultNetDocumentRegistry(CultDocumentRegistry.Shared);
registry.Register(CultNetDocumentBinding.ForDocument<MeshNoteDocument>(sourceCache.Registry));
var key = new CultRecordKey("mesh-note:facade-sync-alias");
await sourceCache.UpsertAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "facade-synced",
Revision = 46
}, new CultRecordHandle<MeshNoteDocument>(key));
var requests = new List<CultNetSnapshotRequestMessage>();
using var node = await CultMesh.CreateNodeAsync(
Path.Combine(Path.GetTempPath(), $"cultmesh-facade-sync-alias-{Guid.NewGuid():N}.ccmp"),
new CultMeshNodeOptions
{
StartServer = false,
CacheOptions = new CultCacheOpenOptions
{
Registry = sourceCache.Registry
},
DatabaseOptions = new CultNetDatabaseOptions
{
DocumentRegistry = registry
}
});
var synced = await CultMesh.SyncDocumentFromPeerSnapshotAsync<MeshNoteDocument>(
node,
() => new MeshSnapshotSchemaClient(request =>
{
requests.Add(request);
return registry.CreateRawSnapshotResponse(sourceCache, request.MessageId, request);
}),
"cultnet://facade-sync.test:3075",
key.Value,
new CultMeshSnapshotEndpointOptions
{
DocumentRegistry = registry,
Context = CultMesh.Verse("starbridge", "unity-raven").Context
});
var local = await node.Document<MeshNoteDocument>(key.Value, CultMesh.Verse("starbridge", "unity-raven"))
.LatestAsync();
synced.Text.Should().Be("facade-synced");
synced.Revision.Should().Be(46);
local.Text.Should().Be("facade-synced");
node.Cache.Get<MeshNoteDocument>(key)!.Revision.Should().Be(46);
requests.Should().ContainSingle().Which.RecordKeys.Should().Equal(key.Value);
}
[Test]
public async Task SnapshotSession_ReusesOneConnectedClientAcrossOrderedRequests()
{
var sourceCache = new CultCache();
var registry = new CultNetDocumentRegistry(CultDocumentRegistry.Shared);
registry.Register(CultNetDocumentBinding.ForDocument<MeshNoteDocument>(sourceCache.Registry));
var firstKey = new CultRecordKey("mesh-note:session-first");
var secondKey = new CultRecordKey("mesh-note:session-second");
await sourceCache.UpsertAsync(
new MeshNoteDocument { Schema = "tests.mesh_note.v1", Text = "first", Revision = 1 },
new CultRecordHandle<MeshNoteDocument>(firstKey));
await sourceCache.UpsertAsync(
new MeshNoteDocument { Schema = "tests.mesh_note.v1", Text = "second", Revision = 2 },
new CultRecordHandle<MeshNoteDocument>(secondKey));
var client = new MeshSnapshotSchemaClient(request =>
registry.CreateRawSnapshotResponse(sourceCache, request.MessageId, request));
using var session = CultMesh.SnapshotSession(
"cultnet://session.test:3075",
new CultMeshSnapshotRequestOptions { CreateClient = () => client },
registry);
var first = await session.FetchDocumentsAsync<MeshNoteDocument>(new[] { firstKey.Value });
var second = await session.FetchDocumentsAsync<MeshNoteDocument>(new[] { secondKey.Value });
client.ConnectCount.Should().Be(1);
first.Should().ContainSingle().Which.Text.Should().Be("first");
second.Should().ContainSingle().Which.Text.Should().Be("second");
}
[Test]
public async Task DocumentRegistryHelpers_CreateTypedNetworkRegistriesForAliases()
{
var sourceCache = new CultCache();
var cacheRegistry = CultMesh.CreateCultCacheDocumentRegistry(
typeof(MeshNoteDocument),
typeof(MeshNoteDocument));
var networkRegistry = CultMesh.CreateCultNetDocumentRegistry(
new[] { typeof(MeshNoteDocument), typeof(MeshNoteDocument) },
cacheRegistry);
var key = new CultRecordKey("mesh-note:registry-helper");
await sourceCache.UpsertAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "registry-helper",
Revision = 23
}, new CultRecordHandle<MeshNoteDocument>(key));
var endpoint = "cultnet://registry-helper.test:3077";
var documents = await CultMesh.FetchSnapshotDocumentsAsync<MeshNoteDocument>(
endpoint,
new CultMeshSnapshotRequestOptions
{
Selection = new CultNetSelection { Keys = new[] { key.Value } },
CreateClient = () => new MeshSnapshotSchemaClient(request =>
networkRegistry.CreateRawSnapshotResponse(sourceCache, request.MessageId, request))
},
networkRegistry);
var schemaId = cacheRegistry.GetRequired<MeshNoteDocument>().SchemaId;
cacheRegistry.GetRequired<MeshNoteDocument>().SchemaId.Should().Be(schemaId);
networkRegistry.GetByDocumentType(typeof(MeshNoteDocument)).Should().NotBeNull();
networkRegistry.GetByDocumentType(typeof(MeshNoteDocument)).Should().NotBeNull();
networkRegistry.GetBySchemaId(schemaId)!.DocumentType.Should().Be(typeof(MeshNoteDocument));
documents.Should().ContainSingle().Which.Text.Should().Be("registry-helper");
}
[Test]
public async Task SnapshotEndpoint_ProvidesTypedHandles()
{
var sourceCache = new CultCache();
var registry = new CultNetDocumentRegistry(CultDocumentRegistry.Shared);
registry.Register(CultNetDocumentBinding.ForDocument<MeshNoteDocument>(sourceCache.Registry));
var key = new CultRecordKey("mesh-note:snapshot-endpoint");
await sourceCache.UpsertAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "snapshot-endpoint",
Revision = 19
}, new CultRecordHandle<MeshNoteDocument>(key));
var requests = new List<CultNetSnapshotRequestMessage>();
var endpoint = "cultnet://snapshot-endpoint.test:3076";
var surface = CultMesh.SnapshotEndpoint(
endpoint,
new CultMeshSnapshotEndpointOptions
{
Context = CultMesh.Verse("starbridge", "browser-starfire").Context,
DocumentRegistry = registry,
Request = new CultMeshSnapshotRequestOptions
{
ShardId = "primary",
ShardEpoch = 9,
CreateClient = () => new MeshSnapshotSchemaClient(request =>
{
requests.Add(request);
return registry.CreateRawSnapshotResponse(sourceCache, request.MessageId, request);
})
}
});
var fetched = await surface.FetchDocumentAsync<MeshNoteDocument>(key.Value);
using var node = await CultMesh.CreateNodeAsync(
Path.Combine(Path.GetTempPath(), $"cultmesh-snapshot-endpoint-{Guid.NewGuid():N}.ccmp"),
new CultMeshNodeOptions
{
StartServer = false,
CacheOptions = new CultCacheOpenOptions
{
Registry = sourceCache.Registry
},
DatabaseOptions = new CultNetDatabaseOptions
{
DocumentRegistry = registry
}
});
var syncedDocument = await surface.SyncDocumentAsync<MeshNoteDocument>(node, key.Value);
var endpointHandle = surface.Document<MeshNoteDocument>(key.Value);
var endpointLatest = await endpointHandle.LatestAsync();
var catalog = surface.Documents(
CultMesh.SnapshotDocument<MeshNoteDocument>(key.Value, "daemon:mesh-note:snapshot-endpoint"));
var catalogLatest = await catalog.LatestAsync<MeshNoteDocument>();
await sourceCache.UpsertAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "snapshot-endpoint-synced",
Revision = 20
}, new CultRecordHandle<MeshNoteDocument>(key));
var syncedEndpoint = surface.SyncTo(node);
var syncedHandle = syncedEndpoint.Document<MeshNoteDocument>(key.Value);
var syncedLatest = await syncedHandle.LatestAsync();
await sourceCache.UpsertAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "snapshot-endpoint-catalog-synced",
Revision = 21
}, new CultRecordHandle<MeshNoteDocument>(key));
var syncedCatalog = syncedEndpoint.Documents(
CultMesh.SnapshotDocument<MeshNoteDocument>(key.Value));
var syncedCatalogLatest = await syncedCatalog.LatestAsync<MeshNoteDocument>();
fetched.Text.Should().Be("snapshot-endpoint");
syncedDocument.Text.Should().Be("snapshot-endpoint");
endpointLatest.Revision.Should().Be(19);
catalog.Document<MeshNoteDocument>().DocumentId.Should().Be("daemon:mesh-note:snapshot-endpoint");
catalogLatest.Text.Should().Be("snapshot-endpoint");
syncedLatest.Text.Should().Be("snapshot-endpoint-synced");
node.Cache.Get<MeshNoteDocument>(key)!.Revision.Should().Be(21);
syncedCatalogLatest.Text.Should().Be("snapshot-endpoint-catalog-synced");
requests.Should().HaveCount(6);
requests.Should().OnlyContain(request =>
request.SchemaIds == null &&
request.RecordKeys!.SequenceEqual(new[] { key.Value }) &&
request.ShardId == "primary" &&
request.ShardEpoch == 9 &&
request.MessageId.StartsWith("cultmesh:browser-starfire:snapshot:", StringComparison.Ordinal));
}
[Test]
public async Task CollectionHandle_ReadsAndWatchesDatabaseCollectionsByIndex()
{
var cache = new CultCache();
var database = new CultNetDatabase(cache);
var teamId = Guid.NewGuid().ToString("D");
var collection = CultMesh.CollectionByIndex<MeshIndexedPlayer>(
database,
"TeamId",
teamId);
var changes = new List<CultMeshCollectionChange<MeshIndexedPlayer>>();
using var subscription = collection.WatchChanges(change => changes.Add(change));
await database.PutAsync(
new CultRecordKey("player:one"),
new MeshIndexedPlayer
{
Name = "One",
TeamId = teamId,
Score = 10
});
await database.PutAsync(
new CultRecordKey("player:two"),
new MeshIndexedPlayer
{
Name = "Two",
TeamId = Guid.NewGuid().ToString("D"),
Score = 20
});
collection.CollectionId.Should().Contain(teamId);
collection.SchemaName.Should().Be("tests.mesh_indexed_player");
collection.RouteHint.Kind.Should().Be(CultMeshLocalityKind.Automatic);
collection.Sources.Should().ContainSingle().Which.SchemaId.Should().Be(collection.SchemaId);
var snapshot = await collection.LatestAsync();
snapshot.Should().ContainSingle().Which.Name.Should().Be("One");
changes.Should().ContainSingle();
changes[0].Kind.Should().Be(CultMeshCollectionChangeKind.Added);
changes[0].Document!.Name.Should().Be("One");
}
[Test]
public async Task CollectionHandle_IsIndexedInItsCatalogByTypeAndSchema()
{
var cache = new CultCache();
await cache.UpsertAsync(
new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "collection-alias",
Revision = 4
},
new CultRecordHandle<MeshNoteDocument>(new CultRecordKey("mesh-note:collection")));
var collection = CultMesh.Collection<MeshNoteDocument>(cache);
var catalog = CultMesh.Collections(collection);
catalog.Collections.Should().ContainSingle().Which.Should().BeSameAs(collection);
catalog.TryGetCollection<MeshNoteDocument>(out var byType).Should().BeTrue();
byType.Should().BeSameAs(collection);
catalog.TryGetCollection<MeshOtherDocument>(out _).Should().BeFalse();
catalog.TryGetCollectionBySchema("tests.mesh_note.v1", out var byVersion).Should().BeTrue();
byVersion.Should().BeSameAs(collection);
catalog.CollectionBySchema("tests.mesh_note").Should().BeSameAs(collection);
catalog.CollectionBySchema(CultDocumentRegistry.Shared.GetRequired<MeshNoteDocument>().SchemaId)
.Should()
.BeSameAs(collection);
var snapshot = await collection.LatestAsync();
snapshot.Should().ContainSingle().Which.Text.Should().Be("collection-alias");
var catalogSnapshot = await catalog.LatestAsync<MeshNoteDocument>();
catalogSnapshot.Should().ContainSingle().Which.Revision.Should().Be(4);
CultMeshCollectionChange<MeshNoteDocument> observed = null!;
using var subscription = catalog.WatchChanges<MeshNoteDocument>(change => observed = change);
await cache.UpsertAsync(
new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "collection-alias-updated",
Revision = 5
},
new CultRecordHandle<MeshNoteDocument>(new CultRecordKey("mesh-note:collection")));
observed.Kind.Should().Be(CultMeshCollectionChangeKind.Updated);
observed.Document!.Text.Should().Be("collection-alias-updated");
observed.Document.Revision.Should().Be(5);
observed.PreviousDocument!.Text.Should().Be("collection-alias");
}
[Test]
public async Task DocumentHandle_ReplacesThroughCultMeshNodeDatabase()
{
using var directory = new TemporaryDirectory();
using var node = await CultMesh.CreateNodeAsync(
Path.Combine(directory.Path, "mesh-node.cc"),
new CultMeshNodeOptions
{
DatabaseOptions = new CultNetDatabaseOptions
{
RuntimeId = "node-runtime",
Shards = new[]
{
new CultNetShardDescriptor(
"mesh",
"node-runtime",
epoch: 1,
isPrimary: true,
schemaIds: new[] { CultDocumentRegistry.Shared.GetRequired<MeshNoteDocument>().SchemaId })
}
},
StartServer = false
});
var key = new CultRecordKey("mesh-note:node");
var handle = CultMesh.Document<MeshNoteDocument>(
node,
key,
CultMesh.Verse("starbridge", "unity-pilot"));
MeshNoteDocument observed = null!;
using var subscription = handle.Watch(value => observed = value);
await handle.ReplaceAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "node-replaced",
Revision = 3
});
var snapshot = await handle.LatestAsync();
snapshot.Text.Should().Be("node-replaced");
observed.Text.Should().Be("node-replaced");
observed.Revision.Should().Be(3);
}
[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<MeshViewportRequest, string[]>(
"aetheria.zone.objects.visible",
(_request, _context) => Task.FromResult(Array.Empty<string>()),
sources: sources,
routeHint: route);
var feed = CultMesh.LiveFeed<MeshViewportRequest, string>(
"aetheria.rts.viewport.feed",
(_request, _context) => Task.FromResult("frame"),
sources: sources,
routeHint: route);
var operation = new CultMeshOperationHandle<MeshMoveRequest, CultMeshOperationReceipt>(
"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<string>(),
route,
sources);
var document = CultMesh.Document(
"daemon:aetheria.frame.latest.v1",
CultMesh.Verse("aetheria.local", "unity-raven").Context,
_context => Task.FromResult(new MeshNoteDocument { Schema = "frame", Text = "current", Revision = 1 }),
_context => new Subject<MeshNoteDocument>(),
sources,
route);
var collection = new CultMeshCollectionHandle<MeshNoteDocument>(
"daemon:aetheria.contacts.v1",
() => Task.FromResult<IReadOnlyList<MeshNoteDocument>>(Array.Empty<MeshNoteDocument>()),
() => new Subject<CultMeshCollectionChange<MeshNoteDocument>>(),
sources,
route);
var nativeView = new CultMeshNativeSliceViewDescriptor(
"aetheria.zone.render",
"gamecult.aetheria.render_body.v1",
rowCount: 128,
new[] { CultMeshNativeSliceColumn.For<MeshVec2>("position") },
route);
var catalog = CultMesh.DescribeSurfaceCatalog(
"gamecult.aetheria.rts.surfaces.v1",
new[]
{
CultMesh.DescribeSurface(query),
CultMesh.DescribeSurface(feed),
CultMesh.DescribeSurface(operation),
CultMesh.DescribeSurface(document),
CultMesh.DescribeSurface(collection),
CultMesh.DescribeSurface(pointer),
CultMesh.DescribeSurface(nativeView)
});
catalog.CatalogId.Should().Be("gamecult.aetheria.rts.surfaces.v1");
catalog.Surfaces.Should().HaveCount(7);
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("daemon:aetheria.frame.latest.v1")!.Kind.Should().Be(CultMeshSurfaceKind.Document);
catalog.Find("daemon:aetheria.contacts.v1")!.Kind.Should().Be(CultMeshSurfaceKind.Collection);
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.Documents.Should().ContainSingle().Which.SurfaceId.Should().Be("daemon:aetheria.frame.latest.v1");
index.Collections.Should().ContainSingle().Which.SurfaceId.Should().Be("daemon:aetheria.contacts.v1");
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<int>();
var feed = CultMesh.LiveFeed<MeshViewportRequest, int>(
"aetheria.rts.viewport.feed",
(_request, _context) => Task.FromResult(frameId),
CultMesh.PollingQueryWatcher<MeshViewportRequest, int>(
(_request, _context) => Task.FromResult(frameId),
new CultMeshPollingWatchOptions<int>(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<MeshVec2>("position"),
CultMeshNativeSliceColumn.For<MeshVec2>("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 void NegotiatesGpuTextureStreamsWithoutForcingCopies()
{
var catalog = CultMesh.CreateStreamCatalog();
var stream = new CultMeshStreamDescriptor(
"mimir:kiyo-pro:rgba",
"mimir-live",
"starfire",
CultMeshStreamKind.Video,
new CultMeshStreamClock("mimir:clock", "kiyo-pro", sampleRate: 90_000, confidence: 0.92d),
new[]
{
CultMeshStreamBodyTransport.SharedD3D12Texture,
CultMeshStreamBodyTransport.SharedMemory,
CultMeshStreamBodyTransport.CultCachePage
},
video: new CultMeshVideoStreamFormat(1920, 1080, "rgba8", framesPerSecond: 60),
maxInFlightFrames: 4);
catalog.Declare(stream);
var negotiation = catalog.Negotiate(
stream.StreamId,
new CultMeshStreamConsumerProfile(
"fensalir",
"mimir-live",
new[] { CultMeshStreamBodyTransport.SharedD3D12Texture, CultMeshStreamBodyTransport.CultCachePage },
acceptedKinds: new[] { CultMeshStreamKind.Video },
canImportGpuHandles: true,
maxInFlightFrames: 2));
negotiation.Transport.Should().Be(CultMeshStreamBodyTransport.SharedD3D12Texture);
negotiation.CopyBudget.Should().Be(CultMeshStreamCopyBudget.ZeroCopyTarget);
negotiation.MaxInFlightFrames.Should().Be(2);
var handle = new CultMeshStreamFrameHandle(
stream.StreamId,
sequence: 42,
timestampNs: 123_456_789,
CultMeshStreamBodyTransport.SharedD3D12Texture,
nativeHandle: "shared-handle:0xfeedbeef",
fenceHandle: "fence:0x1234",
fenceValue: 7);
catalog.PublishFrame(handle);
catalog.LatestFrame(stream.StreamId).Should().BeSameAs(handle);
}
[Test]
public void NegotiationRejectsStreamsWithoutACommonTransport()
{
var catalog = CultMesh.CreateStreamCatalog();
catalog.Declare(new CultMeshStreamDescriptor(
"mimir:kiyo-pro:gpu-only",
"mimir-live",
"starfire",
CultMeshStreamKind.Video,
new CultMeshStreamClock("mimir:clock", "kiyo-pro"),
new[] { CultMeshStreamBodyTransport.SharedD3D12Texture }));
var consumer = new CultMeshStreamConsumerProfile(
"cpu-recorder",
"mimir-live",
new[] { CultMeshStreamBodyTransport.SharedMemory },
acceptedKinds: new[] { CultMeshStreamKind.Video });
Action act = () => catalog.Negotiate("mimir:kiyo-pro:gpu-only", consumer);
act.Should().Throw<InvalidOperationException>()
.WithMessage("Stream and consumer have no compatible body transport.");
}
[Test]
public void SharedMemoryRingPublishesWritableSlotsWithoutInternalCopies()
{
var catalog = CatalogWithByteStream();
using var ring = catalog.CreateSharedMemoryRing("mimir:leap:depth", slotCount: 3, slotByteLength: 16);
ring.TryAcquireWriteSlot(out var write).Should().BeTrue();
ReadOnlySpan<byte> seed = stackalloc byte[] { 1, 2, 3, 4 };
seed.CopyTo(write.Span);
var handle = ring.CommitWriteSlot(write, timestampNs: 99, byteLength: 4);
catalog.PublishFrame(handle);
ring.TryAcquireLatestRead(out var read).Should().BeTrue();
using (read)
{
read.Handle.Sequence.Should().Be(0);
read.Handle.UnavoidableCopyCount.Should().Be(0);
read.Span.ToArray().Should().Equal(1, 2, 3, 4);
}
var stats = ring.Stats();
stats.PublishedFrames.Should().Be(1);
stats.UnavoidableCopyCount.Should().Be(0);
catalog.LatestFrame("mimir:leap:depth")!.ResourceKey.Should().Be("mimir:leap:depth:slot:0");
}
[Test]
public void SharedMemoryRingDoesNotOverwriteSlotsHeldByReaders()
{
var catalog = CatalogWithByteStream();
using var ring = catalog.CreateSharedMemoryRing("mimir:leap:depth", slotCount: 3, slotByteLength: 8);
ring.TryAcquireWriteSlot(out var firstWrite).Should().BeTrue();
firstWrite.Span[0] = 11;
ring.CommitWriteSlot(firstWrite, timestampNs: 1, byteLength: 1);
ring.TryAcquireLatestRead(out var read).Should().BeTrue();
ring.TryAcquireWriteSlot(out var secondWrite).Should().BeTrue();
ring.CommitWriteSlot(secondWrite, timestampNs: 2, byteLength: 1);
ring.TryAcquireWriteSlot(out var thirdWrite).Should().BeTrue();
ring.CommitWriteSlot(thirdWrite, timestampNs: 3, byteLength: 1);
ring.TryAcquireLatestRead(out var thirdRead).Should().BeTrue();
ring.TryAcquireWriteSlot(out var overwriteSecond).Should().BeTrue();
ring.CommitWriteSlot(overwriteSecond, timestampNs: 4, byteLength: 1);
ring.TryAcquireLatestRead(out var secondRead).Should().BeTrue();
ring.TryAcquireWriteSlot(out _).Should().BeFalse();
ring.Stats().BlockedWrites.Should().Be(1);
read.Dispose();
thirdRead.Dispose();
secondRead.Dispose();
ring.TryAcquireWriteSlot(out var fifthWrite).Should().BeTrue();
fifthWrite.Span[0] = 12;
ring.CommitWriteSlot(fifthWrite, timestampNs: 5, byteLength: 1);
var stats = ring.Stats();
stats.PublishedFrames.Should().Be(5);
stats.DroppedFrames.Should().Be(2);
stats.LatestSequence.Should().Be(4);
}
[Test]
public void CopyPublishMarksFallbackCopiesExplicitly()
{
var catalog = CatalogWithByteStream();
using var ring = catalog.CreateSharedMemoryRing("mimir:leap:depth", slotCount: 3, slotByteLength: 8);
ring.TryPublishCopy(stackalloc byte[] { 5, 6, 7 }, timestampNs: 10, durationNs: 2, out var handle)
.Should()
.BeTrue();
handle.UnavoidableCopyCount.Should().Be(1);
ring.Stats().UnavoidableCopyCount.Should().Be(1);
}
private static CultMeshStreamCatalog CatalogWithByteStream()
{
var catalog = CultMesh.CreateStreamCatalog();
catalog.Declare(new CultMeshStreamDescriptor(
"mimir:leap:depth",
"mimir-live",
"starfire",
CultMeshStreamKind.Tensor,
new CultMeshStreamClock("mimir:clock", "leap", confidence: 0.8d),
new[] { CultMeshStreamBodyTransport.SharedMemory, CultMeshStreamBodyTransport.CultCachePage }));
return catalog;
}
private static async Task WaitForAsync(Func<bool> predicate)
{
var deadline = DateTimeOffset.UtcNow.AddSeconds(2);
while (DateTimeOffset.UtcNow < deadline)
{
if (predicate())
{
return;
}
await Task.Delay(20);
}
predicate().Should().BeTrue();
}
private sealed class TemporaryDirectory : IDisposable
{
public TemporaryDirectory()
{
Path = System.IO.Path.Combine(
System.IO.Path.GetTempPath(),
"cultmesh-tests-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(Path);
}
public string Path { get; }
public void Dispose()
{
if (Directory.Exists(Path))
{
Directory.Delete(Path, recursive: true);
}
}
}
[CultDocument("tests.mesh_indexed_player", "tests.mesh_indexed_player.v1")]
[MessagePackObject(AllowPrivate = true)]
internal sealed class MeshIndexedPlayer
{
[Key(0)]
public string Name { get; set; } = string.Empty;
[Key(1)]
[CultIndex]
public string TeamId { get; set; } = string.Empty;
[Key(2)]
public int Score { get; set; }
}
[CultDocument("tests.mesh_note", "tests.mesh_note.v1")]
[MessagePackObject(AllowPrivate = true)]
internal sealed class MeshNoteDocument
{
[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_publication_note", "tests.mesh_publication_note.v1")]
[MessagePackObject(AllowPrivate = true)]
internal sealed class MeshPublicationNoteDocument
{
[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<MeshMoveRequest, CultMeshOperationReceipt> _move;
public MeshPilotFacade(CultMeshVerseContext context, int entityId)
{
_entityId = entityId;
_move = CultMesh.BindOperation(
context,
new CultMeshOperationHandle<MeshMoveRequest, CultMeshOperationReceipt>(
"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<CultMeshOperationReceipt> 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<MeshViewportRequest, string[]> _visibleObjects;
public MeshObjectsFacade(CultMeshVerseContext context, string zoneId)
{
_zoneId = zoneId;
_visibleObjects = CultMesh.BindQuery(
context,
new CultMeshQuerySurface<MeshViewportRequest, string[]>(
"aetheria.zone.objects.visible",
(parameters, context) => Task.FromResult(new[]
{
$"{context.RuntimeId}:{_zoneId}:{parameters.MinX}:{parameters.MaxX}:{context.RouteHint.Kind}"
})));
}
public Task<string[]> VisibleWithinAsync(MeshViewportRequest request)
{
return _visibleObjects.ExecuteAsync(request);
}
}
internal sealed class MeshSnapshotSchemaClient : ICultNetSchemaClient
{
private readonly Func<CultNetSnapshotRequestMessage, CultNetSnapshotResponseRawMessage> _respond;
private readonly List<Action<CultNetSnapshotResponseRawMessage>> _snapshotHandlers = new();
private readonly List<Action<CultNetErrorMessage>> _errorHandlers = new();
public MeshSnapshotSchemaClient(
Func<CultNetSnapshotRequestMessage, CultNetSnapshotResponseRawMessage> respond)
{
_respond = respond;
}
public bool Connected { get; private set; }
public int ConnectCount { get; private set; }
public void Connect(string host, int port)
{
ConnectCount++;
Connected = true;
}
public void SendCultNet<T>(T message) where T : ICultNetSchemaMessage
{
if (message is not CultNetSnapshotRequestMessage request)
{
foreach (var handler in _errorHandlers)
{
handler(new CultNetErrorMessage { Error = $"Unexpected message {typeof(T).Name}." });
}
return;
}
var response = _respond(request);
foreach (var handler in _snapshotHandlers)
{
handler(response);
}
}
public void OnCultNet<T>(Action<T> callback) where T : ICultNetSchemaMessage
{
if (typeof(T) == typeof(CultNetSnapshotResponseRawMessage))
{
_snapshotHandlers.Add(message => callback((T)(object)message));
}
else if (typeof(T) == typeof(CultNetErrorMessage))
{
_errorHandlers.Add(message => callback((T)(object)message));
}
}
public void Dispose()
{
}
}
internal sealed class FailedBackgroundSchemaClient : ICultNetSchemaClient, ICultNetSchemaClientHealth
{
private readonly bool _connected;
public FailedBackgroundSchemaClient(Exception error, bool connected = true)
{
_connected = connected;
BackgroundFailure = Task.FromResult(error);
}
public bool Connected { get; private set; }
public Task<Exception> BackgroundFailure { get; }
public void Connect(string host, int port) => Connected = _connected;
public void SendCultNet<T>(T message) where T : ICultNetSchemaMessage
{
}
public void OnCultNet<T>(Action<T> callback) where T : ICultNetSchemaMessage
{
}
public void Dispose()
{
}
}
internal sealed class BlockingCountClock : ICultMeshClock
{
private int _delayCount;
public DateTimeOffset UtcNow => DateTimeOffset.UnixEpoch;
public int DelayCount => Volatile.Read(ref _delayCount);
public Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken = default)
{
Interlocked.Increment(ref _delayCount);
return Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
}
}