CultMesh: AsSchemaAlias is gone; a catalog finds a handle by its type

F7 (operator, 2026-09-30): "Schema aliasing sounds like a really bad idea,
I support retiring it." One CLR type owns a schema, so a handle never needs
converting to another type for the same schema.

- Delete AsSchemaAlias from the document and collection handles and their
  interfaces, with the MessagePack round-trip converters only it used.
- The document and collection catalogs look a typed handle up by its type.
  The schema-name/version fallback existed only to find a handle of another
  type and alias it.
- Tests: the MeshNoteAliasDocument fixture is deleted. Tests that used it
  for publication, sync, snapshot and prediction paths now use
  MeshNoteDocument; the one test that only checked alias refusal is deleted.
  Catalog tests check a type with no handle is not found.
- public-api.md stops documenting aliasing; a stale comment in
  ReadDocumentFromSnapshotResponse that cited the fixture is trimmed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-30 16:15:48 +02:00
commit 1e0455028c
5 changed files with 140 additions and 338 deletions

View file

@ -2728,11 +2728,9 @@ namespace GameCult.Mesh
// runtime does not recognize by any alias) its payload's own embedded schema stamp,
// alias-matched against TDocument's descriptor. The parse step
// (CultNetDocumentRegistry.TryReadSchemaVersion) is the one decode rule shared with
// DecodeSnapshotDocuments; the match itself stays a direct alias-match against the caller's
// own descriptor rather than a global "which registered type owns this schema string" lookup,
// because that lookup is ambiguous when two document types alias the same schema id (as
// MeshNoteDocument/MeshNoteAliasDocument do in tests/GameCult.Mesh.Tests). No re-filter of the
// server's answer by schema alone, and no fallback that returns a record under the wrong key.
// DecodeSnapshotDocuments; the match itself is a direct alias-match against the caller's own
// descriptor. No re-filter of the server's answer by schema alone, and no fallback that
// returns a record under the wrong key.
private static TDocument ReadDocumentFromSnapshotResponse<TDocument>(
CultNetSnapshotResponseRawMessage response,
string schemaId,

View file

@ -910,13 +910,10 @@ namespace GameCult.Mesh
/// <summary>Gets whether this handle can submit a client prediction for the underlying document value.</summary>
bool CanSubmitPrediction { get; }
/// <summary>Creates a same-schema alias presentation for another CLR document type.</summary>
CultMeshDocumentHandle<TAlias> AsSchemaAlias<TAlias>() where TAlias : class;
}
/// <summary>
/// Typed reactive document handle with schema-aware alias conversion.
/// Typed reactive document handle.
/// </summary>
public sealed class CultMeshDocumentHandle<TDocument> : ICultMeshDocumentHandle
where TDocument : class
@ -1117,39 +1114,6 @@ namespace GameCult.Mesh
return ObserveAsync().ConfigureAwait(false).GetAwaiter().GetResult();
}
/// <summary>Creates a same-schema alias presentation for another CLR document type.</summary>
public CultMeshDocumentHandle<TAlias> AsSchemaAlias<TAlias>() where TAlias : class
{
var aliasDescriptor = CultDocumentRegistry.Shared.GetRequired<TAlias>();
if (!string.Equals(Descriptor.SchemaName, aliasDescriptor.SchemaName, StringComparison.Ordinal) ||
!string.Equals(Descriptor.SchemaVersion, aliasDescriptor.SchemaVersion, StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"Document type {typeof(TAlias).FullName} uses schema '{aliasDescriptor.SchemaName}' " +
$"version '{aliasDescriptor.SchemaVersion}', but handle '{DocumentId}' exposes " +
$"schema '{SchemaName}' version '{SchemaVersion}'.");
}
var aliasFeed = new CultMeshLiveFeed<CultMeshDocumentQueryParameters, TAlias>(
DocumentId,
async (parameters, context) => ConvertDocument<TDocument, TAlias>(
await Feed.SnapshotAsync(parameters, context).ConfigureAwait(false)),
(parameters, context) => Feed
.Watch(parameters, context)
.Select(ConvertDocument<TDocument, TAlias>),
Sources,
RouteHint);
Func<TAlias, Task>? replace = _replace == null
? null
: value => _replace(ConvertDocument<TAlias, TDocument>(value));
Func<TAlias, Task>? submitPrediction = _submitPrediction == null
? null
: value => _submitPrediction(ConvertDocument<TAlias, TDocument>(value));
return new CultMeshDocumentHandle<TAlias>(aliasFeed.Bind(Context), replace, submitPrediction);
}
internal static TDocumentValue CloneDocument<TDocumentValue>(TDocumentValue document)
where TDocumentValue : class
{
@ -1216,19 +1180,6 @@ namespace GameCult.Mesh
"field");
}
internal static TTarget ConvertDocument<TSource, TTarget>(TSource document)
where TSource : class
where TTarget : class
{
if (document == null) throw new ArgumentNullException(nameof(document));
if (document is TTarget alreadyTyped)
{
return alreadyTyped;
}
var payload = CultDocumentMessagePackSerialization.SerializeUntyped(document, typeof(TSource));
return (TTarget)CultDocumentMessagePackSerialization.DeserializeUntyped(typeof(TTarget), payload);
}
}
/// <summary>
@ -1912,11 +1863,12 @@ namespace GameCult.Mesh
$"CultMesh document catalog does not expose a document for schema '{schema}'.");
}
/// <summary>Looks up a typed document handle by CLR type or same-schema alias.</summary>
/// <summary>Looks up a typed document handle by CLR type.</summary>
public bool TryGetDocument<TDocument>(
out CultMeshDocumentHandle<TDocument> document)
where TDocument : class
{
// One type owns a schema, so the handle for TDocument's schema is the handle for TDocument.
if (_documentsByType.TryGetValue(typeof(TDocument), out var untypedDocument) &&
untypedDocument is CultMeshDocumentHandle<TDocument> typedDocument)
{
@ -1924,25 +1876,11 @@ namespace GameCult.Mesh
return true;
}
var descriptor = CultDocumentRegistry.Shared.GetRequired<TDocument>();
if (_documentsBySchema.TryGetValue(descriptor.SchemaVersion, out var schemaDocument) ||
_documentsBySchema.TryGetValue(descriptor.SchemaName, out schemaDocument!))
{
if (schemaDocument is CultMeshDocumentHandle<TDocument> schemaTypedDocument)
{
document = schemaTypedDocument;
return true;
}
document = schemaDocument.AsSchemaAlias<TDocument>();
return true;
}
document = null!;
return false;
}
/// <summary>Looks up a typed document handle by CLR type or same-schema alias.</summary>
/// <summary>Looks up a typed document handle by CLR type.</summary>
public CultMeshDocumentHandle<TDocument> Document<TDocument>()
where TDocument : class
{
@ -1953,70 +1891,70 @@ namespace GameCult.Mesh
$"CultMesh document catalog does not expose a document for {typeof(TDocument).FullName}.");
}
/// <summary>Reads one typed document snapshot by CLR type or same-schema alias.</summary>
/// <summary>Reads one typed document snapshot by CLR type.</summary>
public Task<TDocument> LatestAsync<TDocument>()
where TDocument : class
{
return Document<TDocument>().LatestAsync();
}
/// <summary>Reads one typed document synchronously by CLR type or same-schema alias.</summary>
/// <summary>Reads one typed document synchronously by CLR type.</summary>
public TDocument Latest<TDocument>()
where TDocument : class
{
return Document<TDocument>().Latest();
}
/// <summary>Gets whether one typed document can be replaced by CLR type or same-schema alias.</summary>
/// <summary>Gets whether one typed document can be replaced by CLR type.</summary>
public bool CanReplace<TDocument>()
where TDocument : class
{
return TryGetDocument<TDocument>(out var document) && document.CanReplace;
}
/// <summary>Replaces one typed document by CLR type or same-schema alias.</summary>
/// <summary>Replaces one typed document by CLR type.</summary>
public Task ReplaceAsync<TDocument>(TDocument value)
where TDocument : class
{
return Document<TDocument>().ReplaceAsync(value);
}
/// <summary>Gets whether one typed document can submit client predictions by CLR type or same-schema alias.</summary>
/// <summary>Gets whether one typed document can submit client predictions by CLR type.</summary>
public bool CanSubmitPrediction<TDocument>()
where TDocument : class
{
return TryGetDocument<TDocument>(out var document) && document.CanSubmitPrediction;
}
/// <summary>Submits a client prediction for one typed document by CLR type or same-schema alias.</summary>
/// <summary>Submits a client prediction for one typed document by CLR type.</summary>
public Task SubmitPredictionAsync<TDocument>(TDocument value)
where TDocument : class
{
return Document<TDocument>().SubmitPredictionAsync(value);
}
/// <summary>Gets an explicit authoritative writer by CLR type or same-schema alias.</summary>
/// <summary>Gets an explicit authoritative writer by CLR type.</summary>
public CultMeshDocumentWriter<TDocument> AuthoritativeWriter<TDocument>()
where TDocument : class
{
return Document<TDocument>().AuthoritativeWriter();
}
/// <summary>Gets an explicit prediction writer by CLR type or same-schema alias.</summary>
/// <summary>Gets an explicit prediction writer by CLR type.</summary>
public CultMeshDocumentWriter<TDocument> PredictionWriter<TDocument>()
where TDocument : class
{
return Document<TDocument>().PredictionWriter();
}
/// <summary>Watches one typed document by CLR type or same-schema alias.</summary>
/// <summary>Watches one typed document by CLR type.</summary>
public Observable<TDocument> Watch<TDocument>()
where TDocument : class
{
return Document<TDocument>().Watch();
}
/// <summary>Subscribes to one typed document by CLR type or same-schema alias.</summary>
/// <summary>Subscribes to one typed document by CLR type.</summary>
public IDisposable Watch<TDocument>(Action<TDocument> onNext)
where TDocument : class
{
@ -2050,9 +1988,6 @@ namespace GameCult.Mesh
/// <summary>Gets typed state sources this collection handle depends on, when known.</summary>
IReadOnlyList<CultMeshProjectionSource> Sources { get; }
/// <summary>Creates a same-schema alias presentation for another CLR document type.</summary>
CultMeshCollectionHandle<TAlias> AsSchemaAlias<TAlias>() where TAlias : class;
}
/// <summary>
@ -2118,11 +2053,12 @@ namespace GameCult.Mesh
$"CultMesh collection catalog does not expose a collection for schema '{schema}'.");
}
/// <summary>Looks up a typed collection handle by CLR type or same-schema alias.</summary>
/// <summary>Looks up a typed collection handle by CLR type.</summary>
public bool TryGetCollection<TDocument>(
out CultMeshCollectionHandle<TDocument> collection)
where TDocument : class
{
// One type owns a schema, so the handle for TDocument's schema is the handle for TDocument.
if (_collectionsByType.TryGetValue(typeof(TDocument), out var untypedCollection) &&
untypedCollection is CultMeshCollectionHandle<TDocument> typedCollection)
{
@ -2130,26 +2066,11 @@ namespace GameCult.Mesh
return true;
}
var descriptor = CultDocumentRegistry.Shared.GetRequired<TDocument>();
if (_collectionsBySchema.TryGetValue(descriptor.SchemaId, out var schemaCollection) ||
_collectionsBySchema.TryGetValue(descriptor.SchemaVersion, out schemaCollection!) ||
_collectionsBySchema.TryGetValue(descriptor.SchemaName, out schemaCollection!))
{
if (schemaCollection is CultMeshCollectionHandle<TDocument> schemaTypedCollection)
{
collection = schemaTypedCollection;
return true;
}
collection = schemaCollection.AsSchemaAlias<TDocument>();
return true;
}
collection = null!;
return false;
}
/// <summary>Looks up a typed collection handle by CLR type or same-schema alias.</summary>
/// <summary>Looks up a typed collection handle by CLR type.</summary>
public CultMeshCollectionHandle<TDocument> Collection<TDocument>()
where TDocument : class
{
@ -2160,21 +2081,21 @@ namespace GameCult.Mesh
$"CultMesh collection catalog does not expose a collection for {typeof(TDocument).FullName}.");
}
/// <summary>Reads one typed collection snapshot by CLR type or same-schema alias.</summary>
/// <summary>Reads one typed collection snapshot by CLR type.</summary>
public Task<IReadOnlyList<TDocument>> LatestAsync<TDocument>()
where TDocument : class
{
return Collection<TDocument>().LatestAsync();
}
/// <summary>Watches one typed collection by CLR type or same-schema alias.</summary>
/// <summary>Watches one typed collection by CLR type.</summary>
public Observable<CultMeshCollectionChange<TDocument>> WatchChanges<TDocument>()
where TDocument : class
{
return Collection<TDocument>().WatchChanges();
}
/// <summary>Subscribes to one typed collection by CLR type or same-schema alias.</summary>
/// <summary>Subscribes to one typed collection by CLR type.</summary>
public IDisposable WatchChanges<TDocument>(Action<CultMeshCollectionChange<TDocument>> onNext)
where TDocument : class
{
@ -2246,7 +2167,7 @@ namespace GameCult.Mesh
}
/// <summary>
/// Typed reactive collection handle with schema-aware alias conversion.
/// Typed reactive collection handle.
/// </summary>
public sealed class CultMeshCollectionHandle<TDocument> : ICultMeshCollectionHandle
where TDocument : class
@ -2313,49 +2234,6 @@ namespace GameCult.Mesh
if (onNext == null) throw new ArgumentNullException(nameof(onNext));
return WatchChanges().Subscribe(onNext);
}
/// <summary>Creates a same-schema alias presentation for another CLR document type.</summary>
public CultMeshCollectionHandle<TAlias> AsSchemaAlias<TAlias>() where TAlias : class
{
var aliasDescriptor = CultDocumentRegistry.Shared.GetRequired<TAlias>();
if (!string.Equals(Descriptor.SchemaName, aliasDescriptor.SchemaName, StringComparison.Ordinal) ||
!string.Equals(Descriptor.SchemaVersion, aliasDescriptor.SchemaVersion, StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"Document type {typeof(TAlias).FullName} uses schema '{aliasDescriptor.SchemaName}' " +
$"version '{aliasDescriptor.SchemaVersion}', but collection '{CollectionId}' exposes " +
$"schema '{SchemaName}' version '{SchemaVersion}'.");
}
return new CultMeshCollectionHandle<TAlias>(
CollectionId,
async () => (await LatestAsync().ConfigureAwait(false))
.Select(ConvertDocument<TDocument, TAlias>)
.ToArray(),
() => WatchChanges().Select(change => new CultMeshCollectionChange<TAlias>(
change.Kind,
change.Key,
change.SchemaId,
change.Document == null ? null : ConvertDocument<TDocument, TAlias>(change.Document),
change.PreviousDocument == null ? null : ConvertDocument<TDocument, TAlias>(change.PreviousDocument),
change.Message)),
Sources,
RouteHint);
}
private static TTarget ConvertDocument<TSource, TTarget>(TSource document)
where TSource : class
where TTarget : class
{
if (document == null) throw new ArgumentNullException(nameof(document));
if (document is TTarget alreadyTyped)
{
return alreadyTyped;
}
var payload = CultDocumentMessagePackSerialization.SerializeUntyped(document, typeof(TSource));
return (TTarget)CultDocumentMessagePackSerialization.DeserializeUntyped(typeof(TTarget), payload);
}
}
/// <summary>

