Add schema-aware collection catalogs
This commit is contained in:
parent
a825974e9a
commit
964a3ff1f8
4 changed files with 212 additions and 1 deletions
|
|
@ -1782,6 +1782,22 @@ namespace GameCult.Mesh
|
|||
return new CultMeshDocumentCatalog(documents);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a schema-aware catalog over typed collection handles.
|
||||
/// </summary>
|
||||
public static CultMeshCollectionCatalog Collections(IEnumerable<ICultMeshCollectionHandle> collections)
|
||||
{
|
||||
return new CultMeshCollectionCatalog(collections);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a schema-aware catalog over typed collection handles.
|
||||
/// </summary>
|
||||
public static CultMeshCollectionCatalog Collections(params ICultMeshCollectionHandle[] collections)
|
||||
{
|
||||
return new CultMeshCollectionCatalog(collections);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads two typed document handles and projects them into one caller-defined view.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1763,6 +1763,163 @@ namespace GameCult.Mesh
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inspectable typed collection handle exposed by CultMesh.
|
||||
/// </summary>
|
||||
public interface ICultMeshCollectionHandle
|
||||
{
|
||||
/// <summary>Gets the CLR document type presented by this collection.</summary>
|
||||
Type DocumentType { get; }
|
||||
|
||||
/// <summary>Gets the semantic collection id.</summary>
|
||||
string CollectionId { get; }
|
||||
|
||||
/// <summary>Gets the stable CultCache schema name.</summary>
|
||||
string SchemaName { get; }
|
||||
|
||||
/// <summary>Gets the stable CultCache schema version.</summary>
|
||||
string SchemaVersion { get; }
|
||||
|
||||
/// <summary>Gets the content-derived schema identifier.</summary>
|
||||
string SchemaId { get; }
|
||||
|
||||
/// <summary>Gets the preferred or observed route for collection access.</summary>
|
||||
CultMeshRouteHint RouteHint { get; }
|
||||
|
||||
/// <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>
|
||||
/// Schema-aware lookup table for typed CultMesh collection handles.
|
||||
/// </summary>
|
||||
public sealed class CultMeshCollectionCatalog
|
||||
{
|
||||
private readonly IReadOnlyList<ICultMeshCollectionHandle> _collections;
|
||||
private readonly IReadOnlyDictionary<Type, ICultMeshCollectionHandle> _collectionsByType;
|
||||
private readonly IReadOnlyDictionary<string, ICultMeshCollectionHandle> _collectionsBySchema;
|
||||
|
||||
/// <summary>Creates a collection catalog from known handles.</summary>
|
||||
public CultMeshCollectionCatalog(IEnumerable<ICultMeshCollectionHandle> collections)
|
||||
{
|
||||
if (collections == null) throw new ArgumentNullException(nameof(collections));
|
||||
|
||||
var collectionList = collections
|
||||
.Where(collection => collection != null)
|
||||
.ToArray();
|
||||
var byType = new Dictionary<Type, ICultMeshCollectionHandle>();
|
||||
var bySchema = new Dictionary<string, ICultMeshCollectionHandle>(StringComparer.Ordinal);
|
||||
foreach (var collection in collectionList)
|
||||
{
|
||||
byType[collection.DocumentType] = collection;
|
||||
if (!string.IsNullOrWhiteSpace(collection.SchemaId))
|
||||
bySchema[collection.SchemaId] = collection;
|
||||
if (!string.IsNullOrWhiteSpace(collection.SchemaVersion))
|
||||
bySchema[collection.SchemaVersion] = collection;
|
||||
if (!string.IsNullOrWhiteSpace(collection.SchemaName))
|
||||
bySchema[collection.SchemaName] = collection;
|
||||
}
|
||||
|
||||
_collections = collectionList;
|
||||
_collectionsByType = byType;
|
||||
_collectionsBySchema = bySchema;
|
||||
}
|
||||
|
||||
/// <summary>Gets the handles in this catalog.</summary>
|
||||
public IReadOnlyList<ICultMeshCollectionHandle> Collections => _collections;
|
||||
|
||||
/// <summary>Looks up a collection handle by schema id, shared schema name, or schema version.</summary>
|
||||
public bool TryGetCollectionBySchema(
|
||||
string schema,
|
||||
out ICultMeshCollectionHandle collection)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(schema) &&
|
||||
_collectionsBySchema.TryGetValue(schema, out collection!))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
collection = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Looks up a collection handle by schema id, shared schema name, or schema version.</summary>
|
||||
public ICultMeshCollectionHandle CollectionBySchema(string schema)
|
||||
{
|
||||
if (TryGetCollectionBySchema(schema, out var collection))
|
||||
return collection;
|
||||
|
||||
throw new NotSupportedException(
|
||||
$"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>
|
||||
public bool TryGetCollection<TDocument>(
|
||||
out CultMeshCollectionHandle<TDocument> collection)
|
||||
where TDocument : class
|
||||
{
|
||||
if (_collectionsByType.TryGetValue(typeof(TDocument), out var untypedCollection) &&
|
||||
untypedCollection is CultMeshCollectionHandle<TDocument> typedCollection)
|
||||
{
|
||||
collection = typedCollection;
|
||||
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>
|
||||
public CultMeshCollectionHandle<TDocument> Collection<TDocument>()
|
||||
where TDocument : class
|
||||
{
|
||||
if (TryGetCollection<TDocument>(out var collection))
|
||||
return collection;
|
||||
|
||||
throw new NotSupportedException(
|
||||
$"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>
|
||||
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>
|
||||
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>
|
||||
public IDisposable WatchChanges<TDocument>(Action<CultMeshCollectionChange<TDocument>> onNext)
|
||||
where TDocument : class
|
||||
{
|
||||
return Collection<TDocument>().WatchChanges(onNext);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes how a CultMesh collection document changed.
|
||||
/// </summary>
|
||||
|
|
@ -1829,7 +1986,7 @@ namespace GameCult.Mesh
|
|||
/// <summary>
|
||||
/// Typed reactive collection handle with schema-aware alias conversion.
|
||||
/// </summary>
|
||||
public sealed class CultMeshCollectionHandle<TDocument>
|
||||
public sealed class CultMeshCollectionHandle<TDocument> : ICultMeshCollectionHandle
|
||||
where TDocument : class
|
||||
{
|
||||
private static readonly CultDocumentDescriptor Descriptor =
|
||||
|
|
|
|||
|
|
@ -191,6 +191,12 @@ portable machinery that makes it feel native.
|
|||
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.
|
||||
- `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
|
||||
`Collection<T>()`, `LatestAsync<T>()`, and `WatchChanges<T>()` so domain
|
||||
facades can publish typed multi-record state without maintaining parallel
|
||||
schema lookup tables.
|
||||
- `CultMeshProjectionRecipe<TParameters, TResult>` names a reusable projection
|
||||
from typed source state into derived state. It records source handles,
|
||||
route hints, and projection execution, and can be exposed as a typed query
|
||||
|
|
@ -321,6 +327,10 @@ 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);
|
||||
```
|
||||
|
||||
```ts
|
||||
|
|
|
|||
|
|
@ -2665,12 +2665,40 @@ public sealed class CultMeshStreamingTests
|
|||
|
||||
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.TryGetCollectionBySchema("tests.mesh_note.v1", out var byVersion).Should().BeTrue();
|
||||
byVersion.Should().BeSameAs(collection);
|
||||
catalog.CollectionBySchema("tests.mesh_note").Should().BeSameAs(collection);
|
||||
catalog.CollectionBySchema(CultDocumentRegistry.Shared.GetRequired<MeshNoteDocument>().SchemaId)
|
||||
.Should()
|
||||
.BeSameAs(collection);
|
||||
|
||||
var snapshot = await alias.LatestAsync();
|
||||
snapshot.Should().ContainSingle().Which.Text.Should().Be("collection-alias");
|
||||
var catalogSnapshot = await catalog.LatestAsync<MeshNoteAliasDocument>();
|
||||
catalogSnapshot.Should().ContainSingle().Which.Revision.Should().Be(4);
|
||||
|
||||
CultMeshCollectionChange<MeshNoteAliasDocument> observed = null!;
|
||||
using var subscription = catalog.WatchChanges<MeshNoteAliasDocument>(change => observed = change);
|
||||
await cache.UpsertAsync(
|
||||
new MeshNoteDocument
|
||||
{
|
||||
Schema = "tests.mesh_note.v1",
|
||||
Text = "collection-alias-updated",
|
||||
Revision = 5
|
||||
},
|
||||
new CultRecordHandle<MeshNoteDocument>(new CultRecordKey("mesh-note:collection")));
|
||||
|
||||
observed.Kind.Should().Be(CultMeshCollectionChangeKind.Updated);
|
||||
observed.Document!.Text.Should().Be("collection-alias-updated");
|
||||
observed.Document.Revision.Should().Be(5);
|
||||
observed.PreviousDocument!.Text.Should().Be("collection-alias");
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
|
|||
Loading…
Reference in a new issue