2026-05-19 17:03:31 +01:00
|
|
|
using System;
|
2026-05-19 17:10:24 +01:00
|
|
|
using System.Collections.Concurrent;
|
2026-09-29 08:28:24 +02:00
|
|
|
using System.Collections.Generic;
|
2026-05-19 17:10:24 +01:00
|
|
|
using System.Linq;
|
|
|
|
|
using System.Reflection;
|
2026-05-19 17:03:31 +01:00
|
|
|
using System.Threading.Tasks;
|
2026-05-19 17:10:24 +01:00
|
|
|
using GameCult.Caching;
|
2026-05-19 17:03:31 +01:00
|
|
|
using LiteNetLib;
|
2026-05-19 17:10:24 +01:00
|
|
|
using R3;
|
2026-05-19 17:03:31 +01:00
|
|
|
|
|
|
|
|
namespace GameCult.Networking
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Routes CultNet schema-v0 document messages through a <see cref="CultNetDatabase"/>.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public sealed class CultNetDatabaseServer : IDisposable
|
|
|
|
|
{
|
|
|
|
|
private readonly Server _server;
|
|
|
|
|
private readonly CultNetDatabase _database;
|
2026-05-19 17:40:19 +01:00
|
|
|
private readonly CultNetDatabaseServerOptions _options;
|
2026-06-16 16:07:56 +01:00
|
|
|
private readonly Func<CultNetSnapshotRequestMessage, CultNetServerPeer, Task> _snapshotHandler;
|
CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
|
|
|
private readonly Func<CultNetSnapshotRequestV1Message, CultNetServerPeer, Task> _snapshotHandlerV1;
|
2026-06-16 16:07:56 +01:00
|
|
|
private readonly Func<CultNetDocumentPutRawMessage, CultNetServerPeer, Task> _putHandler;
|
|
|
|
|
private readonly Func<CultNetDocumentDeleteMessage, CultNetServerPeer, Task> _deleteHandler;
|
|
|
|
|
private readonly Func<CultNetShardCatalogRequestMessage, CultNetServerPeer, Task> _shardCatalogHandler;
|
|
|
|
|
private readonly Func<CultNetShardLogRequestMessage, CultNetServerPeer, Task> _shardLogHandler;
|
|
|
|
|
private readonly Func<CultNetDatabaseSubscribeMessage, CultNetServerPeer, Task> _subscribeHandler;
|
CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
|
|
|
private readonly Func<CultNetDatabaseSubscribeV1Message, CultNetServerPeer, Task> _subscribeHandlerV1;
|
2026-06-16 16:07:56 +01:00
|
|
|
private readonly Func<CultNetDatabaseUnsubscribeMessage, CultNetServerPeer, Task> _unsubscribeHandler;
|
2026-05-19 17:10:24 +01:00
|
|
|
private readonly ConcurrentDictionary<string, IDisposable> _subscriptions = new(StringComparer.Ordinal);
|
2026-05-19 17:03:31 +01:00
|
|
|
private bool _disposed;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Creates and attaches a database message bridge to a server.
|
|
|
|
|
/// </summary>
|
2026-05-19 17:40:19 +01:00
|
|
|
public CultNetDatabaseServer(
|
|
|
|
|
Server server,
|
|
|
|
|
CultNetDatabase database,
|
|
|
|
|
CultNetDatabaseServerOptions? options = null)
|
2026-05-19 17:03:31 +01:00
|
|
|
{
|
|
|
|
|
_server = server ?? throw new ArgumentNullException(nameof(server));
|
|
|
|
|
_database = database ?? throw new ArgumentNullException(nameof(database));
|
2026-05-19 17:40:19 +01:00
|
|
|
_options = options ?? new CultNetDatabaseServerOptions();
|
2026-05-19 17:03:31 +01:00
|
|
|
_snapshotHandler = HandleSnapshotRequestAsync;
|
CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
|
|
|
_snapshotHandlerV1 = HandleSnapshotRequestV1Async;
|
2026-05-19 17:03:31 +01:00
|
|
|
_putHandler = HandlePutAsync;
|
|
|
|
|
_deleteHandler = HandleDeleteAsync;
|
2026-05-19 17:34:49 +01:00
|
|
|
_shardCatalogHandler = HandleShardCatalogRequestAsync;
|
2026-05-19 18:01:11 +01:00
|
|
|
_shardLogHandler = HandleShardLogRequestAsync;
|
2026-05-19 17:10:24 +01:00
|
|
|
_subscribeHandler = HandleSubscribeAsync;
|
CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
|
|
|
_subscribeHandlerV1 = HandleSubscribeV1Async;
|
2026-05-19 17:10:24 +01:00
|
|
|
_unsubscribeHandler = HandleUnsubscribeAsync;
|
2026-05-19 17:03:31 +01:00
|
|
|
|
|
|
|
|
_server.OnCultNet(_snapshotHandler);
|
CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
|
|
|
_server.OnCultNet(_snapshotHandlerV1);
|
2026-05-19 17:03:31 +01:00
|
|
|
_server.OnCultNet(_putHandler);
|
|
|
|
|
_server.OnCultNet(_deleteHandler);
|
2026-05-19 17:34:49 +01:00
|
|
|
_server.OnCultNet(_shardCatalogHandler);
|
2026-05-19 18:01:11 +01:00
|
|
|
_server.OnCultNet(_shardLogHandler);
|
2026-05-19 17:10:24 +01:00
|
|
|
_server.OnCultNet(_subscribeHandler);
|
CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
|
|
|
_server.OnCultNet(_subscribeHandlerV1);
|
2026-05-19 17:10:24 +01:00
|
|
|
_server.OnCultNet(_unsubscribeHandler);
|
2026-05-19 17:03:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets the database used by this bridge.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public CultNetDatabase Database => _database;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Applies a raw put message through the database shard policy.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Task<object> ApplyPutAsync(CultNetDocumentPutRawMessage message)
|
|
|
|
|
{
|
|
|
|
|
return _database.ApplyPutAsync(message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Applies a raw delete message through the database shard policy.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Task ApplyDeleteAsync(CultNetDocumentDeleteMessage message)
|
|
|
|
|
{
|
|
|
|
|
return _database.ApplyDeleteAsync(message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Creates a raw snapshot response from the database cache.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public CultNetSnapshotResponseRawMessage CreateSnapshotResponse(CultNetSnapshotRequestMessage request)
|
|
|
|
|
{
|
|
|
|
|
if (request == null) throw new ArgumentNullException(nameof(request));
|
2026-05-19 21:07:46 +01:00
|
|
|
if (!string.IsNullOrWhiteSpace(request.ShardId))
|
|
|
|
|
{
|
|
|
|
|
var shard = _database.Shards.FirstOrDefault(candidate =>
|
|
|
|
|
string.Equals(candidate.ShardId, request.ShardId, StringComparison.Ordinal));
|
|
|
|
|
if (shard == null)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException($"Shard '{request.ShardId}' is not known by this database.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (request.ShardEpoch.HasValue && request.ShardEpoch.Value != shard.Epoch)
|
|
|
|
|
{
|
|
|
|
|
throw new CultNetShardAuthorityException(
|
|
|
|
|
shard,
|
|
|
|
|
$"Shard '{shard.ShardId}' is at epoch {shard.Epoch}, not request epoch {request.ShardEpoch.Value}.",
|
|
|
|
|
"stale_epoch");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return _database.CreateShardSnapshotResponse(
|
|
|
|
|
shard,
|
|
|
|
|
string.IsNullOrWhiteSpace(request.MessageId) ? Guid.NewGuid().ToString("N") : request.MessageId,
|
|
|
|
|
request);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 17:03:31 +01:00
|
|
|
return _database.Documents.CreateRawSnapshotResponse(
|
|
|
|
|
_database.Cache,
|
|
|
|
|
string.IsNullOrWhiteSpace(request.MessageId) ? Guid.NewGuid().ToString("N") : request.MessageId,
|
|
|
|
|
request);
|
|
|
|
|
}
|
|
|
|
|
|
CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
|
|
|
/// <summary>
|
|
|
|
|
/// Answers a typed-selection snapshot request (R-A, docs/cultnet-selection-cut.md): the door,
|
|
|
|
|
/// then one evaluation over the database's rows in last-write order, through
|
|
|
|
|
/// <see cref="CultNetDocumentRegistry.CreateSelectionResponse"/>.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public CultNetSnapshotResponseRawV1Message CreateSelectionResponse(CultNetSnapshotRequestV1Message request)
|
|
|
|
|
{
|
|
|
|
|
if (request == null) throw new ArgumentNullException(nameof(request));
|
|
|
|
|
return _database.Documents.CreateSelectionResponse(
|
|
|
|
|
_database.Cache,
|
|
|
|
|
string.IsNullOrWhiteSpace(request.MessageId) ? Guid.NewGuid().ToString("N") : request.MessageId,
|
|
|
|
|
request.Selection,
|
|
|
|
|
ordinalOf: (schemaId, key) => _database.LastWriteSequence(schemaId, key) ?? 0,
|
Land fix batch 3's R-O and R-Q: keyed length-prefixed cursors, replica-apply sequence gap
R-O: CultNetSelectionCursorKey is a per-process (per-CultNetDatabase-instance) 32-byte
random key. CultNetSelectionCursor.ComputeDigest is now an HMAC-SHA256 under that key
instead of an unkeyed SHA-256, so a cursor cannot be forged without the key and does not
verify across a restart (a fresh CultNetDatabase mints a fresh key). The cursor body
(asOf/ordinal/schemaId/recordKey/digest) is length-prefixed (<byte length>:<value>, no
delimiter) instead of joined with a bare "\x01"-separated string, so a record key
carrying any character, U+241F included, round-trips. CultNetSelectionEvaluator.Select/
Page/ComputeDigest take an optional CultNetSelectionCursorKey, defaulting to a
process-wide key generated once so existing callers that never wire up a database still
get a keyed digest. CultNetDatabase gets a CursorKey property (random unless
CultNetDatabaseOptions.CursorKey is supplied) and passes it through
CultNetDatabaseServer.CreateSelectionResponse.
Rust already landed R-O (5aac120) under its own algorithm, deliberately not byte-matching
the reference: its own doc comment says a cursor is never handed across runtimes, so only
collision-freedom and keying are load-bearing, not the wire bytes. That is a real
divergence from this pass's instruction to match Rust's wire encoding exactly - reported
in full in the batch report.
R-Q: the two replica-apply paths (ApplyCommittedPutAsync, ApplyCommittedDelete) recorded
the mutation-log entry but never set _lastWriteSequence, so a row's order/cursor position
looked like "never written" until it was written again locally. Both now set it from the
entry's own sequence, matching AppendMutationLogEntry's local-commit path.
InitializeLogSequencesFromStore rebuilt only _nextLogSequences at startup; it now also
rebuilds _lastWriteSequence by replaying the durable log's Put/Delete entries in sequence
order. CultNetDatabase.ResolveShard is a new public wrapper over the existing shard
resolution; CultNetDocumentRegistry.SelectPage/CreateSelectionResponse take an optional
shardIdOf callback and refuse (reusing CultNetSelectionInvalidException/selection_invalid,
R-N) when a selection's matched rows resolve to more than one shard's log, because asOf is
a single watermark and is not exact for a shard-spanning page.
CultNetDatabaseServer.CreateSelectionResponse wires shardIdOf/cursorKey through from the
database.
Also fixes a defect the Rust Hands found while matching wire bytes:
CultNetErrorMessage.ForReferenceOutsideTarget double-prefixed "reference_outside_target: "
- the exception's own message already carries it. Now prefixed once, matching Rust's
ForReferenceOutsideTarget.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 20:04:22 +02:00
|
|
|
asOf: _database.CurrentAsOf(),
|
|
|
|
|
// R-Q: asOf is one shard-log watermark, so a selection whose matched rows span more than
|
|
|
|
|
// one shard's log is refused rather than answered against a watermark that is not
|
|
|
|
|
// exact for all of them.
|
2026-09-30 05:29:35 +02:00
|
|
|
// A row whose schema no shard owns is not part of what this database serves: it is left out, not an error.
|
|
|
|
|
rowFilter: _database.Owns,
|
Land fix batch 3's R-O and R-Q: keyed length-prefixed cursors, replica-apply sequence gap
R-O: CultNetSelectionCursorKey is a per-process (per-CultNetDatabase-instance) 32-byte
random key. CultNetSelectionCursor.ComputeDigest is now an HMAC-SHA256 under that key
instead of an unkeyed SHA-256, so a cursor cannot be forged without the key and does not
verify across a restart (a fresh CultNetDatabase mints a fresh key). The cursor body
(asOf/ordinal/schemaId/recordKey/digest) is length-prefixed (<byte length>:<value>, no
delimiter) instead of joined with a bare "\x01"-separated string, so a record key
carrying any character, U+241F included, round-trips. CultNetSelectionEvaluator.Select/
Page/ComputeDigest take an optional CultNetSelectionCursorKey, defaulting to a
process-wide key generated once so existing callers that never wire up a database still
get a keyed digest. CultNetDatabase gets a CursorKey property (random unless
CultNetDatabaseOptions.CursorKey is supplied) and passes it through
CultNetDatabaseServer.CreateSelectionResponse.
Rust already landed R-O (5aac120) under its own algorithm, deliberately not byte-matching
the reference: its own doc comment says a cursor is never handed across runtimes, so only
collision-freedom and keying are load-bearing, not the wire bytes. That is a real
divergence from this pass's instruction to match Rust's wire encoding exactly - reported
in full in the batch report.
R-Q: the two replica-apply paths (ApplyCommittedPutAsync, ApplyCommittedDelete) recorded
the mutation-log entry but never set _lastWriteSequence, so a row's order/cursor position
looked like "never written" until it was written again locally. Both now set it from the
entry's own sequence, matching AppendMutationLogEntry's local-commit path.
InitializeLogSequencesFromStore rebuilt only _nextLogSequences at startup; it now also
rebuilds _lastWriteSequence by replaying the durable log's Put/Delete entries in sequence
order. CultNetDatabase.ResolveShard is a new public wrapper over the existing shard
resolution; CultNetDocumentRegistry.SelectPage/CreateSelectionResponse take an optional
shardIdOf callback and refuse (reusing CultNetSelectionInvalidException/selection_invalid,
R-N) when a selection's matched rows resolve to more than one shard's log, because asOf is
a single watermark and is not exact for a shard-spanning page.
CultNetDatabaseServer.CreateSelectionResponse wires shardIdOf/cursorKey through from the
database.
Also fixes a defect the Rust Hands found while matching wire bytes:
CultNetErrorMessage.ForReferenceOutsideTarget double-prefixed "reference_outside_target: "
- the exception's own message already carries it. Now prefixed once, matching Rust's
ForReferenceOutsideTarget.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 20:04:22 +02:00
|
|
|
shardIdOf: (schemaId, key) => _database.ResolveShard(schemaId, key).ShardId,
|
Fix batch 4 (S-9/S-11): shard-scoped asOf, and the v1 subscribe door runs before the hop refusal
S-9: CurrentAsOf() returned the maximum shard-log sequence across every
shard the database holds, but that sequence is a per-shard counter -
mixing shards' counters together could report a page as exact "as of" a
sequence a different, untouched shard advanced to while the page's own
shard sat still. CultNetDatabase gains CurrentAsOf(shardId), the watermark
of one shard's own log, and CultNetDocumentRegistry.SelectPage now scopes
asOf to the single shard EnsureSingleShard already resolves the matched
rows to, falling back to the caller-supplied value only when the selection
matches no rows.
S-11: CultNetDatabaseServer.HandleSubscribeV1Async refused a hop-bearing
selection before running the door (Validate), so a selection invalid for
an unrelated reason (an empty keys list, an undeclared index, ...) got a
hop refusal that had nothing to do with why it was actually rejected. The
door now runs first, unconditionally. The hop refusal also always named
the field "cites" even when the selection carried Cited instead; it now
names whichever hop is actually set.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 21:12:07 +02:00
|
|
|
cursorKey: _database.CursorKey,
|
|
|
|
|
// S-9: once the matched rows resolve to one shard, asOf is that shard's own watermark,
|
|
|
|
|
// not CurrentAsOf()'s database-wide maximum across every shard.
|
|
|
|
|
asOfForShard: _database.CurrentAsOf);
|
CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-19 17:34:49 +01:00
|
|
|
/// <summary>
|
|
|
|
|
/// Creates a shard catalog response from the database shard map.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public CultNetShardCatalogResponseMessage CreateShardCatalogResponse(CultNetShardCatalogRequestMessage request)
|
|
|
|
|
{
|
|
|
|
|
return _database.CreateShardCatalogResponse(request);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 18:01:11 +01:00
|
|
|
/// <summary>
|
|
|
|
|
/// Creates a shard mutation-log response from the database log.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public CultNetShardLogResponseMessage CreateShardLogResponse(CultNetShardLogRequestMessage request)
|
|
|
|
|
{
|
|
|
|
|
if (request == null) throw new ArgumentNullException(nameof(request));
|
|
|
|
|
if (string.IsNullOrWhiteSpace(request.ShardId))
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentException("Shard log request requires a shardId.", nameof(request));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var shard = _database.Shards.FirstOrDefault(candidate =>
|
|
|
|
|
string.Equals(candidate.ShardId, request.ShardId, StringComparison.Ordinal));
|
|
|
|
|
if (shard == null)
|
|
|
|
|
{
|
|
|
|
|
return new CultNetShardLogResponseMessage
|
|
|
|
|
{
|
|
|
|
|
MessageId = RequestMessageId(request.MessageId),
|
|
|
|
|
ShardId = request.ShardId,
|
|
|
|
|
ResyncRequired = true,
|
|
|
|
|
Reason = "unknown_shard"
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (request.ShardEpoch.HasValue && request.ShardEpoch.Value != shard.Epoch)
|
|
|
|
|
{
|
|
|
|
|
return new CultNetShardLogResponseMessage
|
|
|
|
|
{
|
|
|
|
|
MessageId = RequestMessageId(request.MessageId),
|
|
|
|
|
ShardId = request.ShardId,
|
|
|
|
|
ShardEpoch = shard.Epoch,
|
|
|
|
|
ResyncRequired = true,
|
|
|
|
|
Reason = "stale_epoch"
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 20:53:45 +01:00
|
|
|
var compactedThrough = _database.GetCompactedMutationLogSequence(request.ShardId);
|
|
|
|
|
if (request.AfterSequence < compactedThrough)
|
|
|
|
|
{
|
|
|
|
|
return new CultNetShardLogResponseMessage
|
|
|
|
|
{
|
|
|
|
|
MessageId = RequestMessageId(request.MessageId),
|
|
|
|
|
ShardId = request.ShardId,
|
|
|
|
|
ShardEpoch = shard.Epoch,
|
|
|
|
|
ResyncRequired = true,
|
|
|
|
|
Reason = "compacted_history",
|
|
|
|
|
CompactedThrough = compactedThrough
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 19:55:29 +01:00
|
|
|
var entries = _database.GetMutationLogMessages(request.ShardId, request.AfterSequence, request.Limit)
|
2026-05-19 18:01:11 +01:00
|
|
|
.ToArray();
|
|
|
|
|
|
|
|
|
|
return new CultNetShardLogResponseMessage
|
|
|
|
|
{
|
|
|
|
|
MessageId = RequestMessageId(request.MessageId),
|
|
|
|
|
ShardId = request.ShardId,
|
|
|
|
|
ShardEpoch = shard.Epoch,
|
|
|
|
|
Entries = entries
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 17:03:31 +01:00
|
|
|
/// <summary>
|
|
|
|
|
/// Detaches handlers from the server.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
if (_disposed)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_disposed = true;
|
|
|
|
|
_server.RemoveCultNetMessageListener<CultNetSnapshotRequestMessage>(_snapshotHandler);
|
CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
|
|
|
_server.RemoveCultNetMessageListener<CultNetSnapshotRequestV1Message>(_snapshotHandlerV1);
|
2026-05-19 17:03:31 +01:00
|
|
|
_server.RemoveCultNetMessageListener<CultNetDocumentPutRawMessage>(_putHandler);
|
|
|
|
|
_server.RemoveCultNetMessageListener<CultNetDocumentDeleteMessage>(_deleteHandler);
|
2026-05-19 17:34:49 +01:00
|
|
|
_server.RemoveCultNetMessageListener<CultNetShardCatalogRequestMessage>(_shardCatalogHandler);
|
2026-05-19 18:01:11 +01:00
|
|
|
_server.RemoveCultNetMessageListener<CultNetShardLogRequestMessage>(_shardLogHandler);
|
2026-05-19 17:10:24 +01:00
|
|
|
_server.RemoveCultNetMessageListener<CultNetDatabaseSubscribeMessage>(_subscribeHandler);
|
CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
|
|
|
_server.RemoveCultNetMessageListener<CultNetDatabaseSubscribeV1Message>(_subscribeHandlerV1);
|
2026-05-19 17:10:24 +01:00
|
|
|
_server.RemoveCultNetMessageListener<CultNetDatabaseUnsubscribeMessage>(_unsubscribeHandler);
|
|
|
|
|
foreach (var subscription in _subscriptions.Values)
|
|
|
|
|
{
|
|
|
|
|
subscription.Dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_subscriptions.Clear();
|
2026-05-19 17:03:31 +01:00
|
|
|
}
|
|
|
|
|
|
2026-06-16 11:54:54 +01:00
|
|
|
private Task HandleSnapshotRequestAsync(CultNetSnapshotRequestMessage request, CultNetServerPeer peer)
|
|
|
|
|
{
|
|
|
|
|
peer.SendCultNet(CreateSnapshotResponse(request));
|
2026-05-19 17:03:31 +01:00
|
|
|
return Task.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
|
|
|
private Task HandleSnapshotRequestV1Async(CultNetSnapshotRequestV1Message request, CultNetServerPeer peer)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
peer.SendCultNet(CreateSelectionResponse(request));
|
|
|
|
|
}
|
|
|
|
|
catch (CultNetSelectionInvalidException ex)
|
|
|
|
|
{
|
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>
2026-09-22 19:44:18 +02:00
|
|
|
peer.SendCultNet(CultNetErrorMessage.ForSelectionInvalid(ex));
|
CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
|
|
|
}
|
|
|
|
|
catch (CultNetSelectionCursorException ex)
|
|
|
|
|
{
|
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>
2026-09-22 19:44:18 +02:00
|
|
|
peer.SendCultNet(CultNetErrorMessage.ForCursor(ex));
|
CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
|
|
|
}
|
|
|
|
|
catch (CultNetSelectionReferenceOutsideTargetException ex)
|
|
|
|
|
{
|
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>
2026-09-22 19:44:18 +02:00
|
|
|
peer.SendCultNet(CultNetErrorMessage.ForReferenceOutsideTarget(ex));
|
CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
|
|
|
}
|
2026-09-23 07:15:43 +02:00
|
|
|
// R-AM: an untyped fault answers like HandleShardLogRequestAsync/HandlePutAsync's own
|
|
|
|
|
// catch (Exception) - a CultNetErrorMessage on the wire, not silence. Before this, only
|
|
|
|
|
// the three typed selection exceptions were caught here; anything else propagated up into
|
|
|
|
|
// the dispatch backstop (CultNetRudpSchemaServer.cs), which swallowed it with no log and
|
|
|
|
|
// hid SM-6's ArgumentNullException regression for a whole batch.
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_server.Logger.LogError($"CultNet snapshot v1 request failed: {ex.Message}");
|
|
|
|
|
peer.SendCultNet(new CultNetErrorMessage { Error = ex.Message });
|
|
|
|
|
}
|
CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
|
|
|
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 11:54:54 +01:00
|
|
|
private Task HandleShardCatalogRequestAsync(CultNetShardCatalogRequestMessage request, CultNetServerPeer peer)
|
|
|
|
|
{
|
|
|
|
|
peer.SendCultNet(CreateShardCatalogResponse(request));
|
2026-05-19 17:34:49 +01:00
|
|
|
return Task.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 11:54:54 +01:00
|
|
|
private Task HandleShardLogRequestAsync(CultNetShardLogRequestMessage request, CultNetServerPeer peer)
|
2026-05-19 18:01:11 +01:00
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-06-16 11:54:54 +01:00
|
|
|
peer.SendCultNet(CreateShardLogResponse(request));
|
2026-05-19 18:01:11 +01:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-06-16 11:54:54 +01:00
|
|
|
peer.SendCultNet(new CultNetErrorMessage { Error = ex.Message });
|
2026-05-19 18:01:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 11:54:54 +01:00
|
|
|
private async Task HandlePutAsync(CultNetDocumentPutRawMessage message, CultNetServerPeer peer)
|
2026-05-19 17:03:31 +01:00
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await ApplyPutAsync(message).ConfigureAwait(false);
|
|
|
|
|
}
|
2026-05-19 17:34:49 +01:00
|
|
|
catch (CultNetShardAuthorityException ex)
|
|
|
|
|
{
|
2026-05-19 17:40:19 +01:00
|
|
|
if (!await TryForwardPutAsync(ex, message).ConfigureAwait(false))
|
|
|
|
|
{
|
2026-06-15 19:43:29 +01:00
|
|
|
_server.Logger.LogError($"CultNet raw put authority failed: {ex.Message}");
|
2026-06-16 11:54:54 +01:00
|
|
|
peer.SendCultNet(CreateRoutingError(ex));
|
2026-05-19 17:40:19 +01:00
|
|
|
}
|
2026-05-19 17:34:49 +01:00
|
|
|
}
|
2026-05-19 17:03:31 +01:00
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-06-15 19:43:29 +01:00
|
|
|
_server.Logger.LogError($"CultNet raw put failed: {ex.Message}");
|
2026-06-16 11:54:54 +01:00
|
|
|
peer.SendCultNet(new CultNetErrorMessage { Error = ex.Message });
|
2026-05-19 17:03:31 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 11:54:54 +01:00
|
|
|
private async Task HandleDeleteAsync(CultNetDocumentDeleteMessage message, CultNetServerPeer peer)
|
2026-05-19 17:03:31 +01:00
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await ApplyDeleteAsync(message).ConfigureAwait(false);
|
|
|
|
|
}
|
2026-05-19 17:34:49 +01:00
|
|
|
catch (CultNetShardAuthorityException ex)
|
|
|
|
|
{
|
2026-05-19 17:40:19 +01:00
|
|
|
if (!await TryForwardDeleteAsync(ex, message).ConfigureAwait(false))
|
|
|
|
|
{
|
2026-06-16 11:54:54 +01:00
|
|
|
peer.SendCultNet(CreateRoutingError(ex));
|
2026-05-19 17:40:19 +01:00
|
|
|
}
|
2026-05-19 17:34:49 +01:00
|
|
|
}
|
2026-05-19 17:03:31 +01:00
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-06-16 11:54:54 +01:00
|
|
|
peer.SendCultNet(new CultNetErrorMessage { Error = ex.Message });
|
2026-05-19 17:03:31 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-19 17:10:24 +01:00
|
|
|
|
2026-06-16 11:54:54 +01:00
|
|
|
private Task HandleSubscribeAsync(CultNetDatabaseSubscribeMessage message, CultNetServerPeer peer)
|
2026-05-19 17:10:24 +01:00
|
|
|
{
|
|
|
|
|
var subscriptionId = string.IsNullOrWhiteSpace(message.SubscriptionId)
|
|
|
|
|
? message.MessageId
|
|
|
|
|
: message.SubscriptionId;
|
|
|
|
|
if (string.IsNullOrWhiteSpace(subscriptionId))
|
|
|
|
|
{
|
2026-06-16 11:54:54 +01:00
|
|
|
peer.SendCultNet(new CultNetErrorMessage { Error = "Database subscription requires a subscriptionId or messageId." });
|
2026-05-19 17:10:24 +01:00
|
|
|
return Task.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 11:54:54 +01:00
|
|
|
var key = SubscriptionKey(peer.Peer, subscriptionId);
|
2026-05-19 17:10:24 +01:00
|
|
|
_subscriptions.AddOrUpdate(
|
|
|
|
|
key,
|
|
|
|
|
_ => CreateSubscription(message, subscriptionId, peer),
|
|
|
|
|
(_, existing) =>
|
|
|
|
|
{
|
|
|
|
|
existing.Dispose();
|
|
|
|
|
return CreateSubscription(message, subscriptionId, peer);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (message.IncludeSnapshot)
|
|
|
|
|
{
|
2026-06-16 11:54:54 +01:00
|
|
|
peer.SendCultNet(CreateSnapshotResponse(new CultNetSnapshotRequestMessage
|
2026-05-19 17:10:24 +01:00
|
|
|
{
|
|
|
|
|
MessageId = message.MessageId,
|
|
|
|
|
SchemaIds = message.SchemaIds,
|
|
|
|
|
RecordKeys = message.RecordKeys
|
|
|
|
|
}));
|
|
|
|
|
}
|
2026-07-13 04:19:54 +01:00
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
peer.SendCultNet(new CultNetSnapshotResponseRawMessage
|
|
|
|
|
{
|
|
|
|
|
MessageId = message.MessageId,
|
|
|
|
|
Documents = Array.Empty<CultNetRawDocumentRecord>()
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-05-19 17:10:24 +01:00
|
|
|
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
|
|
|
private Task HandleSubscribeV1Async(CultNetDatabaseSubscribeV1Message message, CultNetServerPeer peer)
|
|
|
|
|
{
|
|
|
|
|
var subscriptionId = string.IsNullOrWhiteSpace(message.SubscriptionId)
|
|
|
|
|
? message.MessageId
|
|
|
|
|
: message.SubscriptionId;
|
|
|
|
|
if (string.IsNullOrWhiteSpace(subscriptionId))
|
|
|
|
|
{
|
|
|
|
|
peer.SendCultNet(new CultNetErrorMessage { Error = "Database subscription requires a subscriptionId or messageId." });
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
Fix batch 4 (S-9/S-11): shard-scoped asOf, and the v1 subscribe door runs before the hop refusal
S-9: CurrentAsOf() returned the maximum shard-log sequence across every
shard the database holds, but that sequence is a per-shard counter -
mixing shards' counters together could report a page as exact "as of" a
sequence a different, untouched shard advanced to while the page's own
shard sat still. CultNetDatabase gains CurrentAsOf(shardId), the watermark
of one shard's own log, and CultNetDocumentRegistry.SelectPage now scopes
asOf to the single shard EnsureSingleShard already resolves the matched
rows to, falling back to the caller-supplied value only when the selection
matches no rows.
S-11: CultNetDatabaseServer.HandleSubscribeV1Async refused a hop-bearing
selection before running the door (Validate), so a selection invalid for
an unrelated reason (an empty keys list, an undeclared index, ...) got a
hop refusal that had nothing to do with why it was actually rejected. The
door now runs first, unconditionally. The hop refusal also always named
the field "cites" even when the selection carried Cited instead; it now
names whichever hop is actually set.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 21:12:07 +02:00
|
|
|
// S-11: the door runs first, unconditionally (R-F) - a selection that is invalid for reasons
|
|
|
|
|
// unrelated to its hop (an empty keys list, an undeclared index, ...) reports that refusal,
|
|
|
|
|
// not a hop refusal that has nothing to do with why it was actually rejected.
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
message.Selection.Validate(_database.Cache.Registry.AllDescriptors.ToArray());
|
|
|
|
|
}
|
|
|
|
|
catch (CultNetSelectionInvalidException ex)
|
|
|
|
|
{
|
|
|
|
|
peer.SendCultNet(CultNetErrorMessage.ForSelectionInvalid(ex));
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
|
|
|
// This server delivers live changes through the single-row fast path (CreateChangeMessage /
|
|
|
|
|
// CultNetSelectionEvaluator.Matches), which is set-independent by construction - the same
|
|
|
|
|
// ceiling v0 subscriptions on this server already have. A hop-bearing selection is
|
|
|
|
|
// set-dependent (D6) and needs the subscription server's reconcile loop instead, so it is
|
Fix batch 4 (S-9/S-11): shard-scoped asOf, and the v1 subscribe door runs before the hop refusal
S-9: CurrentAsOf() returned the maximum shard-log sequence across every
shard the database holds, but that sequence is a per-shard counter -
mixing shards' counters together could report a page as exact "as of" a
sequence a different, untouched shard advanced to while the page's own
shard sat still. CultNetDatabase gains CurrentAsOf(shardId), the watermark
of one shard's own log, and CultNetDocumentRegistry.SelectPage now scopes
asOf to the single shard EnsureSingleShard already resolves the matched
rows to, falling back to the caller-supplied value only when the selection
matches no rows.
S-11: CultNetDatabaseServer.HandleSubscribeV1Async refused a hop-bearing
selection before running the door (Validate), so a selection invalid for
an unrelated reason (an empty keys list, an undeclared index, ...) got a
hop refusal that had nothing to do with why it was actually rejected. The
door now runs first, unconditionally. The hop refusal also always named
the field "cites" even when the selection carried Cited instead; it now
names whichever hop is actually set.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 21:12:07 +02:00
|
|
|
// refused here, after the door, rather than reaching Matches' own InvalidOperationException
|
|
|
|
|
// on the first change. The refused field names whichever hop is actually set - cited, not
|
|
|
|
|
// always cites - so the refusal describes the selection that was actually sent.
|
CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
|
|
|
if (message.Selection.HasHop)
|
|
|
|
|
{
|
Fix batch 4 (S-9/S-11): shard-scoped asOf, and the v1 subscribe door runs before the hop refusal
S-9: CurrentAsOf() returned the maximum shard-log sequence across every
shard the database holds, but that sequence is a per-shard counter -
mixing shards' counters together could report a page as exact "as of" a
sequence a different, untouched shard advanced to while the page's own
shard sat still. CultNetDatabase gains CurrentAsOf(shardId), the watermark
of one shard's own log, and CultNetDocumentRegistry.SelectPage now scopes
asOf to the single shard EnsureSingleShard already resolves the matched
rows to, falling back to the caller-supplied value only when the selection
matches no rows.
S-11: CultNetDatabaseServer.HandleSubscribeV1Async refused a hop-bearing
selection before running the door (Validate), so a selection invalid for
an unrelated reason (an empty keys list, an undeclared index, ...) got a
hop refusal that had nothing to do with why it was actually rejected. The
door now runs first, unconditionally. The hop refusal also always named
the field "cites" even when the selection carried Cited instead; it now
names whichever hop is actually set.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 21:12:07 +02:00
|
|
|
var hopField = message.Selection.Cites != null ? "cites" : "cited";
|
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>
2026-09-22 19:44:18 +02:00
|
|
|
peer.SendCultNet(new CultNetErrorMessage
|
|
|
|
|
{
|
Fix batch 4 (S-9/S-11): shard-scoped asOf, and the v1 subscribe door runs before the hop refusal
S-9: CurrentAsOf() returned the maximum shard-log sequence across every
shard the database holds, but that sequence is a per-shard counter -
mixing shards' counters together could report a page as exact "as of" a
sequence a different, untouched shard advanced to while the page's own
shard sat still. CultNetDatabase gains CurrentAsOf(shardId), the watermark
of one shard's own log, and CultNetDocumentRegistry.SelectPage now scopes
asOf to the single shard EnsureSingleShard already resolves the matched
rows to, falling back to the caller-supplied value only when the selection
matches no rows.
S-11: CultNetDatabaseServer.HandleSubscribeV1Async refused a hop-bearing
selection before running the door (Validate), so a selection invalid for
an unrelated reason (an empty keys list, an undeclared index, ...) got a
hop refusal that had nothing to do with why it was actually rejected. The
door now runs first, unconditionally. The hop refusal also always named
the field "cites" even when the selection carried Cited instead; it now
names whichever hop is actually set.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 21:12:07 +02:00
|
|
|
Error = $"selection_invalid: selection.{hopField} needs the subscription server's reconcile loop (D6); this server only fast-matches a single row.",
|
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>
2026-09-22 19:44:18 +02:00
|
|
|
Code = "selection_invalid",
|
Fix batch 4 (S-9/S-11): shard-scoped asOf, and the v1 subscribe door runs before the hop refusal
S-9: CurrentAsOf() returned the maximum shard-log sequence across every
shard the database holds, but that sequence is a per-shard counter -
mixing shards' counters together could report a page as exact "as of" a
sequence a different, untouched shard advanced to while the page's own
shard sat still. CultNetDatabase gains CurrentAsOf(shardId), the watermark
of one shard's own log, and CultNetDocumentRegistry.SelectPage now scopes
asOf to the single shard EnsureSingleShard already resolves the matched
rows to, falling back to the caller-supplied value only when the selection
matches no rows.
S-11: CultNetDatabaseServer.HandleSubscribeV1Async refused a hop-bearing
selection before running the door (Validate), so a selection invalid for
an unrelated reason (an empty keys list, an undeclared index, ...) got a
hop refusal that had nothing to do with why it was actually rejected. The
door now runs first, unconditionally. The hop refusal also always named
the field "cites" even when the selection carried Cited instead; it now
names whichever hop is actually set.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 21:12:07 +02:00
|
|
|
Details = new CultNetErrorDetails { Field = hopField }
|
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>
2026-09-22 19:44:18 +02:00
|
|
|
});
|
CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
|
|
|
return Task.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var key = SubscriptionKey(peer.Peer, subscriptionId);
|
|
|
|
|
_subscriptions.AddOrUpdate(
|
|
|
|
|
key,
|
|
|
|
|
_ => CreateSubscription(message.Selection, subscriptionId, peer),
|
|
|
|
|
(_, existing) =>
|
|
|
|
|
{
|
|
|
|
|
existing.Dispose();
|
|
|
|
|
return CreateSubscription(message.Selection, subscriptionId, peer);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (message.IncludeSnapshot)
|
|
|
|
|
{
|
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>
2026-09-22 19:44:18 +02:00
|
|
|
try
|
CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
|
|
|
{
|
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>
2026-09-22 19:44:18 +02:00
|
|
|
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));
|
|
|
|
|
}
|
CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
peer.SendCultNet(new CultNetSnapshotResponseRawV1Message
|
|
|
|
|
{
|
|
|
|
|
MessageId = message.MessageId,
|
|
|
|
|
AsOf = _database.CurrentAsOf()
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 16:07:56 +01:00
|
|
|
private Task HandleUnsubscribeAsync(CultNetDatabaseUnsubscribeMessage message, CultNetServerPeer peer)
|
2026-05-19 17:10:24 +01:00
|
|
|
{
|
|
|
|
|
var subscriptionId = string.IsNullOrWhiteSpace(message.SubscriptionId)
|
|
|
|
|
? message.MessageId
|
|
|
|
|
: message.SubscriptionId;
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(subscriptionId) &&
|
2026-06-16 16:07:56 +01:00
|
|
|
_subscriptions.TryRemove(SubscriptionKey(peer.Peer, subscriptionId), out var subscription))
|
2026-05-19 17:10:24 +01:00
|
|
|
{
|
|
|
|
|
subscription.Dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private IDisposable CreateSubscription(
|
|
|
|
|
CultNetDatabaseSubscribeMessage request,
|
|
|
|
|
string subscriptionId,
|
2026-06-16 11:54:54 +01:00
|
|
|
CultNetServerPeer peer)
|
2026-05-19 17:10:24 +01:00
|
|
|
{
|
CultNet selection cut1, commit 1: the shape, the evaluator, v1 messages
docs/cultnet-selection-cut.md section 2/4/6/7/8: CultNetSelection (schemas,
keys, fields, cites, cited, projection, descending, limit, cursor) with its
door validation (S1), and CultNetSelectionEvaluator: the single-row Matches
fast path (D6) and the full Select (order, hop, cursor, paging). Neither
reflects over a document; both read only through the cache's public surface
(commit 0).
Three v1 messages beside v0 (cultnet.snapshot_request.v1,
cultnet.database_subscribe.v1, cultnet.snapshot_response_raw.v1), their
schema files under contracts/cultnet, registry entries and dispatch arms.
CultNetSchemaAliasMatching goes internal -> public (D4), the one alias
matcher. CultNetDatabase gains LastWriteSequence(schemaId, key), confirmed
at the one append site (AppendMutationLogEntry) that every committed write
takes a shard-log sequence, including a local PutAsync outside any shard.
Three engines deleted, one evaluator wired in their place:
- CultNetDocumentRegistry: CreateRawSnapshotResponse's reflection loop over
cache.AllEntries and MatchesRequestedSchema gone; cache.AllStoredDocuments
replaces the reflection entirely. v0 lowers into a Selection and walks the
evaluator's own pages so a v0 caller still sees its whole matching set
(D3: v0 has no engine of its own). CreateSelectionResponse/SelectPage/
ToRawRecord/CreateSnapshotRequestV1 added.
- CultNetDatabaseServer: CreateChangeMessage/CreateRawDocumentRecord/Matches/
MatchesSchema/InferSchemaName (122 lines) deleted; CreateSubscription lowers
v0 into a Selection and matches through the evaluator.
- CultNetDatabaseSubscriptionServer: CreateAuthorizedChange/CreateChange/
CreateChangeCore/ResolveWireSchemaId/MatchesRequestedSchema/CreateRawRecord
(92 lines) deleted, replaced by CreateMatchedRecord. _projectRecord and
CreateProjectedSnapshot's collapse are commit 2's, per the map's own
deletion line, and are untouched here.
Discrepancy found and fixed (not in the map): CultNetDocumentBinding can
carry a schema id different from its descriptor's own
(CultNetDocumentBinding.ForDocument's schemaId override), which the old
per-engine matchers checked and a descriptor-only evaluator cannot see by
design (it stays decoupled from CultNetDocumentRegistry). Fixed at the two
call sites and in CultNetDocumentRegistry.SelectPage by resolving the
binding's schema id into the effective selection before evaluation
(CultNetSchemaAliasMatching.WithBindingSchemaAlias,
CultNetDocumentRegistry.ExpandSchemaBindingAliases), not in the evaluator.
Found by DatabaseSubscription_FiltersLiveChangesByWireSchemaBinding timing
out; both call sites now pass.
Second discrepancy, reported not fixed: CultNetDatabase.cs:1707 has a fourth,
undocumented copy of schema-matching logic (MatchesRequestedSchema, private,
serving CreateShardSnapshotResponse), which the map's section 1 audit did not
name among "three separate selector engines". Left untouched this commit:
it is out of the map's explicit deletes list and touching it risks shard
snapshot behavior with no budgeted verification.
Verification: dotnet test tests/GameCult.Networking.Tests - 162 passed (150
existing + 12 new), 0 failed; the seven map-named tests all still pass, two
rewritten for the new signatures (CreateChangeMessage now takes
CultNetSelection; CreateChange/CreateAuthorizedChange collapsed into
CreateMatchedRecord, returning CultNetRawDocumentRecord? directly).
tests/GameCult.Mesh.Tests - 252 passed, 1 pre-existing skip, 0 failed (no
Mesh source touched this commit). tests/GameCult.Caching.Tests - unaffected
(188, unchanged from commit 0).
Negative greps: GetProperty|GetField|GetCustomAttribute in
GameCult.Networking - only against the generic `object change` dispatch
(CultNetDatabaseChange<T>) and CultRecordHandle<T> construction, pre-existing
and out of this cut's scope, never against a document's declared values.
_projectRecord survives in CultNetDatabaseSubscriptionServer.cs (expected,
commit 2). CultNetSchemaAliasMatching is the only schema matcher outside the
one reported discrepancy.
scripts/mutate-dotnet.ps1: the caching mutation runner generalized into a
shared dotnet/NUnit harness (Target/TestProject/Entries/ControlFilter),
replacing scripts/mutate-cultnet-selection-caching.ps1 with an entries file
of the same six mutations (re-verified: all six still KILL). It is now
generic to any NUnit project in this repo, not only this campaign's two
targets - a candidate to sit beside tools/eureka-mutations.ps1 as the
dotnet-side counterpart. Nine new networking entries (S3, S5 x2, S7 x2,
S16 x2, S18) all KILLED. Not yet mutation-run: S1, S2, S6, S23 (pinned by
tests, not by a committed mutant); S9-S15 (subscription reconcile lowering,
v0 no-engine-of-its-own); S17, S20 (pinned by tests only). S19/S21/S22 are
cache-layer, covered in commit 0.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 02:31:42 +02:00
|
|
|
// v0 has no engine of its own (docs/cultnet-selection-cut.md, D3/D4): it lowers into a
|
CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
|
|
|
// Selection and is matched by the one evaluator, same as a v1 subscribe.
|
CultNet selection cut1, commit 1: the shape, the evaluator, v1 messages
docs/cultnet-selection-cut.md section 2/4/6/7/8: CultNetSelection (schemas,
keys, fields, cites, cited, projection, descending, limit, cursor) with its
door validation (S1), and CultNetSelectionEvaluator: the single-row Matches
fast path (D6) and the full Select (order, hop, cursor, paging). Neither
reflects over a document; both read only through the cache's public surface
(commit 0).
Three v1 messages beside v0 (cultnet.snapshot_request.v1,
cultnet.database_subscribe.v1, cultnet.snapshot_response_raw.v1), their
schema files under contracts/cultnet, registry entries and dispatch arms.
CultNetSchemaAliasMatching goes internal -> public (D4), the one alias
matcher. CultNetDatabase gains LastWriteSequence(schemaId, key), confirmed
at the one append site (AppendMutationLogEntry) that every committed write
takes a shard-log sequence, including a local PutAsync outside any shard.
Three engines deleted, one evaluator wired in their place:
- CultNetDocumentRegistry: CreateRawSnapshotResponse's reflection loop over
cache.AllEntries and MatchesRequestedSchema gone; cache.AllStoredDocuments
replaces the reflection entirely. v0 lowers into a Selection and walks the
evaluator's own pages so a v0 caller still sees its whole matching set
(D3: v0 has no engine of its own). CreateSelectionResponse/SelectPage/
ToRawRecord/CreateSnapshotRequestV1 added.
- CultNetDatabaseServer: CreateChangeMessage/CreateRawDocumentRecord/Matches/
MatchesSchema/InferSchemaName (122 lines) deleted; CreateSubscription lowers
v0 into a Selection and matches through the evaluator.
- CultNetDatabaseSubscriptionServer: CreateAuthorizedChange/CreateChange/
CreateChangeCore/ResolveWireSchemaId/MatchesRequestedSchema/CreateRawRecord
(92 lines) deleted, replaced by CreateMatchedRecord. _projectRecord and
CreateProjectedSnapshot's collapse are commit 2's, per the map's own
deletion line, and are untouched here.
Discrepancy found and fixed (not in the map): CultNetDocumentBinding can
carry a schema id different from its descriptor's own
(CultNetDocumentBinding.ForDocument's schemaId override), which the old
per-engine matchers checked and a descriptor-only evaluator cannot see by
design (it stays decoupled from CultNetDocumentRegistry). Fixed at the two
call sites and in CultNetDocumentRegistry.SelectPage by resolving the
binding's schema id into the effective selection before evaluation
(CultNetSchemaAliasMatching.WithBindingSchemaAlias,
CultNetDocumentRegistry.ExpandSchemaBindingAliases), not in the evaluator.
Found by DatabaseSubscription_FiltersLiveChangesByWireSchemaBinding timing
out; both call sites now pass.
Second discrepancy, reported not fixed: CultNetDatabase.cs:1707 has a fourth,
undocumented copy of schema-matching logic (MatchesRequestedSchema, private,
serving CreateShardSnapshotResponse), which the map's section 1 audit did not
name among "three separate selector engines". Left untouched this commit:
it is out of the map's explicit deletes list and touching it risks shard
snapshot behavior with no budgeted verification.
Verification: dotnet test tests/GameCult.Networking.Tests - 162 passed (150
existing + 12 new), 0 failed; the seven map-named tests all still pass, two
rewritten for the new signatures (CreateChangeMessage now takes
CultNetSelection; CreateChange/CreateAuthorizedChange collapsed into
CreateMatchedRecord, returning CultNetRawDocumentRecord? directly).
tests/GameCult.Mesh.Tests - 252 passed, 1 pre-existing skip, 0 failed (no
Mesh source touched this commit). tests/GameCult.Caching.Tests - unaffected
(188, unchanged from commit 0).
Negative greps: GetProperty|GetField|GetCustomAttribute in
GameCult.Networking - only against the generic `object change` dispatch
(CultNetDatabaseChange<T>) and CultRecordHandle<T> construction, pre-existing
and out of this cut's scope, never against a document's declared values.
_projectRecord survives in CultNetDatabaseSubscriptionServer.cs (expected,
commit 2). CultNetSchemaAliasMatching is the only schema matcher outside the
one reported discrepancy.
scripts/mutate-dotnet.ps1: the caching mutation runner generalized into a
shared dotnet/NUnit harness (Target/TestProject/Entries/ControlFilter),
replacing scripts/mutate-cultnet-selection-caching.ps1 with an entries file
of the same six mutations (re-verified: all six still KILL). It is now
generic to any NUnit project in this repo, not only this campaign's two
targets - a candidate to sit beside tools/eureka-mutations.ps1 as the
dotnet-side counterpart. Nine new networking entries (S3, S5 x2, S7 x2,
S16 x2, S18) all KILLED. Not yet mutation-run: S1, S2, S6, S23 (pinned by
tests, not by a committed mutant); S9-S15 (subscription reconcile lowering,
v0 no-engine-of-its-own); S17, S20 (pinned by tests only). S19/S21/S22 are
cache-layer, covered in commit 0.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 02:31:42 +02:00
|
|
|
var selection = new CultNetSelection
|
|
|
|
|
{
|
|
|
|
|
Schemas = request.SchemaIds,
|
|
|
|
|
Keys = request.RecordKeys,
|
|
|
|
|
Projection = CultNetSelectionProjections.Document
|
|
|
|
|
};
|
CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
|
|
|
return CreateSubscription(selection, subscriptionId, peer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private IDisposable CreateSubscription(
|
|
|
|
|
CultNetSelection selection,
|
|
|
|
|
string subscriptionId,
|
|
|
|
|
CultNetServerPeer peer)
|
|
|
|
|
{
|
2026-09-29 08:28:24 +02:00
|
|
|
// Keys this subscriber only ever received as a refusal: it was never given them as records.
|
|
|
|
|
var refused = new HashSet<string>(StringComparer.Ordinal);
|
2026-05-19 17:10:24 +01:00
|
|
|
return _database.WatchAllChanges().Subscribe(change =>
|
|
|
|
|
{
|
2026-09-29 08:28:24 +02:00
|
|
|
var outbound = DeliverChange(change, subscriptionId, selection, refused, error => peer.SendCultNet(error));
|
2026-05-19 17:10:24 +01:00
|
|
|
if (outbound != null)
|
2026-06-16 11:54:54 +01:00
|
|
|
peer.SendCultNet(outbound);
|
2026-05-19 17:10:24 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-29 08:28:24 +02:00
|
|
|
// One subscriber's view of one change. A variant is not a record CultNet can carry (Q6): the subscriber is told, by key, and
|
|
|
|
|
// is not later told the variant was removed, because it never held it.
|
|
|
|
|
internal CultNetDatabaseChangeRawMessage? DeliverChange(
|
|
|
|
|
object change,
|
|
|
|
|
string subscriptionId,
|
|
|
|
|
CultNetSelection selection,
|
|
|
|
|
HashSet<string> refused,
|
|
|
|
|
Action<CultNetErrorMessage> sendError)
|
|
|
|
|
{
|
|
|
|
|
CultNetDatabaseChangeRawMessage? outbound;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
outbound = CreateChangeMessage(change, subscriptionId, selection);
|
|
|
|
|
}
|
|
|
|
|
catch (NotSupportedException refusal)
|
|
|
|
|
{
|
|
|
|
|
var refusedKey = change.GetType().GetProperty("Key")?.GetValue(change) is CultRecordKey changed ? changed.Value : string.Empty;
|
|
|
|
|
lock (refused)
|
|
|
|
|
refused.Add(refusedKey);
|
|
|
|
|
sendError(new CultNetErrorMessage { Error = refusal.Message });
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (outbound == null)
|
|
|
|
|
return null;
|
|
|
|
|
var recordKey = outbound.RecordKey ?? outbound.Document?.RecordKey ?? string.Empty;
|
|
|
|
|
bool wasRefused;
|
|
|
|
|
lock (refused)
|
|
|
|
|
wasRefused = refused.Remove(recordKey);
|
|
|
|
|
return wasRefused && outbound.ChangeKind == "removed" ? null : outbound;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 17:10:24 +01:00
|
|
|
internal CultNetDatabaseChangeRawMessage? CreateChangeMessage(
|
|
|
|
|
object change,
|
|
|
|
|
string subscriptionId,
|
CultNet selection cut1, commit 1: the shape, the evaluator, v1 messages
docs/cultnet-selection-cut.md section 2/4/6/7/8: CultNetSelection (schemas,
keys, fields, cites, cited, projection, descending, limit, cursor) with its
door validation (S1), and CultNetSelectionEvaluator: the single-row Matches
fast path (D6) and the full Select (order, hop, cursor, paging). Neither
reflects over a document; both read only through the cache's public surface
(commit 0).
Three v1 messages beside v0 (cultnet.snapshot_request.v1,
cultnet.database_subscribe.v1, cultnet.snapshot_response_raw.v1), their
schema files under contracts/cultnet, registry entries and dispatch arms.
CultNetSchemaAliasMatching goes internal -> public (D4), the one alias
matcher. CultNetDatabase gains LastWriteSequence(schemaId, key), confirmed
at the one append site (AppendMutationLogEntry) that every committed write
takes a shard-log sequence, including a local PutAsync outside any shard.
Three engines deleted, one evaluator wired in their place:
- CultNetDocumentRegistry: CreateRawSnapshotResponse's reflection loop over
cache.AllEntries and MatchesRequestedSchema gone; cache.AllStoredDocuments
replaces the reflection entirely. v0 lowers into a Selection and walks the
evaluator's own pages so a v0 caller still sees its whole matching set
(D3: v0 has no engine of its own). CreateSelectionResponse/SelectPage/
ToRawRecord/CreateSnapshotRequestV1 added.
- CultNetDatabaseServer: CreateChangeMessage/CreateRawDocumentRecord/Matches/
MatchesSchema/InferSchemaName (122 lines) deleted; CreateSubscription lowers
v0 into a Selection and matches through the evaluator.
- CultNetDatabaseSubscriptionServer: CreateAuthorizedChange/CreateChange/
CreateChangeCore/ResolveWireSchemaId/MatchesRequestedSchema/CreateRawRecord
(92 lines) deleted, replaced by CreateMatchedRecord. _projectRecord and
CreateProjectedSnapshot's collapse are commit 2's, per the map's own
deletion line, and are untouched here.
Discrepancy found and fixed (not in the map): CultNetDocumentBinding can
carry a schema id different from its descriptor's own
(CultNetDocumentBinding.ForDocument's schemaId override), which the old
per-engine matchers checked and a descriptor-only evaluator cannot see by
design (it stays decoupled from CultNetDocumentRegistry). Fixed at the two
call sites and in CultNetDocumentRegistry.SelectPage by resolving the
binding's schema id into the effective selection before evaluation
(CultNetSchemaAliasMatching.WithBindingSchemaAlias,
CultNetDocumentRegistry.ExpandSchemaBindingAliases), not in the evaluator.
Found by DatabaseSubscription_FiltersLiveChangesByWireSchemaBinding timing
out; both call sites now pass.
Second discrepancy, reported not fixed: CultNetDatabase.cs:1707 has a fourth,
undocumented copy of schema-matching logic (MatchesRequestedSchema, private,
serving CreateShardSnapshotResponse), which the map's section 1 audit did not
name among "three separate selector engines". Left untouched this commit:
it is out of the map's explicit deletes list and touching it risks shard
snapshot behavior with no budgeted verification.
Verification: dotnet test tests/GameCult.Networking.Tests - 162 passed (150
existing + 12 new), 0 failed; the seven map-named tests all still pass, two
rewritten for the new signatures (CreateChangeMessage now takes
CultNetSelection; CreateChange/CreateAuthorizedChange collapsed into
CreateMatchedRecord, returning CultNetRawDocumentRecord? directly).
tests/GameCult.Mesh.Tests - 252 passed, 1 pre-existing skip, 0 failed (no
Mesh source touched this commit). tests/GameCult.Caching.Tests - unaffected
(188, unchanged from commit 0).
Negative greps: GetProperty|GetField|GetCustomAttribute in
GameCult.Networking - only against the generic `object change` dispatch
(CultNetDatabaseChange<T>) and CultRecordHandle<T> construction, pre-existing
and out of this cut's scope, never against a document's declared values.
_projectRecord survives in CultNetDatabaseSubscriptionServer.cs (expected,
commit 2). CultNetSchemaAliasMatching is the only schema matcher outside the
one reported discrepancy.
scripts/mutate-dotnet.ps1: the caching mutation runner generalized into a
shared dotnet/NUnit harness (Target/TestProject/Entries/ControlFilter),
replacing scripts/mutate-cultnet-selection-caching.ps1 with an entries file
of the same six mutations (re-verified: all six still KILL). It is now
generic to any NUnit project in this repo, not only this campaign's two
targets - a candidate to sit beside tools/eureka-mutations.ps1 as the
dotnet-side counterpart. Nine new networking entries (S3, S5 x2, S7 x2,
S16 x2, S18) all KILLED. Not yet mutation-run: S1, S2, S6, S23 (pinned by
tests, not by a committed mutant); S9-S15 (subscription reconcile lowering,
v0 no-engine-of-its-own); S17, S20 (pinned by tests only). S19/S21/S22 are
cache-layer, covered in commit 0.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 02:31:42 +02:00
|
|
|
CultNetSelection selection)
|
2026-05-19 17:10:24 +01:00
|
|
|
{
|
|
|
|
|
var changeType = change.GetType();
|
|
|
|
|
var key = (CultRecordKey)(changeType.GetProperty("Key")?.GetValue(change) ?? new CultRecordKey(string.Empty));
|
|
|
|
|
var schemaId = (string?)changeType.GetProperty("SchemaId")?.GetValue(change) ?? string.Empty;
|
2026-06-27 21:53:28 +01:00
|
|
|
var documentType = changeType.IsGenericType ? changeType.GetGenericArguments()[0] : null;
|
|
|
|
|
var descriptor = documentType == null ? null : _database.Cache.Registry.GetRequired(documentType);
|
CultNet selection cut1, commit 1: the shape, the evaluator, v1 messages
docs/cultnet-selection-cut.md section 2/4/6/7/8: CultNetSelection (schemas,
keys, fields, cites, cited, projection, descending, limit, cursor) with its
door validation (S1), and CultNetSelectionEvaluator: the single-row Matches
fast path (D6) and the full Select (order, hop, cursor, paging). Neither
reflects over a document; both read only through the cache's public surface
(commit 0).
Three v1 messages beside v0 (cultnet.snapshot_request.v1,
cultnet.database_subscribe.v1, cultnet.snapshot_response_raw.v1), their
schema files under contracts/cultnet, registry entries and dispatch arms.
CultNetSchemaAliasMatching goes internal -> public (D4), the one alias
matcher. CultNetDatabase gains LastWriteSequence(schemaId, key), confirmed
at the one append site (AppendMutationLogEntry) that every committed write
takes a shard-log sequence, including a local PutAsync outside any shard.
Three engines deleted, one evaluator wired in their place:
- CultNetDocumentRegistry: CreateRawSnapshotResponse's reflection loop over
cache.AllEntries and MatchesRequestedSchema gone; cache.AllStoredDocuments
replaces the reflection entirely. v0 lowers into a Selection and walks the
evaluator's own pages so a v0 caller still sees its whole matching set
(D3: v0 has no engine of its own). CreateSelectionResponse/SelectPage/
ToRawRecord/CreateSnapshotRequestV1 added.
- CultNetDatabaseServer: CreateChangeMessage/CreateRawDocumentRecord/Matches/
MatchesSchema/InferSchemaName (122 lines) deleted; CreateSubscription lowers
v0 into a Selection and matches through the evaluator.
- CultNetDatabaseSubscriptionServer: CreateAuthorizedChange/CreateChange/
CreateChangeCore/ResolveWireSchemaId/MatchesRequestedSchema/CreateRawRecord
(92 lines) deleted, replaced by CreateMatchedRecord. _projectRecord and
CreateProjectedSnapshot's collapse are commit 2's, per the map's own
deletion line, and are untouched here.
Discrepancy found and fixed (not in the map): CultNetDocumentBinding can
carry a schema id different from its descriptor's own
(CultNetDocumentBinding.ForDocument's schemaId override), which the old
per-engine matchers checked and a descriptor-only evaluator cannot see by
design (it stays decoupled from CultNetDocumentRegistry). Fixed at the two
call sites and in CultNetDocumentRegistry.SelectPage by resolving the
binding's schema id into the effective selection before evaluation
(CultNetSchemaAliasMatching.WithBindingSchemaAlias,
CultNetDocumentRegistry.ExpandSchemaBindingAliases), not in the evaluator.
Found by DatabaseSubscription_FiltersLiveChangesByWireSchemaBinding timing
out; both call sites now pass.
Second discrepancy, reported not fixed: CultNetDatabase.cs:1707 has a fourth,
undocumented copy of schema-matching logic (MatchesRequestedSchema, private,
serving CreateShardSnapshotResponse), which the map's section 1 audit did not
name among "three separate selector engines". Left untouched this commit:
it is out of the map's explicit deletes list and touching it risks shard
snapshot behavior with no budgeted verification.
Verification: dotnet test tests/GameCult.Networking.Tests - 162 passed (150
existing + 12 new), 0 failed; the seven map-named tests all still pass, two
rewritten for the new signatures (CreateChangeMessage now takes
CultNetSelection; CreateChange/CreateAuthorizedChange collapsed into
CreateMatchedRecord, returning CultNetRawDocumentRecord? directly).
tests/GameCult.Mesh.Tests - 252 passed, 1 pre-existing skip, 0 failed (no
Mesh source touched this commit). tests/GameCult.Caching.Tests - unaffected
(188, unchanged from commit 0).
Negative greps: GetProperty|GetField|GetCustomAttribute in
GameCult.Networking - only against the generic `object change` dispatch
(CultNetDatabaseChange<T>) and CultRecordHandle<T> construction, pre-existing
and out of this cut's scope, never against a document's declared values.
_projectRecord survives in CultNetDatabaseSubscriptionServer.cs (expected,
commit 2). CultNetSchemaAliasMatching is the only schema matcher outside the
one reported discrepancy.
scripts/mutate-dotnet.ps1: the caching mutation runner generalized into a
shared dotnet/NUnit harness (Target/TestProject/Entries/ControlFilter),
replacing scripts/mutate-cultnet-selection-caching.ps1 with an entries file
of the same six mutations (re-verified: all six still KILL). It is now
generic to any NUnit project in this repo, not only this campaign's two
targets - a candidate to sit beside tools/eureka-mutations.ps1 as the
dotnet-side counterpart. Nine new networking entries (S3, S5 x2, S7 x2,
S16 x2, S18) all KILLED. Not yet mutation-run: S1, S2, S6, S23 (pinned by
tests, not by a committed mutant); S9-S15 (subscription reconcile lowering,
v0 no-engine-of-its-own); S17, S20 (pinned by tests only). S19/S21/S22 are
cache-layer, covered in commit 0.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 02:31:42 +02:00
|
|
|
var kind = (CultNetDatabaseChangeKind)(changeType.GetProperty("Kind")?.GetValue(change) ?? CultNetDatabaseChangeKind.Updated);
|
|
|
|
|
var document = changeType.GetProperty("Document")?.GetValue(change);
|
|
|
|
|
|
|
|
|
|
if (descriptor == null)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
|
|
|
// R-M: the one binding-alias reconciler (CultNetDocumentRegistry.ExpandSchemaBindingAliases).
|
|
|
|
|
var effectiveSelection = _database.Documents.ExpandSchemaBindingAliases(selection);
|
CultNet selection cut1, commit 1: the shape, the evaluator, v1 messages
docs/cultnet-selection-cut.md section 2/4/6/7/8: CultNetSelection (schemas,
keys, fields, cites, cited, projection, descending, limit, cursor) with its
door validation (S1), and CultNetSelectionEvaluator: the single-row Matches
fast path (D6) and the full Select (order, hop, cursor, paging). Neither
reflects over a document; both read only through the cache's public surface
(commit 0).
Three v1 messages beside v0 (cultnet.snapshot_request.v1,
cultnet.database_subscribe.v1, cultnet.snapshot_response_raw.v1), their
schema files under contracts/cultnet, registry entries and dispatch arms.
CultNetSchemaAliasMatching goes internal -> public (D4), the one alias
matcher. CultNetDatabase gains LastWriteSequence(schemaId, key), confirmed
at the one append site (AppendMutationLogEntry) that every committed write
takes a shard-log sequence, including a local PutAsync outside any shard.
Three engines deleted, one evaluator wired in their place:
- CultNetDocumentRegistry: CreateRawSnapshotResponse's reflection loop over
cache.AllEntries and MatchesRequestedSchema gone; cache.AllStoredDocuments
replaces the reflection entirely. v0 lowers into a Selection and walks the
evaluator's own pages so a v0 caller still sees its whole matching set
(D3: v0 has no engine of its own). CreateSelectionResponse/SelectPage/
ToRawRecord/CreateSnapshotRequestV1 added.
- CultNetDatabaseServer: CreateChangeMessage/CreateRawDocumentRecord/Matches/
MatchesSchema/InferSchemaName (122 lines) deleted; CreateSubscription lowers
v0 into a Selection and matches through the evaluator.
- CultNetDatabaseSubscriptionServer: CreateAuthorizedChange/CreateChange/
CreateChangeCore/ResolveWireSchemaId/MatchesRequestedSchema/CreateRawRecord
(92 lines) deleted, replaced by CreateMatchedRecord. _projectRecord and
CreateProjectedSnapshot's collapse are commit 2's, per the map's own
deletion line, and are untouched here.
Discrepancy found and fixed (not in the map): CultNetDocumentBinding can
carry a schema id different from its descriptor's own
(CultNetDocumentBinding.ForDocument's schemaId override), which the old
per-engine matchers checked and a descriptor-only evaluator cannot see by
design (it stays decoupled from CultNetDocumentRegistry). Fixed at the two
call sites and in CultNetDocumentRegistry.SelectPage by resolving the
binding's schema id into the effective selection before evaluation
(CultNetSchemaAliasMatching.WithBindingSchemaAlias,
CultNetDocumentRegistry.ExpandSchemaBindingAliases), not in the evaluator.
Found by DatabaseSubscription_FiltersLiveChangesByWireSchemaBinding timing
out; both call sites now pass.
Second discrepancy, reported not fixed: CultNetDatabase.cs:1707 has a fourth,
undocumented copy of schema-matching logic (MatchesRequestedSchema, private,
serving CreateShardSnapshotResponse), which the map's section 1 audit did not
name among "three separate selector engines". Left untouched this commit:
it is out of the map's explicit deletes list and touching it risks shard
snapshot behavior with no budgeted verification.
Verification: dotnet test tests/GameCult.Networking.Tests - 162 passed (150
existing + 12 new), 0 failed; the seven map-named tests all still pass, two
rewritten for the new signatures (CreateChangeMessage now takes
CultNetSelection; CreateChange/CreateAuthorizedChange collapsed into
CreateMatchedRecord, returning CultNetRawDocumentRecord? directly).
tests/GameCult.Mesh.Tests - 252 passed, 1 pre-existing skip, 0 failed (no
Mesh source touched this commit). tests/GameCult.Caching.Tests - unaffected
(188, unchanged from commit 0).
Negative greps: GetProperty|GetField|GetCustomAttribute in
GameCult.Networking - only against the generic `object change` dispatch
(CultNetDatabaseChange<T>) and CultRecordHandle<T> construction, pre-existing
and out of this cut's scope, never against a document's declared values.
_projectRecord survives in CultNetDatabaseSubscriptionServer.cs (expected,
commit 2). CultNetSchemaAliasMatching is the only schema matcher outside the
one reported discrepancy.
scripts/mutate-dotnet.ps1: the caching mutation runner generalized into a
shared dotnet/NUnit harness (Target/TestProject/Entries/ControlFilter),
replacing scripts/mutate-cultnet-selection-caching.ps1 with an entries file
of the same six mutations (re-verified: all six still KILL). It is now
generic to any NUnit project in this repo, not only this campaign's two
targets - a candidate to sit beside tools/eureka-mutations.ps1 as the
dotnet-side counterpart. Nine new networking entries (S3, S5 x2, S7 x2,
S16 x2, S18) all KILLED. Not yet mutation-run: S1, S2, S6, S23 (pinned by
tests, not by a committed mutant); S9-S15 (subscription reconcile lowering,
v0 no-engine-of-its-own); S17, S20 (pinned by tests only). S19/S21/S22 are
cache-layer, covered in commit 0.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 02:31:42 +02:00
|
|
|
if (!CultNetSelectionEvaluator.Matches(descriptor, key, document, effectiveSelection))
|
2026-05-19 17:10:24 +01:00
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (kind == CultNetDatabaseChangeKind.Removed || document == null)
|
|
|
|
|
{
|
|
|
|
|
return new CultNetDatabaseChangeRawMessage
|
|
|
|
|
{
|
|
|
|
|
MessageId = Guid.NewGuid().ToString("N"),
|
|
|
|
|
SubscriptionId = subscriptionId,
|
|
|
|
|
ChangeKind = "removed",
|
|
|
|
|
RecordKey = key.Value,
|
|
|
|
|
SchemaId = schemaId
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-29 06:58:51 +02:00
|
|
|
CultNetDocumentRegistry.RefuseVariantChange(_database.Cache, key);
|
2026-05-19 17:10:24 +01:00
|
|
|
return new CultNetDatabaseChangeRawMessage
|
|
|
|
|
{
|
|
|
|
|
MessageId = Guid.NewGuid().ToString("N"),
|
|
|
|
|
SubscriptionId = subscriptionId,
|
2026-09-29 08:28:24 +02:00
|
|
|
ChangeKind = kind == CultNetDatabaseChangeKind.Added ? "added" : "updated",
|
CultNet selection cut1, commit 1: the shape, the evaluator, v1 messages
docs/cultnet-selection-cut.md section 2/4/6/7/8: CultNetSelection (schemas,
keys, fields, cites, cited, projection, descending, limit, cursor) with its
door validation (S1), and CultNetSelectionEvaluator: the single-row Matches
fast path (D6) and the full Select (order, hop, cursor, paging). Neither
reflects over a document; both read only through the cache's public surface
(commit 0).
Three v1 messages beside v0 (cultnet.snapshot_request.v1,
cultnet.database_subscribe.v1, cultnet.snapshot_response_raw.v1), their
schema files under contracts/cultnet, registry entries and dispatch arms.
CultNetSchemaAliasMatching goes internal -> public (D4), the one alias
matcher. CultNetDatabase gains LastWriteSequence(schemaId, key), confirmed
at the one append site (AppendMutationLogEntry) that every committed write
takes a shard-log sequence, including a local PutAsync outside any shard.
Three engines deleted, one evaluator wired in their place:
- CultNetDocumentRegistry: CreateRawSnapshotResponse's reflection loop over
cache.AllEntries and MatchesRequestedSchema gone; cache.AllStoredDocuments
replaces the reflection entirely. v0 lowers into a Selection and walks the
evaluator's own pages so a v0 caller still sees its whole matching set
(D3: v0 has no engine of its own). CreateSelectionResponse/SelectPage/
ToRawRecord/CreateSnapshotRequestV1 added.
- CultNetDatabaseServer: CreateChangeMessage/CreateRawDocumentRecord/Matches/
MatchesSchema/InferSchemaName (122 lines) deleted; CreateSubscription lowers
v0 into a Selection and matches through the evaluator.
- CultNetDatabaseSubscriptionServer: CreateAuthorizedChange/CreateChange/
CreateChangeCore/ResolveWireSchemaId/MatchesRequestedSchema/CreateRawRecord
(92 lines) deleted, replaced by CreateMatchedRecord. _projectRecord and
CreateProjectedSnapshot's collapse are commit 2's, per the map's own
deletion line, and are untouched here.
Discrepancy found and fixed (not in the map): CultNetDocumentBinding can
carry a schema id different from its descriptor's own
(CultNetDocumentBinding.ForDocument's schemaId override), which the old
per-engine matchers checked and a descriptor-only evaluator cannot see by
design (it stays decoupled from CultNetDocumentRegistry). Fixed at the two
call sites and in CultNetDocumentRegistry.SelectPage by resolving the
binding's schema id into the effective selection before evaluation
(CultNetSchemaAliasMatching.WithBindingSchemaAlias,
CultNetDocumentRegistry.ExpandSchemaBindingAliases), not in the evaluator.
Found by DatabaseSubscription_FiltersLiveChangesByWireSchemaBinding timing
out; both call sites now pass.
Second discrepancy, reported not fixed: CultNetDatabase.cs:1707 has a fourth,
undocumented copy of schema-matching logic (MatchesRequestedSchema, private,
serving CreateShardSnapshotResponse), which the map's section 1 audit did not
name among "three separate selector engines". Left untouched this commit:
it is out of the map's explicit deletes list and touching it risks shard
snapshot behavior with no budgeted verification.
Verification: dotnet test tests/GameCult.Networking.Tests - 162 passed (150
existing + 12 new), 0 failed; the seven map-named tests all still pass, two
rewritten for the new signatures (CreateChangeMessage now takes
CultNetSelection; CreateChange/CreateAuthorizedChange collapsed into
CreateMatchedRecord, returning CultNetRawDocumentRecord? directly).
tests/GameCult.Mesh.Tests - 252 passed, 1 pre-existing skip, 0 failed (no
Mesh source touched this commit). tests/GameCult.Caching.Tests - unaffected
(188, unchanged from commit 0).
Negative greps: GetProperty|GetField|GetCustomAttribute in
GameCult.Networking - only against the generic `object change` dispatch
(CultNetDatabaseChange<T>) and CultRecordHandle<T> construction, pre-existing
and out of this cut's scope, never against a document's declared values.
_projectRecord survives in CultNetDatabaseSubscriptionServer.cs (expected,
commit 2). CultNetSchemaAliasMatching is the only schema matcher outside the
one reported discrepancy.
scripts/mutate-dotnet.ps1: the caching mutation runner generalized into a
shared dotnet/NUnit harness (Target/TestProject/Entries/ControlFilter),
replacing scripts/mutate-cultnet-selection-caching.ps1 with an entries file
of the same six mutations (re-verified: all six still KILL). It is now
generic to any NUnit project in this repo, not only this campaign's two
targets - a candidate to sit beside tools/eureka-mutations.ps1 as the
dotnet-side counterpart. Nine new networking entries (S3, S5 x2, S7 x2,
S16 x2, S18) all KILLED. Not yet mutation-run: S1, S2, S6, S23 (pinned by
tests, not by a committed mutant); S9-S15 (subscription reconcile lowering,
v0 no-engine-of-its-own); S17, S20 (pinned by tests only). S19/S21/S22 are
cache-layer, covered in commit 0.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 02:31:42 +02:00
|
|
|
Document = _database.Documents.ToRawRecord(descriptor, key, document, DateTimeOffset.UtcNow.ToString("O"))
|
2026-05-19 17:10:24 +01:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string SubscriptionKey(NetPeer peer, string subscriptionId)
|
|
|
|
|
{
|
|
|
|
|
return $"{peer.Id}:{subscriptionId}";
|
|
|
|
|
}
|
2026-05-19 17:34:49 +01:00
|
|
|
|
2026-05-19 18:01:11 +01:00
|
|
|
private static string RequestMessageId(string messageId)
|
|
|
|
|
{
|
|
|
|
|
return string.IsNullOrWhiteSpace(messageId)
|
|
|
|
|
? Guid.NewGuid().ToString("N")
|
|
|
|
|
: messageId;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 17:34:49 +01:00
|
|
|
private static CultNetErrorMessage CreateRoutingError(CultNetShardAuthorityException exception)
|
|
|
|
|
{
|
|
|
|
|
return new CultNetErrorMessage
|
|
|
|
|
{
|
|
|
|
|
Error = exception.Message,
|
|
|
|
|
RoutingHint = new CultNetShardRoutingHint
|
|
|
|
|
{
|
2026-05-19 17:40:19 +01:00
|
|
|
Reason = exception.Reason,
|
2026-05-19 17:34:49 +01:00
|
|
|
Shard = CultNetDatabase.ToMessage(exception.Shard)
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-05-19 17:40:19 +01:00
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Attempts to forward a rejected raw put to the shard owner.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public async Task<bool> TryForwardPutAsync(
|
|
|
|
|
CultNetShardAuthorityException exception,
|
|
|
|
|
CultNetDocumentPutRawMessage message)
|
|
|
|
|
{
|
|
|
|
|
if (!_options.ForwardNonPrimaryWrites ||
|
|
|
|
|
_options.WriteForwarder == null ||
|
|
|
|
|
exception.Reason != "not_primary")
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
message.ShardId ??= exception.Shard.ShardId;
|
|
|
|
|
message.ShardEpoch ??= exception.Shard.Epoch;
|
|
|
|
|
await _options.WriteForwarder.ForwardPutAsync(exception.Shard, message).ConfigureAwait(false);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Attempts to forward a rejected raw delete to the shard owner.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public async Task<bool> TryForwardDeleteAsync(
|
|
|
|
|
CultNetShardAuthorityException exception,
|
|
|
|
|
CultNetDocumentDeleteMessage message)
|
|
|
|
|
{
|
|
|
|
|
if (!_options.ForwardNonPrimaryWrites ||
|
|
|
|
|
_options.WriteForwarder == null ||
|
|
|
|
|
exception.Reason != "not_primary")
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
message.ShardId ??= exception.Shard.ShardId;
|
|
|
|
|
message.ShardEpoch ??= exception.Shard.Epoch;
|
|
|
|
|
await _options.WriteForwarder.ForwardDeleteAsync(exception.Shard, message).ConfigureAwait(false);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-05-19 17:03:31 +01:00
|
|
|
}
|
|
|
|
|
}
|