diff --git a/src/GameCult.Mesh/CultMesh.cs b/src/GameCult.Mesh/CultMesh.cs index 0339158f..ac2053d5 100644 --- a/src/GameCult.Mesh/CultMesh.cs +++ b/src/GameCult.Mesh/CultMesh.cs @@ -112,7 +112,7 @@ namespace GameCult.Mesh /// /// Options for opening one typed document from a remote CultNet snapshot endpoint. /// - public sealed class CultMeshPeerSnapshotDocumentOptions + public sealed record CultMeshPeerSnapshotDocumentOptions { /// Gets or sets the semantic document id. Defaults to the record key. public string? DocumentId { get; set; } @@ -135,8 +135,8 @@ namespace GameCult.Mesh /// Gets or sets the message id prefix used for snapshot requests. public string? MessageIdPrefix { get; set; } - /// Gets or sets schema ids to request. Empty means the document type schema is requested. - public IReadOnlyList? SchemaIds { get; set; } + /// Gets or sets the selection to request. Absent means the document type's own schema. + public CultNetSelection? Selection { get; set; } /// Gets or sets the target shard id, when the endpoint is shard-aware. public string? ShardId { get; set; } @@ -1944,28 +1944,12 @@ namespace GameCult.Mesh string? documentId, string? sourceId) { - return new CultMeshPeerSnapshotDocumentOptions - { - DocumentId = string.IsNullOrWhiteSpace(documentId) ? key.Value : documentId, - SourceId = string.IsNullOrWhiteSpace(sourceId) - ? (string.IsNullOrWhiteSpace(documentId) ? key.Value : documentId) - : sourceId, - RouteHint = options?.RouteHint, - ResponseTimeout = options?.ResponseTimeout ?? TimeSpan.FromSeconds(5), - ConnectTimeout = options?.ConnectTimeout ?? TimeSpan.FromSeconds(5), - PollInterval = options?.PollInterval ?? TimeSpan.FromMilliseconds(250), - MessageIdPrefix = options?.MessageIdPrefix, - SchemaIds = options?.SchemaIds, - ShardId = options?.ShardId, - ShardEpoch = options?.ShardEpoch, - Security = options?.Security, - ConfigureClient = options?.ConfigureClient, - RudpRuntimeId = options?.RudpRuntimeId, - RudpConnectionId = options?.RudpConnectionId ?? 0x43554c54, - RudpConnectPayload = options?.RudpConnectPayload ?? "cultnet-schema-rudp", - RudpMaxFragmentBytes = options?.RudpMaxFragmentBytes ?? 1024, - RudpResendDelayMs = options?.RudpResendDelayMs ?? 25 - }; + var resolved = options is null ? new CultMeshPeerSnapshotDocumentOptions() : options with { }; + resolved.DocumentId = string.IsNullOrWhiteSpace(documentId) ? key.Value : documentId; + resolved.SourceId = string.IsNullOrWhiteSpace(sourceId) + ? (string.IsNullOrWhiteSpace(documentId) ? key.Value : documentId) + : sourceId; + return resolved; } /// @@ -2707,46 +2691,42 @@ namespace GameCult.Mesh where TDocument : class { var descriptor = CultDocumentRegistry.Shared.GetRequired(); + var selection = new CultNetSelection + { + Schemas = options.Selection?.Schemas ?? new[] { descriptor.SchemaId }, + Keys = new[] { recordKey } + }; return await FetchSnapshotAsync( createClient, endpoint, - ToSnapshotRequestOptions( - options, - context, - options.SchemaIds ?? new[] { descriptor.SchemaId }, - new[] { recordKey })) + new CultMeshSnapshotRequestOptions + { + Selection = selection, + ShardId = options.ShardId, + ShardEpoch = options.ShardEpoch, + ResponseTimeout = options.ResponseTimeout, + ConnectTimeout = options.ConnectTimeout, + MessageIdPrefix = string.IsNullOrWhiteSpace(options.MessageIdPrefix) + ? $"cultmesh:{context.RuntimeId}:snapshot" + : options.MessageIdPrefix, + Security = options.Security, + ConfigureClient = options.ConfigureClient, + RudpRuntimeId = string.IsNullOrWhiteSpace(options.RudpRuntimeId) + ? context.RuntimeId + : options.RudpRuntimeId, + RudpConnectionId = options.RudpConnectionId, + RudpConnectPayload = options.RudpConnectPayload, + RudpMaxFragmentBytes = options.RudpMaxFragmentBytes, + RudpResendDelayMs = options.RudpResendDelayMs + }) .ConfigureAwait(false); } - private static CultMeshSnapshotRequestOptions ToSnapshotRequestOptions( - CultMeshPeerSnapshotDocumentOptions options, - CultMeshQueryContext context, - IReadOnlyList? schemaIds, - IReadOnlyList? recordKeys) - { - return new CultMeshSnapshotRequestOptions - { - SchemaIds = schemaIds, - RecordKeys = recordKeys, - ShardId = options.ShardId, - ShardEpoch = options.ShardEpoch, - ResponseTimeout = options.ResponseTimeout, - ConnectTimeout = options.ConnectTimeout, - MessageIdPrefix = string.IsNullOrWhiteSpace(options.MessageIdPrefix) - ? $"cultmesh:{context.RuntimeId}:snapshot" - : options.MessageIdPrefix, - Security = options.Security, - ConfigureClient = options.ConfigureClient, - RudpRuntimeId = string.IsNullOrWhiteSpace(options.RudpRuntimeId) - ? context.RuntimeId - : options.RudpRuntimeId, - RudpConnectionId = options.RudpConnectionId, - RudpConnectPayload = options.RudpConnectPayload, - RudpMaxFragmentBytes = options.RudpMaxFragmentBytes, - RudpResendDelayMs = options.RudpResendDelayMs - }; - } - + // One exact read (docs/cultnet-selection-cut.md, section 4/7): the record this recordKey + // names, decoded as TDocument when its schema is this schema, a declared alias of it, or + // (a foreign/runtime-generated schema id this runtime does not recognize by any alias) its + // payload decodes as TDocument. 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( CultNetSnapshotResponseRawMessage response, string schemaId, @@ -2754,16 +2734,21 @@ namespace GameCult.Mesh where TDocument : class { if (response == null) throw new ArgumentNullException(nameof(response)); - var record = response.Documents.FirstOrDefault(candidate => - string.Equals(candidate.SchemaId, schemaId, StringComparison.Ordinal) && - string.Equals(candidate.RecordKey, recordKey, StringComparison.Ordinal)) - ?? response.Documents.FirstOrDefault(candidate => - string.Equals(candidate.RecordKey, recordKey, StringComparison.Ordinal) && - TryDecodeSnapshotDocument(candidate, out TDocument? _)) - ?? response.Documents.FirstOrDefault(candidate => - string.Equals(candidate.SchemaId, schemaId, StringComparison.Ordinal)) - ?? response.Documents.FirstOrDefault(candidate => - string.Equals(candidate.RecordKey, recordKey, StringComparison.Ordinal)); + var descriptor = CultDocumentRegistry.Shared.GetRequired(); + CultNetRawDocumentRecord? record = null; + foreach (var candidate in response.Documents) + { + if (!string.Equals(candidate.RecordKey, recordKey, StringComparison.Ordinal)) + continue; + if (string.Equals(candidate.SchemaId, schemaId, StringComparison.Ordinal) || + CultNetSchemaAliasMatching.Matches(candidate.SchemaId, descriptor) || + TryDecodeAsMessagePack(candidate, out _)) + { + record = candidate; + break; + } + } + if (record == null) { throw new InvalidOperationException( @@ -2776,21 +2761,18 @@ namespace GameCult.Mesh $"CultNet raw document payloadEncoding must be \"messagepack\", not \"{record.PayloadEncoding}\"."); } - return DecodeSnapshotDocument(record); + return (TDocument)CultDocumentMessagePackSerialization.DeserializeUntyped(typeof(TDocument), record.Payload); } - private static bool TryDecodeSnapshotDocument( - CultNetRawDocumentRecord record, - out TDocument? document) + private static bool TryDecodeAsMessagePack(CultNetRawDocumentRecord record, out TDocument? document) where TDocument : class { document = null; if (!string.Equals(record.PayloadEncoding, "messagepack", StringComparison.Ordinal)) return false; - try { - document = DecodeSnapshotDocument(record); + document = (TDocument)CultDocumentMessagePackSerialization.DeserializeUntyped(typeof(TDocument), record.Payload); return true; } catch @@ -2799,14 +2781,6 @@ namespace GameCult.Mesh } } - private static TDocument DecodeSnapshotDocument(CultNetRawDocumentRecord record) - where TDocument : class - { - return (TDocument)CultDocumentMessagePackSerialization.DeserializeUntyped( - typeof(TDocument), - record.Payload); - } - /// /// Parses a rudp://host:port endpoint into its host/port parts. /// @@ -2849,8 +2823,8 @@ namespace GameCult.Mesh throw new ArgumentException("At least one readable body transport is required.", nameof(supportedBodyTransports)); return subscriptions.SubscribeAsync( subscription.SubscriptionId, - recordKeys: subscription.RecordKeys, - schemaIds: subscription.SchemaIds, + recordKeys: subscription.Selection.Keys, + schemaIds: subscription.Selection.Schemas, consumerRuntimeId: subscription.ConsumerRuntimeId, bodyIds: new[] { subscription.BodyId }, supportedBodyTransports: transports.Select(value => value.ToString()), diff --git a/src/GameCult.Mesh/CultMeshBodyDemand.cs b/src/GameCult.Mesh/CultMeshBodyDemand.cs index 2ab90617..cd2a6370 100644 --- a/src/GameCult.Mesh/CultMeshBodyDemand.cs +++ b/src/GameCult.Mesh/CultMeshBodyDemand.cs @@ -188,31 +188,35 @@ namespace GameCult.Mesh SubscriptionId = Require(subscriptionId, nameof(subscriptionId)); ConsumerRuntimeId = Require(consumerRuntimeId, nameof(consumerRuntimeId)); BodyId = Require(bodyId, nameof(bodyId)); - RecordKeys = new[] - { - Require(viewRecordKey, nameof(viewRecordKey)), - CultMeshBodyPublicationDocument.CreateLatestRecordKey(bodyId).Value - } - .Concat(additionalRecordKeys ?? Array.Empty()) - .Where(value => !string.IsNullOrWhiteSpace(value)) - .Distinct(StringComparer.Ordinal) - .ToArray(); - SchemaIds = new[] - { - Require(viewSchemaId, nameof(viewSchemaId)), - CultMeshBodyPublicationSchemaVersions.Publication - } - .Concat(additionalSchemaIds ?? Array.Empty()) - .Where(value => !string.IsNullOrWhiteSpace(value)) - .Distinct(StringComparer.Ordinal) - .ToArray(); + Selection = new CultNetSelection + { + Keys = new[] + { + Require(viewRecordKey, nameof(viewRecordKey)), + CultMeshBodyPublicationDocument.CreateLatestRecordKey(bodyId).Value + } + .Concat(additionalRecordKeys ?? Array.Empty()) + .Where(value => !string.IsNullOrWhiteSpace(value)) + .Distinct(StringComparer.Ordinal) + .ToArray(), + Schemas = new[] + { + Require(viewSchemaId, nameof(viewSchemaId)), + CultMeshBodyPublicationSchemaVersions.Publication + } + .Concat(additionalSchemaIds ?? Array.Empty()) + .Where(value => !string.IsNullOrWhiteSpace(value)) + .Distinct(StringComparer.Ordinal) + .ToArray() + }; } public string SubscriptionId { get; } public string ConsumerRuntimeId { get; } public string BodyId { get; } - public IReadOnlyList RecordKeys { get; } - public IReadOnlyList SchemaIds { get; } + + /// The view record, the body's latest-publication record, and any additional keys/schemas, as one selection. + public CultNetSelection Selection { get; } private static string Require(string value, string parameterName) => string.IsNullOrWhiteSpace(value) diff --git a/src/GameCult.Mesh/CultMeshSnapshots.cs b/src/GameCult.Mesh/CultMeshSnapshots.cs index 9db8727f..5d44a366 100644 --- a/src/GameCult.Mesh/CultMeshSnapshots.cs +++ b/src/GameCult.Mesh/CultMeshSnapshots.cs @@ -13,13 +13,10 @@ namespace GameCult.Mesh /// /// Options for one scoped CultNet snapshot request. /// - public sealed class CultMeshSnapshotRequestOptions + public sealed record CultMeshSnapshotRequestOptions { - /// Gets or sets schema ids to request. Empty means no schema filter. - public IReadOnlyList? SchemaIds { get; set; } - - /// Gets or sets record keys to request. Empty means no record-key filter. - public IReadOnlyList? RecordKeys { get; set; } + /// Gets or sets the selection to request. Absent selects every schema and key. + public CultNetSelection? Selection { get; set; } /// Gets or sets the target shard id, when the endpoint is shard-aware. public string? ShardId { get; set; } @@ -87,7 +84,7 @@ namespace GameCult.Mesh _endpoint = string.IsNullOrWhiteSpace(endpoint) ? throw new ArgumentException("Value must be non-empty.", nameof(endpoint)) : endpoint; - _defaults = CultMesh.CloneSnapshotRequestOptions(options); + _defaults = options with { }; _registry = registry ?? new CultNetDocumentRegistry(); _client = CultMesh.CreateSnapshotClient(endpoint, _defaults)(); _client.OnCultNet(OnResponse); @@ -103,7 +100,7 @@ namespace GameCult.Mesh { _sharedSession = session ?? throw new ArgumentNullException(nameof(session)); _endpoint = session.State.Path?.Endpoint ?? session.Target.AuthorityRuntimeId; - _defaults = CultMesh.CloneSnapshotRequestOptions(options); + _defaults = options with { }; _registry = registry ?? new CultNetDocumentRegistry(); _client = session.Channel; _responseSubscription = session.OnCultNet(OnResponse); @@ -150,9 +147,8 @@ namespace GameCult.Mesh _defaults.ConnectTimeout, (_client as ICultNetSchemaClientHealth)?.BackgroundFailure) .ConfigureAwait(false); - var options = CultMesh.CloneSnapshotRequestOptions(_defaults); - options.SchemaIds = schemaIds ?? options.SchemaIds; - options.RecordKeys = recordKeys ?? options.RecordKeys; + var selection = CultMesh.OverlaySelection(_defaults.Selection, schemaIds, recordKeys); + var options = _defaults with { Selection = selection }; var messageId = CultMesh.CreateSnapshotMessageId(options); var completion = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously); @@ -164,8 +160,8 @@ namespace GameCult.Mesh _client.SendCultNet(new CultNetSnapshotRequestMessage { MessageId = messageId, - SchemaIds = CultMesh.CleanSnapshotFilter(options.SchemaIds), - RecordKeys = CultMesh.CleanSnapshotFilter(options.RecordKeys), + SchemaIds = selection.Schemas, + RecordKeys = selection.Keys, ShardId = string.IsNullOrWhiteSpace(options.ShardId) ? null : options.ShardId, ShardEpoch = options.ShardEpoch }); @@ -195,8 +191,8 @@ namespace GameCult.Mesh where TDocument : class { var descriptor = CultDocumentRegistry.Shared.GetRequired(); - var resolvedSchemas = schemaIds ?? (recordKeys is { Count: > 0 } ? null : new[] { descriptor.SchemaId }); - var snapshot = await FetchSnapshotAsync(resolvedSchemas, recordKeys).ConfigureAwait(false); + var selection = CultMesh.ResolveDefaultSelection(schemaIds, recordKeys, descriptor); + var snapshot = await FetchSnapshotAsync(selection.Schemas, selection.Keys).ConfigureAwait(false); return CultMesh.DecodeSnapshotDocuments(snapshot, _registry); } @@ -397,7 +393,7 @@ namespace GameCult.Mesh var resolvedOptions = options ?? new CultMeshSnapshotEndpointOptions(); Context = resolvedOptions.Context ?? CultMesh.Verse("remote", "cultmesh-snapshot-client").Context; DocumentRegistry = resolvedOptions.DocumentRegistry ?? new CultNetDocumentRegistry(); - Request = CloneSnapshotRequestOptions(resolvedOptions.Request); + Request = resolvedOptions.Request is null ? new CultMeshSnapshotRequestOptions() : resolvedOptions.Request with { }; RouteHint = resolvedOptions.RouteHint ?? new CultMeshRouteHint(CultMeshLocalityKind.Network, Endpoint); SourceId = string.IsNullOrWhiteSpace(resolvedOptions.SourceId) ? Endpoint : resolvedOptions.SourceId!; PollInterval = resolvedOptions.PollInterval; @@ -463,9 +459,10 @@ namespace GameCult.Mesh where TDocument : class { var descriptor = CultDocumentRegistry.Shared.GetRequired(); + var selection = CultMesh.ResolveDefaultSelection(schemaIds, recordKeys, descriptor); return CultMesh.FetchSnapshotDocumentsAsync( Endpoint, - CreateRequest(ResolveDefaultSchemaFilter(schemaIds, recordKeys, descriptor), recordKeys), + CreateRequest(selection.Schemas, selection.Keys), DocumentRegistry); } @@ -480,10 +477,11 @@ namespace GameCult.Mesh if (node == null) throw new ArgumentNullException(nameof(node)); var descriptor = CultDocumentRegistry.Shared.GetRequired(); + var selection = CultMesh.ResolveDefaultSelection(schemaIds, recordKeys, descriptor); var result = await SyncSnapshotAsync( node, - ResolveDefaultSchemaFilter(schemaIds, recordKeys, descriptor), - recordKeys, + selection.Schemas, + selection.Keys, flush) .ConfigureAwait(false); return CultMesh.DecodeSnapshotDocuments(result.Snapshot, DocumentRegistry); @@ -612,53 +610,13 @@ namespace GameCult.Mesh IReadOnlyList? schemaIds, IReadOnlyList? recordKeys) { - var request = CloneSnapshotRequestOptions(Request); - request.SchemaIds = schemaIds ?? request.SchemaIds; - request.RecordKeys = recordKeys ?? request.RecordKeys; + var request = Request with { Selection = CultMesh.OverlaySelection(Request.Selection, schemaIds, recordKeys) }; if (string.IsNullOrWhiteSpace(request.RudpRuntimeId)) request.RudpRuntimeId = Context.RuntimeId; if (string.IsNullOrWhiteSpace(request.MessageIdPrefix)) request.MessageIdPrefix = $"cultmesh:{Context.RuntimeId}:snapshot"; return request; } - - private static IReadOnlyList? ResolveDefaultSchemaFilter( - IReadOnlyList? schemaIds, - IReadOnlyList? recordKeys, - CultDocumentDescriptor descriptor) - { - if (schemaIds != null) - return schemaIds; - - return recordKeys is { Count: > 0 } - ? null - : new[] { descriptor.SchemaId }; - } - - private static CultMeshSnapshotRequestOptions CloneSnapshotRequestOptions(CultMeshSnapshotRequestOptions? source) - { - if (source == null) - return new CultMeshSnapshotRequestOptions(); - - return new CultMeshSnapshotRequestOptions - { - SchemaIds = source.SchemaIds, - RecordKeys = source.RecordKeys, - ShardId = source.ShardId, - ShardEpoch = source.ShardEpoch, - ResponseTimeout = source.ResponseTimeout, - ConnectTimeout = source.ConnectTimeout, - MessageIdPrefix = source.MessageIdPrefix, - Security = source.Security, - ConfigureClient = source.ConfigureClient, - CreateClient = source.CreateClient, - RudpRuntimeId = source.RudpRuntimeId, - RudpConnectionId = source.RudpConnectionId, - RudpConnectPayload = source.RudpConnectPayload, - RudpMaxFragmentBytes = source.RudpMaxFragmentBytes, - RudpResendDelayMs = source.RudpResendDelayMs - }; - } } public static partial class CultMesh @@ -733,7 +691,7 @@ namespace GameCult.Mesh if (string.IsNullOrWhiteSpace(endpoint)) throw new ArgumentException("Value must be non-empty.", nameof(endpoint)); if (string.IsNullOrWhiteSpace(recordKey)) throw new ArgumentException("Value must be non-empty.", nameof(recordKey)); - var request = CloneSnapshotFacadeRequestOptions(options?.Request); + var request = options?.Request is null ? new CultMeshSnapshotRequestOptions() : options.Request with { }; request.CreateClient = createClient; var resolvedOptions = new CultMeshSnapshotEndpointOptions { @@ -747,31 +705,6 @@ namespace GameCult.Mesh return SnapshotEndpoint(endpoint, resolvedOptions).SyncDocumentAsync(node, recordKey, flush); } - private static CultMeshSnapshotRequestOptions CloneSnapshotFacadeRequestOptions(CultMeshSnapshotRequestOptions? source) - { - if (source == null) - return new CultMeshSnapshotRequestOptions(); - - return new CultMeshSnapshotRequestOptions - { - SchemaIds = source.SchemaIds, - RecordKeys = source.RecordKeys, - ShardId = source.ShardId, - ShardEpoch = source.ShardEpoch, - ResponseTimeout = source.ResponseTimeout, - ConnectTimeout = source.ConnectTimeout, - MessageIdPrefix = source.MessageIdPrefix, - Security = source.Security, - ConfigureClient = source.ConfigureClient, - CreateClient = source.CreateClient, - RudpRuntimeId = source.RudpRuntimeId, - RudpConnectionId = source.RudpConnectionId, - RudpConnectPayload = source.RudpConnectPayload, - RudpMaxFragmentBytes = source.RudpMaxFragmentBytes, - RudpResendDelayMs = source.RudpResendDelayMs - }; - } - /// /// Fetches one scoped raw CultNet snapshot from an endpoint. /// @@ -819,8 +752,8 @@ namespace GameCult.Mesh client.SendCultNet(new CultNetSnapshotRequestMessage { MessageId = messageId, - SchemaIds = CleanSnapshotFilter(resolvedOptions.SchemaIds), - RecordKeys = CleanSnapshotFilter(resolvedOptions.RecordKeys), + SchemaIds = resolvedOptions.Selection?.Schemas, + RecordKeys = resolvedOptions.Selection?.Keys, ShardId = string.IsNullOrWhiteSpace(resolvedOptions.ShardId) ? null : resolvedOptions.ShardId, ShardEpoch = resolvedOptions.ShardEpoch }); @@ -886,30 +819,48 @@ namespace GameCult.Mesh }; } - internal static CultMeshSnapshotRequestOptions CloneSnapshotRequestOptions( - CultMeshSnapshotRequestOptions? source) + /// + /// Resolves one selection for a typed convenience call: explicit + /// win; otherwise an explicit, non-empty means no schema filter; + /// otherwise the selection defaults to the document type's own schema. The one owner of that + /// rule (docs/cultnet-selection-cut.md, section 4). + /// + internal static CultNetSelection ResolveDefaultSelection( + IReadOnlyList? schemaIds, + IReadOnlyList? recordKeys, + CultDocumentDescriptor descriptor) { - if (source == null) return new CultMeshSnapshotRequestOptions(); - return new CultMeshSnapshotRequestOptions + var schemas = schemaIds != null + ? Clean(schemaIds) + : (recordKeys is { Count: > 0 } ? null : new[] { descriptor.SchemaId }); + return new CultNetSelection { Schemas = schemas, Keys = Clean(recordKeys) }; + } + + /// Overlays explicit schema/record-key overrides onto a default selection. + internal static CultNetSelection OverlaySelection( + CultNetSelection? defaults, + IReadOnlyList? schemaIds, + IReadOnlyList? recordKeys) + { + return new CultNetSelection { - SchemaIds = source.SchemaIds, - RecordKeys = source.RecordKeys, - ShardId = source.ShardId, - ShardEpoch = source.ShardEpoch, - ResponseTimeout = source.ResponseTimeout, - ConnectTimeout = source.ConnectTimeout, - MessageIdPrefix = source.MessageIdPrefix, - Security = source.Security, - ConfigureClient = source.ConfigureClient, - CreateClient = source.CreateClient, - RudpRuntimeId = source.RudpRuntimeId, - RudpConnectionId = source.RudpConnectionId, - RudpConnectPayload = source.RudpConnectPayload, - RudpMaxFragmentBytes = source.RudpMaxFragmentBytes, - RudpResendDelayMs = source.RudpResendDelayMs + Schemas = schemaIds != null ? Clean(schemaIds) : defaults?.Schemas, + Keys = recordKeys != null ? Clean(recordKeys) : defaults?.Keys }; } + private static string[]? Clean(IReadOnlyList? values) + { + if (values is not { Count: > 0 }) + return null; + + var filtered = values + .Where(value => !string.IsNullOrWhiteSpace(value)) + .Distinct(StringComparer.Ordinal) + .ToArray(); + return filtered.Length == 0 ? null : filtered; + } + internal static string CreateSnapshotMessageId(CultMeshSnapshotRequestOptions options) { var prefix = string.IsNullOrWhiteSpace(options.MessageIdPrefix) @@ -963,25 +914,13 @@ namespace GameCult.Mesh { throw new TimeoutException( $"Timed out waiting for CultNet snapshot response '{messageId}' from {endpoint} " + - $"for schemas [{string.Join(", ", CleanSnapshotFilter(options.SchemaIds) ?? Array.Empty())}] " + - $"and records [{string.Join(", ", CleanSnapshotFilter(options.RecordKeys) ?? Array.Empty())}]."); + $"for schemas [{string.Join(", ", options.Selection?.Schemas ?? Array.Empty())}] " + + $"and records [{string.Join(", ", options.Selection?.Keys ?? Array.Empty())}]."); } return await responseTask.ConfigureAwait(false); } - internal static string[]? CleanSnapshotFilter(IReadOnlyList? values) - { - if (values is not { Count: > 0 }) - return null; - - var filtered = values - .Where(value => !string.IsNullOrWhiteSpace(value)) - .Distinct(StringComparer.Ordinal) - .ToArray(); - return filtered.Length == 0 ? null : filtered; - } - internal static IReadOnlyList DecodeSnapshotDocuments( CultNetSnapshotResponseRawMessage snapshot, CultNetDocumentRegistry registry) diff --git a/tests/GameCult.Mesh.Tests/CultMeshBodyTransportTests.cs b/tests/GameCult.Mesh.Tests/CultMeshBodyTransportTests.cs index 131bcf24..3ce7dc88 100644 --- a/tests/GameCult.Mesh.Tests/CultMeshBodyTransportTests.cs +++ b/tests/GameCult.Mesh.Tests/CultMeshBodyTransportTests.cs @@ -463,11 +463,11 @@ public sealed class CultMeshBodyTransportTests new[] { "eve:field:gravity" }, new[] { "gamecult.eve.fields_splats.v1" }); - subscription.RecordKeys.Should().Equal( + subscription.Selection.Keys.Should().Equal( "eve:view:pilot", CultMeshBodyPublicationDocument.CreateLatestRecordKey("eve:body:pilot").Value, "eve:field:gravity"); - subscription.SchemaIds.Should().Equal( + subscription.Selection.Schemas.Should().Equal( "gamecult.eve.entity_soa_view.v2", CultMeshBodyPublicationSchemaVersions.Publication, "gamecult.eve.fields_splats.v1"); diff --git a/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs b/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs index 584569e6..6b1a9d6b 100644 --- a/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs +++ b/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs @@ -2936,7 +2936,7 @@ public sealed class CultMeshStreamingTests var endpoint = "cultnet://snapshot-helper.test:3075"; var options = new CultMeshSnapshotRequestOptions { - RecordKeys = new[] { key.Value }, + Selection = new CultNetSelection { Keys = new[] { key.Value } }, ShardId = "primary", ShardEpoch = 7, MessageIdPrefix = "mesh-test-scoped-snapshot", @@ -2986,7 +2986,7 @@ public sealed class CultMeshStreamingTests var options = new CultMeshSnapshotRequestOptions { ResponseTimeout = TimeSpan.FromSeconds(30), - RecordKeys = new[] { "eve:surface:test" }, + Selection = new CultNetSelection { Keys = new[] { "eve:surface:test" } }, CreateClient = () => new FailedBackgroundSchemaClient( new FormatException("malformed fragment")) }; @@ -3051,7 +3051,7 @@ public sealed class CultMeshStreamingTests "cultnet://foreign-schema.test:3075", new CultMeshSnapshotRequestOptions { - RecordKeys = new[] { key.Value }, + Selection = new CultNetSelection { Keys = new[] { key.Value } }, CreateClient = () => new MeshSnapshotSchemaClient(request => { snapshot.MessageId = request.MessageId; @@ -3236,7 +3236,7 @@ public sealed class CultMeshStreamingTests endpoint, new CultMeshSnapshotRequestOptions { - RecordKeys = new[] { key.Value }, + Selection = new CultNetSelection { Keys = new[] { key.Value } }, CreateClient = () => new MeshSnapshotSchemaClient(request => networkRegistry.CreateRawSnapshotResponse(sourceCache, request.MessageId, request)) },