View file

@ -979,11 +979,11 @@ namespace GameCult.Mesh
var canDeserializeWithBinding =
binding != null &&
typeof(TDocument).IsAssignableFrom(binding.DocumentType);
var canDeserializeAsSchemaAlias =
var canDeserializeByWireSchema =
CultNetSchemaAliasMatching.Matches(record.SchemaId, descriptor) ||
(binding != null && CultNetSchemaAliasMatching.Matches(binding.SchemaId, descriptor)) ||
CultNetDocumentRegistry.PayloadMatchesSchema(record.Payload, descriptor);
if (!canDeserializeWithBinding && !canDeserializeAsSchemaAlias)
if (!canDeserializeWithBinding && !canDeserializeByWireSchema)
continue;
if (!string.Equals(record.PayloadEncoding, "messagepack", StringComparison.Ordinal))

View file

@ -173,28 +173,22 @@ portable machinery that makes it feel native.
the normal shard log. Callers can bind a projected live feed, a local
`CultCache` record, a distributed `CultNetDatabase` record, or a
`CultMeshNode` record through the same surface instead of walking transport
and projection layers themselves. Same-schema CLR aliases can be requested
directly from node/database/snapshot surfaces; CultMesh resolves the canonical
registered document by shared `[CultDocument(schemaName, schemaVersion)]`
identity and uses the shared CultCache MessagePack codec for
read/watch/replace/prediction conversion. `AsSchemaAlias<TAlias>()` remains
available when a caller already has a handle and wants to project it.
and projection layers themselves. One CLR type owns each
`[CultDocument(schemaName, schemaVersion)]` schema; the registry refuses a
second type for the same schema with `CultSchemaConflictException`.
- `CultMeshDocumentCatalog` is the schema-aware lookup edge for a set of
document handles. It indexes handles by CLR document type, schema name, and
schema version, and resolves same-schema CLR aliases by delegating to
`CultMeshDocumentHandle<TDocument>.AsSchemaAlias<TAlias>()`. Domain facades
can expose `Document<T>()`, `DocumentBySchema(...)`, `LatestAsync<T>()`, and
`Watch<T>()` without owning schema dictionaries or alias serializers. Mutable
facades can also call `ReplaceAsync<T>(...)` or
`SubmitPredictionAsync<T>(...)` directly on the catalog when they only need
the schema-aliased operation and not the intermediate handle.
document handles. It indexes handles by CLR document type, schema id, schema
name, and schema version. Domain facades can expose `Document<T>()`,
`DocumentBySchema(...)`, `LatestAsync<T>()`, and `Watch<T>()` without owning
schema dictionaries. Mutable facades can also call `ReplaceAsync<T>(...)` or
`SubmitPredictionAsync<T>(...)` directly on the catalog when they do not need
the intermediate handle.
- `CultMeshCollectionHandle<TDocument>` is the typed reactive edge for
multi-record state. It exposes one `LatestAsync()` collection snapshot and a
`WatchChanges()` stream while hiding whether the backing source is an
in-process `CultCache`, a distributed `CultNetDatabase`, or a `CultMeshNode`.
Collection handles can represent all records of a document type, a named
document view, or an indexed value view, and they support the same
same-schema CLR alias conversion as document handles.
document view, or an indexed value view.
- `CultMeshCollectionCatalog` is the collection sibling of
`CultMeshDocumentCatalog`. It indexes typed collection handles by CLR
document type, schema id, schema name, and schema version, then exposes
@ -258,19 +252,17 @@ var objects = await visibleObjects.ExecuteAsync(viewportRequest);
await movePilot.InvokeAsync(moveRequest, idempotencyKey);
```
A single typed document can be surfaced the same way. The caller asks for the
typed view it wants; if that type is a same-schema alias of the registered
canonical document, CultMesh still opens the correct backing record:
A single typed document can be surfaced the same way:
```csharp
var stockForUi = CultMesh.Document<StationStockUiDocument>(
var stockHandle = CultMesh.Document<StationStockDocument>(
node,
new CultRecordKey("station:starbridge:stock"),
verse);
var stock = await stockForUi.LatestAsync();
using var watch = stockForUi.Watch(RenderStock);
await stockForUi.SetAsync(updatedStockFromUi);
var stock = await stockHandle.LatestAsync();
using var watch = stockHandle.Watch(RenderStock);
await stockHandle.SetAsync(updatedStock);
```
Projected or derived documents can use the same handle shape with explicit
@ -291,8 +283,6 @@ var cockpit = CultMesh.Document(
var current = await cockpit.LatestAsync();
using var subscription = cockpit.Watch(next => RenderCockpit(next));
var daemonAlias = cockpit.AsSchemaAlias<DaemonCockpitState>();
```
Remote CultNet snapshots use the same document handle surface. The caller names
@ -301,20 +291,18 @@ id, response filtering, raw MessagePack payload decode, route metadata, and
polling watch fallback:
```csharp
var remoteHealth = CultMesh.DocumentFromPeerSnapshot<DaemonHealthUiDocument>(
var remoteHealth = CultMesh.DocumentFromPeerSnapshot<DaemonHealthDocument>(
"cultnet://daemon.local:3075",
"daemon:aetheria.health.v1",
verse);
var uiHealth = await remoteHealth.LatestAsync();
var health = await remoteHealth.LatestAsync();
```
Snapshot requests are record-key-first so same logical documents can still be
read when another runtime generated a different schema id for a compatible
schema alias. The decode path prefers exact schema id matches when present and
falls back to the requested record key. Ask for the runtime-facing alias type
directly; `AsSchemaAlias<TAlias>()` is for adapting handles that were already
constructed elsewhere.
Snapshot requests are record-key-first so the same logical document can still be
read when another runtime generated a different schema id for it. The decode
path prefers exact schema id matches when present and falls back to the
requested record key.
A domain facade can still collect existing handles into one schema-aware
catalog when it is composing pre-built surfaces:
@ -326,7 +314,7 @@ var documents = CultMesh.Documents(
zoneContacts);
var refit = await documents.LatestAsync<StationRefitDocument>();
var uiDocking = documents.Document<CurrentDockingUiDocument>();
var docking = documents.Document<CurrentDockingDocument>();
var bySchema = documents.DocumentBySchema("gamecult.aetheria.station_refit.v1");
```
@ -341,11 +329,9 @@ var allies = CultMesh.CollectionByIndex<PlayerShipDocument>(
var currentAllies = await allies.LatestAsync();
using var changes = allies.WatchChanges(change => UpdateRoster(change));
var uiAllies = allies.AsSchemaAlias<PlayerShipUiDocument>();
var collections = CultMesh.Collections(allies);
var uiRoster = await collections.LatestAsync<PlayerShipUiDocument>();
using var uiRosterChanges = collections.WatchChanges<PlayerShipUiDocument>(RenderRoster);
var roster = await collections.LatestAsync<PlayerShipDocument>();
using var rosterChanges = collections.WatchChanges<PlayerShipDocument>(RenderRoster);
```
```ts

View file

@ -696,7 +696,7 @@ public sealed class CultMeshStreamingTests
}
[Test]
public async Task DocumentHandle_ExposesTypedSnapshotsAndSameSchemaAliases()
public async Task DocumentHandle_ExposesTypedSnapshotsAndWatches()
{
var subject = new Subject<MeshNoteDocument>();
var route = new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "co-located document slab");
@ -733,17 +733,8 @@ public sealed class CultMeshStreamingTests
snapshot.Text.Should().Be("primary");
handle.Latest().Text.Should().Be("primary");
var alias = handle.AsSchemaAlias<MeshNoteAliasDocument>();
var aliasSnapshot = await alias.LatestAsync();
alias.Latest().Text.Should().Be("primary");
alias.DocumentId.Should().Be(handle.DocumentId);
alias.SchemaName.Should().Be(handle.SchemaName);
alias.SchemaVersion.Should().Be(handle.SchemaVersion);
aliasSnapshot.Text.Should().Be("primary");
aliasSnapshot.Revision.Should().Be(1);
MeshNoteAliasDocument observed = null!;
using var subscription = alias.Watch(value => observed = value);
MeshNoteDocument observed = null!;
using var subscription = handle.Watch(value => observed = value);
subject.OnNext(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
@ -790,28 +781,7 @@ public sealed class CultMeshStreamingTests
}
[Test]
public void DocumentHandle_RejectsAliasTypesWithDifferentSchemaIdentity()
{
var handle = CultMesh.Document(
"mesh.note.current",
CultMesh.Verse("starbridge", "unity-pilot"),
_ => Task.FromResult(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "primary",
Revision = 1
}),
_ => new Subject<MeshNoteDocument>());
Action act = () => handle.AsSchemaAlias<MeshOtherDocument>();
act.Should()
.Throw<InvalidOperationException>()
.WithMessage("*tests.mesh_other*tests.mesh_note*");
}
[Test]
public async Task DocumentCatalog_IndexesHandlesByTypeAndSchemaAlias()
public async Task DocumentCatalog_IndexesHandlesByTypeAndSchema()
{
var current = new MeshNoteDocument
{
@ -836,15 +806,12 @@ public sealed class CultMeshStreamingTests
.Should()
.BeSameAs(handle);
catalog.TryGetDocument<MeshNoteAliasDocument>(out var alias).Should().BeTrue();
alias.DocumentId.Should().Be(handle.DocumentId);
alias.SchemaName.Should().Be(handle.SchemaName);
alias.SchemaVersion.Should().Be(handle.SchemaVersion);
catalog.TryGetDocument<MeshOtherDocument>(out _).Should().BeFalse();
var aliasSnapshot = await catalog.LatestAsync<MeshNoteAliasDocument>();
aliasSnapshot.Text.Should().Be("catalog-primary");
aliasSnapshot.Revision.Should().Be(3);
catalog.Latest<MeshNoteAliasDocument>().Text.Should().Be("catalog-primary");
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]
@ -894,21 +861,18 @@ public sealed class CultMeshStreamingTests
cache,
key,
CultMesh.Verse("starbridge", "unity-pilot"));
var alias = handle.AsSchemaAlias<MeshNoteAliasDocument>();
handle.CanReplace.Should().BeTrue();
alias.CanReplace.Should().BeTrue();
handle.CanSubmitPrediction.Should().BeFalse();
alias.CanSubmitPrediction.Should().BeFalse();
handle.DocumentId.Should().Be(key.Value);
handle.Sources.Should().ContainSingle().Which.SchemaId.Should().Be(handle.SchemaId);
var snapshot = await alias.LatestAsync();
var snapshot = await handle.LatestAsync();
snapshot.Text.Should().Be("cache-initial");
MeshNoteAliasDocument observed = null!;
using var subscription = alias.Watch(value => observed = value);
await alias.ReplaceAsync(new MeshNoteAliasDocument
MeshNoteDocument observed = null!;
using var subscription = handle.Watch(value => observed = value);
await handle.ReplaceAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "cache-replaced",
@ -920,8 +884,8 @@ public sealed class CultMeshStreamingTests
observed.Revision.Should().Be(2);
var catalog = CultMesh.Documents(handle);
catalog.CanReplace<MeshNoteAliasDocument>().Should().BeTrue();
await catalog.ReplaceAsync(new MeshNoteAliasDocument
catalog.CanReplace<MeshNoteDocument>().Should().BeTrue();
await catalog.ReplaceAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "cache-catalog-replaced",
@ -929,7 +893,7 @@ public sealed class CultMeshStreamingTests
});
cache.Get<MeshNoteDocument>(key)!.Text.Should().Be("cache-catalog-replaced");
Func<Task> act = () => alias.SubmitPredictionAsync(new MeshNoteAliasDocument
Func<Task> act = () => handle.SubmitPredictionAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "cache-predicted",
@ -970,19 +934,17 @@ public sealed class CultMeshStreamingTests
database,
key,
CultMesh.Verse("starbridge", "pilot-a"));
var alias = handle.AsSchemaAlias<MeshNoteAliasDocument>();
handle.CanReplace.Should().BeFalse();
alias.CanReplace.Should().BeFalse();
alias.CanSubmitPrediction.Should().BeTrue();
handle.CanSubmitPrediction.Should().BeTrue();
var catalog = CultMesh.Documents(handle);
catalog.CanSubmitPrediction<MeshNoteAliasDocument>().Should().BeTrue();
var predictionWriter = catalog.PredictionWriter<MeshNoteAliasDocument>();
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 MeshNoteAliasDocument
await predictionWriter.WriteAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "predicted-thermal",
@ -994,7 +956,7 @@ public sealed class CultMeshStreamingTests
observed.Revision.Should().Be(5);
(await handle.LatestAsync()).Text.Should().Be("predicted-thermal");
var updated = await alias.PredictionWriter().UpdateAsync(value => new MeshNoteAliasDocument
var updated = await handle.PredictionWriter().UpdateAsync(value => new MeshNoteDocument
{
Schema = value.Schema,
Text = "updated-as-prediction",
@ -1035,7 +997,7 @@ public sealed class CultMeshStreamingTests
});
var catalog = CultMesh.Documents(handle);
using var reactive = await catalog.PredictionWriter<MeshNoteAliasDocument>().ReactiveAsync(
using var reactive = await catalog.PredictionWriter<MeshNoteDocument>().ReactiveAsync(
new CultMeshReactiveDocumentOptions
{
FlushDelay = TimeSpan.FromMinutes(1)
@ -1645,7 +1607,7 @@ public sealed class CultMeshStreamingTests
}
[Test]
public async Task ReactiveDocument_SyncsSameSchemaAliasesAcrossRuntimeHandles()
public async Task ReactiveDocument_SyncsAcrossRuntimeHandles()
{
var cache = new CultCache();
var schemaId = CultDocumentRegistry.Shared.GetRequired<MeshNoteDocument>().SchemaId;
@ -1686,13 +1648,11 @@ public sealed class CultMeshStreamingTests
database,
key,
CultMesh.Verse("starbridge", "commander-rts"));
var observedByCommander = new List<MeshNoteAliasDocument>();
using var commanderSubscription = commanderHandle
.AsSchemaAlias<MeshNoteAliasDocument>()
.Watch(observedByCommander.Add);
var observedByCommander = new List<MeshNoteDocument>();
using var commanderSubscription = commanderHandle.Watch(observedByCommander.Add);
using var pilotReactive = await CultMesh
.Documents(pilotHandle)
.PredictionWriter<MeshNoteAliasDocument>()
.PredictionWriter<MeshNoteDocument>()
.ReactiveAsync(
new CultMeshReactiveDocumentOptions
{
@ -1710,7 +1670,7 @@ public sealed class CultMeshStreamingTests
commanderHandle.Context.RuntimeId.Should().Be("commander-rts");
observedByCommander.Last().Revision.Should().Be(2);
(await commanderHandle.AsSchemaAlias<MeshNoteAliasDocument>().LatestAsync()).Text.Should().Be("pilot-predicted");
(await commanderHandle.LatestAsync()).Text.Should().Be("pilot-predicted");
}
[Test]
@ -1755,12 +1715,9 @@ public sealed class CultMeshStreamingTests
database,
key,
CultMesh.Verse("starbridge", "commander-rts"));
MeshNoteAliasDocument commanderSnapshot = null!;
using var commanderSubscription = commanderHandle
.AsSchemaAlias<MeshNoteAliasDocument>()
.Watch(value => commanderSnapshot = value);
MeshNoteDocument commanderSnapshot = null!;
using var commanderSubscription = commanderHandle.Watch(value => commanderSnapshot = value);
using var pilotReactive = await pilotHandle
.AsSchemaAlias<MeshNoteAliasDocument>()
.PredictionWriter()
.ReactiveAsync(new CultMeshReactiveDocumentOptions
{
@ -1788,7 +1745,7 @@ public sealed class CultMeshStreamingTests
}
[Test]
public async Task NodeDocument_OpensExplicitPredictionWriterForSameSchemaAlias()
public async Task NodeDocument_OpensExplicitPredictionWriter()
{
var schemaId = CultDocumentRegistry.Shared.GetRequired<MeshNoteDocument>().SchemaId;
using var temp = new TemporaryDirectory();
@ -1828,7 +1785,7 @@ public sealed class CultMeshStreamingTests
}, new CultRecordHandle<MeshNoteDocument>(key));
using var pilotReactive = await node
.Document<MeshNoteAliasDocument>(key, CultMesh.Verse("starbridge", "pilot-a"))
.Document<MeshNoteDocument>(key, CultMesh.Verse("starbridge", "pilot-a"))
.PredictionWriter()
.ReactiveAsync(new CultMeshReactiveDocumentOptions
{
@ -1851,7 +1808,7 @@ public sealed class CultMeshStreamingTests
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.AsSchemaAlias<MeshNoteAliasDocument>().LatestAsync()).Revision.Should().Be(2);
(await commanderHandle.LatestAsync()).Revision.Should().Be(2);
}
[Test]
@ -1897,8 +1854,8 @@ public sealed class CultMeshStreamingTests
DocumentRegistry = registry
}
});
var handle = surface.SyncTo(node).Document<MeshNoteAliasDocument>(key.Value);
MeshNoteAliasDocument observed = null!;
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
{
@ -1956,7 +1913,7 @@ public sealed class CultMeshStreamingTests
}
}))
{
var stale = await surface.SyncTo(crashedRuntime).Document<MeshNoteAliasDocument>(key.Value).LatestAsync();
var stale = await surface.SyncTo(crashedRuntime).Document<MeshNoteDocument>(key.Value).LatestAsync();
stale.Text.Should().Be("before-crash");
}
@ -1983,7 +1940,7 @@ public sealed class CultMeshStreamingTests
var reconstructed = await surface
.SyncTo(freshRuntime)
.Documents(CultMesh.SnapshotDocument<MeshNoteDocument>(key.Value))
.LatestAsync<MeshNoteAliasDocument>();
.LatestAsync<MeshNoteDocument>();
reconstructed.Text.Should().Be("after-reboot-authority");
reconstructed.Revision.Should().Be(4);
@ -2034,7 +1991,7 @@ public sealed class CultMeshStreamingTests
});
var readOnlyHandle = surface
.SyncTo(node)
.Document<MeshNoteAliasDocument>(key.Value);
.Document<MeshNoteDocument>(key.Value);
readOnlyHandle.CanReplace.Should().BeFalse();
readOnlyHandle.CanSubmitPrediction.Should().BeFalse();
@ -2645,7 +2602,7 @@ public sealed class CultMeshStreamingTests
.Register(CultNetDocumentBinding.ForDocument<MeshNoteDocument>(CultDocumentRegistry.Shared));
var requests = new List<CultNetSnapshotRequestMessage>();
var handle = CultMesh.DocumentFromPublication<MeshNoteAliasDocument>(
var handle = CultMesh.DocumentFromPublication<MeshNoteDocument>(
CultMeshDocumentPublicationSource.PeerSnapshot(
() => new MeshSnapshotSchemaClient(request =>
{
@ -2666,16 +2623,16 @@ public sealed class CultMeshStreamingTests
snapshot.Text.Should().Be("remote-snapshot");
snapshot.Revision.Should().Be(9);
handle.DocumentType.Should().Be(typeof(MeshNoteAliasDocument));
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<MeshNoteAliasDocument>()).Text.Should().Be("remote-snapshot");
(await catalog.LatestAsync<MeshNoteDocument>()).Text.Should().Be("remote-snapshot");
var aliasSchemaHandle = CultMesh.DocumentFromPeerSnapshot<MeshNoteAliasDocument>(
var aliasSchemaHandle = CultMesh.DocumentFromPeerSnapshot<MeshNoteDocument>(
_ => Task.FromResult(new CultNetSnapshotResponseRawMessage
{
MessageId = "alias-schema",
@ -2703,7 +2660,7 @@ public sealed class CultMeshStreamingTests
(await aliasSchemaHandle.LatestAsync()).Text.Should().Be("record-key-fallback");
var mixedAliasSchemaHandle = CultMesh.DocumentFromPeerSnapshot<MeshNoteAliasDocument>(
var mixedAliasSchemaHandle = CultMesh.DocumentFromPeerSnapshot<MeshNoteDocument>(
_ => Task.FromResult(new CultNetSnapshotResponseRawMessage
{
MessageId = "alias-schema-with-neighbor",
@ -2749,7 +2706,7 @@ public sealed class CultMeshStreamingTests
// 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<MeshNoteAliasDocument>(
var caseVariantKeyHandle = CultMesh.DocumentFromPeerSnapshot<MeshNoteDocument>(
_ => Task.FromResult(new CultNetSnapshotResponseRawMessage
{
MessageId = "case-variant-key",
@ -2862,7 +2819,7 @@ public sealed class CultMeshStreamingTests
CultMesh.PublicationDocument<MeshPublicationNoteDocument>(
firstKey,
documentId: "daemon:first"),
CultMesh.PublicationDocument<MeshNoteAliasDocument>(
CultMesh.PublicationDocument<MeshNoteDocument>(
secondKey,
documentId: "daemon:second",
source: CultMeshDocumentPublicationSource.SingleFile(secondPath))
@ -2876,10 +2833,10 @@ public sealed class CultMeshStreamingTests
catalog.Documents.Select(document => document.DocumentId).Should().Equal("daemon:first", "daemon:second");
(await catalog.LatestAsync<MeshPublicationNoteDocument>()).Text.Should().Be("first source");
(await catalog.LatestAsync<MeshNoteAliasDocument>()).Text.Should().Be("second 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<MeshNoteAliasDocument>().Sources.Should().ContainSingle().Which.SourceId.Should().Be("daemon:second");
catalog.Document<MeshNoteDocument>().Sources.Should().ContainSingle().Which.SourceId.Should().Be("daemon:second");
}
[Test]
@ -2923,7 +2880,7 @@ public sealed class CultMeshStreamingTests
CultMesh.PublicationDocument<MeshPublicationNoteDocument>(
firstKey,
documentId: "local:first"),
CultMesh.PublicationDocument<MeshNoteAliasDocument>(
CultMesh.PublicationDocument<MeshNoteDocument>(
secondKey,
documentId: "local:second",
source: CultMeshDocumentPublicationSource.SingleFile(secondPath))
@ -2936,16 +2893,16 @@ public sealed class CultMeshStreamingTests
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<MeshNoteAliasDocument>()).Text.Should().Be("second 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<MeshNoteAliasDocument>(secondKey, CultMesh.Verse("starbridge", "unity-sync-catalog"))
(await node.Document<MeshNoteDocument>(secondKey, CultMesh.Verse("starbridge", "unity-sync-catalog"))
.LatestAsync()).Revision.Should().Be(32);
}
[Test]
public async Task PublicationHelpers_SyncConfiguredPeerSourceIntoLocalNodeAlias()
public async Task PublicationHelpers_SyncConfiguredPeerSourceIntoLocalNode()
{
var sourceCache = new CultCache();
var registry = new CultNetDocumentRegistry(CultDocumentRegistry.Shared);
@ -2974,7 +2931,7 @@ public sealed class CultMeshStreamingTests
}
});
var synced = await CultMesh.SyncDocumentFromPublicationAsync<MeshNoteAliasDocument>(
var synced = await CultMesh.SyncDocumentFromPublicationAsync<MeshNoteDocument>(
node,
CultMeshDocumentPublicationSource.PeerSnapshot(
() => new MeshSnapshotSchemaClient(request =>
@ -2990,12 +2947,12 @@ public sealed class CultMeshStreamingTests
MessageIdPrefix = "mesh-test-publication-sync",
PollInterval = TimeSpan.FromMilliseconds(10)
});
var localAlias = await node.Document<MeshNoteAliasDocument>(key, verse).LatestAsync();
var local = await node.Document<MeshNoteDocument>(key, verse).LatestAsync();
synced.Text.Should().Be("publication-synced");
synced.Revision.Should().Be(51);
localAlias.Text.Should().Be("publication-synced");
localAlias.Revision.Should().Be(51);
local.Text.Should().Be("publication-synced");
local.Revision.Should().Be(51);
requests.Should().ContainSingle().Which.RecordKeys.Should().Equal(key.Value);
}
@ -3102,7 +3059,7 @@ public sealed class CultMeshStreamingTests
}
[Test]
public async Task SnapshotHelpers_DecodeSameSchemaAliasFromForeignSchemaId()
public async Task SnapshotHelpers_DecodeFromForeignSchemaId()
{
var key = new CultRecordKey("mesh-note:snapshot-alias-foreign-schema");
var snapshot = new CultNetSnapshotResponseRawMessage
@ -3128,7 +3085,7 @@ public sealed class CultMeshStreamingTests
}
};
var documents = await CultMesh.FetchSnapshotDocumentsAsync<MeshNoteAliasDocument>(
var documents = await CultMesh.FetchSnapshotDocumentsAsync<MeshNoteDocument>(
"cultnet://foreign-schema.test:3075",
new CultMeshSnapshotRequestOptions
{
@ -3172,9 +3129,9 @@ public sealed class CultMeshStreamingTests
};
var cacheRegistry = CultMesh.CreateCultCacheDocumentRegistry(
typeof(MeshNoteDocument),
typeof(MeshNoteAliasDocument));
typeof(MeshNoteDocument));
var registry = CultMesh.CreateCultNetDocumentRegistry(
new[] { typeof(MeshNoteDocument), typeof(MeshNoteAliasDocument) },
new[] { typeof(MeshNoteDocument), typeof(MeshNoteDocument) },
cacheRegistry);
var endpoint = CultMesh.SnapshotEndpoint(
"cultnet://foreign-schema.test:3075",
@ -3205,7 +3162,7 @@ public sealed class CultMeshStreamingTests
}
});
var synced = await endpoint.SyncDocumentAsync<MeshNoteAliasDocument>(node, key.Value);
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");
@ -3213,7 +3170,7 @@ public sealed class CultMeshStreamingTests
}
[Test]
public async Task SnapshotFacade_SyncsRemoteDocumentIntoLocalNodeWithSameSchemaAlias()
public async Task SnapshotFacade_SyncsRemoteDocumentIntoLocalNode()
{
var sourceCache = new CultCache();
var registry = new CultNetDocumentRegistry(CultDocumentRegistry.Shared);
@ -3241,7 +3198,7 @@ public sealed class CultMeshStreamingTests
}
});
var synced = await CultMesh.SyncDocumentFromPeerSnapshotAsync<MeshNoteAliasDocument>(
var synced = await CultMesh.SyncDocumentFromPeerSnapshotAsync<MeshNoteDocument>(
node,
() => new MeshSnapshotSchemaClient(request =>
{
@ -3255,12 +3212,12 @@ public sealed class CultMeshStreamingTests
DocumentRegistry = registry,
Context = CultMesh.Verse("starbridge", "unity-raven").Context
});
var localAlias = await node.Document<MeshNoteAliasDocument>(key.Value, CultMesh.Verse("starbridge", "unity-raven"))
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);
localAlias.Text.Should().Be("facade-synced");
local.Text.Should().Be("facade-synced");
node.Cache.Get<MeshNoteDocument>(key)!.Revision.Should().Be(46);
requests.Should().ContainSingle().Which.RecordKeys.Should().Equal(key.Value);
}
@ -3300,9 +3257,9 @@ public sealed class CultMeshStreamingTests
var sourceCache = new CultCache();
var cacheRegistry = CultMesh.CreateCultCacheDocumentRegistry(
typeof(MeshNoteDocument),
typeof(MeshNoteAliasDocument));
typeof(MeshNoteDocument));
var networkRegistry = CultMesh.CreateCultNetDocumentRegistry(
new[] { typeof(MeshNoteDocument), typeof(MeshNoteAliasDocument) },
new[] { typeof(MeshNoteDocument), typeof(MeshNoteDocument) },
cacheRegistry);
var key = new CultRecordKey("mesh-note:registry-helper");
await sourceCache.UpsertAsync(new MeshNoteDocument
@ -3313,7 +3270,7 @@ public sealed class CultMeshStreamingTests
}, new CultRecordHandle<MeshNoteDocument>(key));
var endpoint = "cultnet://registry-helper.test:3077";
var documents = await CultMesh.FetchSnapshotDocumentsAsync<MeshNoteAliasDocument>(
var documents = await CultMesh.FetchSnapshotDocumentsAsync<MeshNoteDocument>(
endpoint,
new CultMeshSnapshotRequestOptions
{
@ -3324,15 +3281,15 @@ public sealed class CultMeshStreamingTests
networkRegistry);
var schemaId = cacheRegistry.GetRequired<MeshNoteDocument>().SchemaId;
cacheRegistry.GetRequired<MeshNoteAliasDocument>().SchemaId.Should().Be(schemaId);
cacheRegistry.GetRequired<MeshNoteDocument>().SchemaId.Should().Be(schemaId);
networkRegistry.GetByDocumentType(typeof(MeshNoteDocument)).Should().NotBeNull();
networkRegistry.GetByDocumentType(typeof(MeshNoteDocument)).Should().NotBeNull();
networkRegistry.GetByDocumentType(typeof(MeshNoteAliasDocument)).Should().NotBeNull();
networkRegistry.GetBySchemaId(schemaId)!.DocumentType.Should().Be(typeof(MeshNoteDocument));
documents.Should().ContainSingle().Which.Text.Should().Be("registry-helper");
}
[Test]
public async Task SnapshotEndpoint_ProvidesTypedHandlesAndSchemaAliases()
public async Task SnapshotEndpoint_ProvidesTypedHandles()
{
var sourceCache = new CultCache();
var registry = new CultNetDocumentRegistry(CultDocumentRegistry.Shared);
@ -3364,7 +3321,7 @@ public sealed class CultMeshStreamingTests
}
});
var fetchedAlias = await surface.FetchDocumentAsync<MeshNoteAliasDocument>(key.Value);
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
@ -3379,12 +3336,12 @@ public sealed class CultMeshStreamingTests
DocumentRegistry = registry
}
});
var syncedAlias = await surface.SyncDocumentAsync<MeshNoteAliasDocument>(node, key.Value);
var aliasHandle = surface.Document<MeshNoteAliasDocument>(key.Value);
var aliasLatest = await aliasHandle.LatestAsync();
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 catalogAlias = await catalog.LatestAsync<MeshNoteAliasDocument>();
var catalogLatest = await catalog.LatestAsync<MeshNoteDocument>();
await sourceCache.UpsertAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
@ -3392,7 +3349,7 @@ public sealed class CultMeshStreamingTests
Revision = 20
}, new CultRecordHandle<MeshNoteDocument>(key));
var syncedEndpoint = surface.SyncTo(node);
var syncedHandle = syncedEndpoint.Document<MeshNoteAliasDocument>(key.Value);
var syncedHandle = syncedEndpoint.Document<MeshNoteDocument>(key.Value);
var syncedLatest = await syncedHandle.LatestAsync();
await sourceCache.UpsertAsync(new MeshNoteDocument
{
@ -3402,16 +3359,16 @@ public sealed class CultMeshStreamingTests
}, new CultRecordHandle<MeshNoteDocument>(key));
var syncedCatalog = syncedEndpoint.Documents(
CultMesh.SnapshotDocument<MeshNoteDocument>(key.Value));
var syncedCatalogAlias = await syncedCatalog.LatestAsync<MeshNoteAliasDocument>();
var syncedCatalogLatest = await syncedCatalog.LatestAsync<MeshNoteDocument>();
fetchedAlias.Text.Should().Be("snapshot-endpoint");
syncedAlias.Text.Should().Be("snapshot-endpoint");
aliasLatest.Revision.Should().Be(19);
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");
catalogAlias.Text.Should().Be("snapshot-endpoint");
catalogLatest.Text.Should().Be("snapshot-endpoint");
syncedLatest.Text.Should().Be("snapshot-endpoint-synced");
node.Cache.Get<MeshNoteDocument>(key)!.Revision.Should().Be(21);
syncedCatalogAlias.Text.Should().Be("snapshot-endpoint-catalog-synced");
syncedCatalogLatest.Text.Should().Be("snapshot-endpoint-catalog-synced");
requests.Should().HaveCount(6);
requests.Should().OnlyContain(request =>
request.SchemaIds == null &&
@ -3464,7 +3421,7 @@ public sealed class CultMeshStreamingTests
}
[Test]
public async Task CollectionHandle_SupportsSameSchemaAliases()
public async Task CollectionHandle_IsIndexedInItsCatalogByTypeAndSchema()
{
var cache = new CultCache();
await cache.UpsertAsync(
@ -3477,14 +3434,12 @@ public sealed class CultMeshStreamingTests
new CultRecordHandle<MeshNoteDocument>(new CultRecordKey("mesh-note:collection")));
var collection = CultMesh.Collection<MeshNoteDocument>(cache);
var alias = collection.AsSchemaAlias<MeshNoteAliasDocument>();
var catalog = CultMesh.Collections(collection);
alias.CollectionId.Should().Be(collection.CollectionId);
alias.SchemaName.Should().Be(collection.SchemaName);
catalog.Collections.Should().ContainSingle().Which.Should().BeSameAs(collection);
catalog.TryGetCollection<MeshNoteAliasDocument>(out var catalogAlias).Should().BeTrue();
catalogAlias.CollectionId.Should().Be(collection.CollectionId);
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);
@ -3492,13 +3447,13 @@ public sealed class CultMeshStreamingTests
.Should()
.BeSameAs(collection);
var snapshot = await alias.LatestAsync();
var snapshot = await collection.LatestAsync();
snapshot.Should().ContainSingle().Which.Text.Should().Be("collection-alias");
var catalogSnapshot = await catalog.LatestAsync<MeshNoteAliasDocument>();
var catalogSnapshot = await catalog.LatestAsync<MeshNoteDocument>();
catalogSnapshot.Should().ContainSingle().Which.Revision.Should().Be(4);
CultMeshCollectionChange<MeshNoteAliasDocument> observed = null!;
using var subscription = catalog.WatchChanges<MeshNoteAliasDocument>(change => observed = change);
CultMeshCollectionChange<MeshNoteDocument> observed = null!;
using var subscription = catalog.WatchChanges<MeshNoteDocument>(change => observed = change);
await cache.UpsertAsync(
new MeshNoteDocument
{
@ -3543,11 +3498,10 @@ public sealed class CultMeshStreamingTests
node,
key,
CultMesh.Verse("starbridge", "unity-pilot"));
var alias = handle.AsSchemaAlias<MeshNoteAliasDocument>();
MeshNoteDocument observed = null!;
using var subscription = handle.Watch(value => observed = value);
await alias.ReplaceAsync(new MeshNoteAliasDocument
await handle.ReplaceAsync(new MeshNoteDocument
{
Schema = "tests.mesh_note.v1",
Text = "node-replaced",
@ -3985,20 +3939,6 @@ public sealed class CultMeshStreamingTests
public int Revision { get; set; }
}
[CultDocument("tests.mesh_note", "tests.mesh_note.v1")]
[MessagePackObject(AllowPrivate = true)]
internal sealed class MeshNoteAliasDocument
{
[Key(0)]
public string Schema { get; set; } = string.Empty;
[Key(1)]
public string Text { get; set; } = string.Empty;
[Key(2)]
public int Revision { get; set; }
}
[CultDocument("tests.mesh_publication_note", "tests.mesh_publication_note.v1")]
[MessagePackObject(AllowPrivate = true)]
internal sealed class MeshPublicationNoteDocument