Fix batch 3: R-N wire refusal codes, R-P one authorizer id, R-T removal/cites gaps
R-N: cultnet.error.v0 gains additive code/details fields (contracts/cultnet). CultNetErrorMessage grows ForSelectionInvalid/ForCursor/ForReferenceOutsideTarget factories; both C# servers (CultNetDatabaseServer for snapshot+live, CultNetDatabaseSubscriptionServer) use them at every existing catch site, plus two sites that previously let the exception escape uncaught instead of reaching the peer (CultNetDatabaseServer's v1 subscribe snapshot branch, CultNetDatabaseSubscriptionServer's Subscribe around CreateProjectedSnapshot). CultNetSelectionCursorException now carries AsOf/Current for cursor_stale. R-P: CultNetDatabaseSubscriptionServer.CreateMatchedRecord (the live fast path) authorized against descriptor.SchemaId; the snapshot path (CreateProjectedSnapshot) already authorized against the wire id. Both now call the same CultNetDocumentRegistry.WireSchemaId (made internal, was private) so one id reaches the authorizer on the snapshot, live and reconcile paths alike. R-T(a): CultNetSelectionEvaluator.MatchesSchemaKeysFields no longer reads a Removed change's (always-null) document against Fields - it matches on keys/schemas alone and treats a field predicate as unknown instead of throwing TryGetIndexValue(null). R-T(b): EnsureWithinDeclaredTarget no longer skips the out-of-target check when TargetType is null; it checks against every registered leaf (unconstrained is a real, checked target set, not a bypass). CultCache's ResolveReferenceTarget also now infers the target from a many reference's CultRecordRef<T> element when no explicit [CultReference(typeof(...))] is declared, which was the one remaining source of a null TargetType reaching that check on a real, testable path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
c606eb38f2
commit
66b5e60b43
7 changed files with 191 additions and 18 deletions
|
|
@ -245,15 +245,15 @@ namespace GameCult.Networking
|
|||
}
|
||||
catch (CultNetSelectionInvalidException ex)
|
||||
{
|
||||
peer.SendCultNet(new CultNetErrorMessage { Error = $"selection_invalid: {ex.Message}" });
|
||||
peer.SendCultNet(CultNetErrorMessage.ForSelectionInvalid(ex));
|
||||
}
|
||||
catch (CultNetSelectionCursorException ex)
|
||||
{
|
||||
peer.SendCultNet(new CultNetErrorMessage { Error = $"{ex.Code}: {ex.Message}" });
|
||||
peer.SendCultNet(CultNetErrorMessage.ForCursor(ex));
|
||||
}
|
||||
catch (CultNetSelectionReferenceOutsideTargetException ex)
|
||||
{
|
||||
peer.SendCultNet(new CultNetErrorMessage { Error = $"reference_outside_target: {ex.Message}" });
|
||||
peer.SendCultNet(CultNetErrorMessage.ForReferenceOutsideTarget(ex));
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
|
|
@ -379,7 +379,12 @@ namespace GameCult.Networking
|
|||
// refused here rather than reaching Matches' own InvalidOperationException on the first change.
|
||||
if (message.Selection.HasHop)
|
||||
{
|
||||
peer.SendCultNet(new CultNetErrorMessage { Error = "selection_invalid: selection.cites/selection.cited need the subscription server's reconcile loop (D6); this server only fast-matches a single row." });
|
||||
peer.SendCultNet(new CultNetErrorMessage
|
||||
{
|
||||
Error = "selection_invalid: selection.cites/selection.cited need the subscription server's reconcile loop (D6); this server only fast-matches a single row.",
|
||||
Code = "selection_invalid",
|
||||
Details = new CultNetErrorDetails { Field = "cites" }
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
|
@ -389,7 +394,7 @@ namespace GameCult.Networking
|
|||
}
|
||||
catch (CultNetSelectionInvalidException ex)
|
||||
{
|
||||
peer.SendCultNet(new CultNetErrorMessage { Error = $"selection_invalid: {ex.Message}" });
|
||||
peer.SendCultNet(CultNetErrorMessage.ForSelectionInvalid(ex));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
|
@ -405,11 +410,26 @@ namespace GameCult.Networking
|
|||
|
||||
if (message.IncludeSnapshot)
|
||||
{
|
||||
peer.SendCultNet(CreateSelectionResponse(new CultNetSnapshotRequestV1Message
|
||||
try
|
||||
{
|
||||
MessageId = message.MessageId,
|
||||
Selection = message.Selection
|
||||
}));
|
||||
peer.SendCultNet(CreateSelectionResponse(new CultNetSnapshotRequestV1Message
|
||||
{
|
||||
MessageId = message.MessageId,
|
||||
Selection = message.Selection
|
||||
}));
|
||||
}
|
||||
catch (CultNetSelectionInvalidException ex)
|
||||
{
|
||||
peer.SendCultNet(CultNetErrorMessage.ForSelectionInvalid(ex));
|
||||
}
|
||||
catch (CultNetSelectionCursorException ex)
|
||||
{
|
||||
peer.SendCultNet(CultNetErrorMessage.ForCursor(ex));
|
||||
}
|
||||
catch (CultNetSelectionReferenceOutsideTargetException ex)
|
||||
{
|
||||
peer.SendCultNet(CultNetErrorMessage.ForReferenceOutsideTarget(ex));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue