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:
parent
5b86bc9faf
commit
1e0455028c
5 changed files with 140 additions and 338 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue