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))
},