Merge origin/main with local pending work and update AGENTS.md references

- Resolve 14 conflicts from popping local stash on top of origin's
  eed1e88 + 8d3352f doc-comment additions (11 mechanical, plus
  version.rs, DashboardAuthenticatorTests.cs, DashboardGalaxyProjector.cs)
- Fix 4 test files that used AGENTS.md as the repo-root sentinel
  (now use CLAUDE.md, since AGENTS.md was removed in 4731ab5)
- Redirect 10 doc citations from AGENTS.md to the matching gateway.md
  sections (Value Model, Status Model, Security, STA Worker Thread
  Model, gRPC Layer rule, cancellation rule)

Verified: solution build clean, x86 worker build clean, 266/266
gateway tests passing, 121/121 worker tests passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-04-30 14:13:33 -04:00
parent 8d3352f2c6
commit ddad573b75
101 changed files with 6053 additions and 621 deletions
+124 -1
View File
@@ -14,6 +14,7 @@ public sealed class GatewaySession
private DateTimeOffset? _leaseExpiresAt;
private bool _closeStarted;
private int _activeEventSubscriberCount;
private readonly Dictionary<(int ServerHandle, int ItemHandle), SessionItemRegistration> _items = [];
/// <summary>
/// Initializes a gateway session with session metadata and timeout configuration.
@@ -41,6 +42,35 @@ public sealed class GatewaySession
TimeSpan startupTimeout,
TimeSpan shutdownTimeout,
DateTimeOffset openedAt)
: this(
sessionId,
backendName,
pipeName,
nonce,
clientIdentity,
clientSessionName,
clientCorrelationId,
commandTimeout,
startupTimeout,
shutdownTimeout,
TimeSpan.FromMinutes(30),
openedAt)
{
}
public GatewaySession(
string sessionId,
string backendName,
string pipeName,
string nonce,
string? clientIdentity,
string? clientSessionName,
string? clientCorrelationId,
TimeSpan commandTimeout,
TimeSpan startupTimeout,
TimeSpan shutdownTimeout,
TimeSpan leaseDuration,
DateTimeOffset openedAt)
{
if (string.IsNullOrWhiteSpace(sessionId))
{
@@ -72,8 +102,10 @@ public sealed class GatewaySession
CommandTimeout = commandTimeout;
StartupTimeout = startupTimeout;
ShutdownTimeout = shutdownTimeout;
LeaseDuration = leaseDuration;
OpenedAt = openedAt;
_lastClientActivityAt = openedAt;
_leaseExpiresAt = openedAt + leaseDuration;
}
/// <summary>
@@ -126,6 +158,8 @@ public sealed class GatewaySession
/// </summary>
public TimeSpan ShutdownTimeout { get; }
public TimeSpan LeaseDuration { get; }
/// <summary>
/// Gets the timestamp when the session opened.
/// </summary>
@@ -282,6 +316,7 @@ public sealed class GatewaySession
lock (_syncRoot)
{
_lastClientActivityAt = activityAt;
_leaseExpiresAt = activityAt + LeaseDuration;
}
}
@@ -305,7 +340,9 @@ public sealed class GatewaySession
{
lock (_syncRoot)
{
return _leaseExpiresAt is not null && _leaseExpiresAt <= now;
return _activeEventSubscriberCount == 0
&& _leaseExpiresAt is not null
&& _leaseExpiresAt <= now;
}
}
@@ -351,6 +388,58 @@ public sealed class GatewaySession
return await workerClient.InvokeAsync(command, CommandTimeout, cancellationToken).ConfigureAwait(false);
}
public bool TryGetItemRegistration(
int serverHandle,
int itemHandle,
out SessionItemRegistration registration)
{
lock (_syncRoot)
{
return _items.TryGetValue((serverHandle, itemHandle), out registration!);
}
}
public void TrackCommandReply(
MxCommand command,
MxCommandReply reply)
{
if (reply.ProtocolStatus?.Code is not ProtocolStatusCode.Ok)
{
return;
}
lock (_syncRoot)
{
switch (command.Kind)
{
case MxCommandKind.AddItem when reply.AddItem is not null:
TrackItem(command.AddItem.ServerHandle, reply.AddItem.ItemHandle, command.AddItem.ItemDefinition);
break;
case MxCommandKind.AddItem2 when reply.AddItem2 is not null:
TrackItem(command.AddItem2.ServerHandle, reply.AddItem2.ItemHandle, command.AddItem2.ItemDefinition);
break;
case MxCommandKind.AddBufferedItem when reply.AddBufferedItem is not null:
TrackItem(command.AddBufferedItem.ServerHandle, reply.AddBufferedItem.ItemHandle, command.AddBufferedItem.ItemDefinition);
break;
case MxCommandKind.AddItemBulk when reply.AddItemBulk is not null:
TrackBulkItems(reply.AddItemBulk);
break;
case MxCommandKind.SubscribeBulk when reply.SubscribeBulk is not null:
TrackBulkItems(reply.SubscribeBulk);
break;
case MxCommandKind.RemoveItem:
_items.Remove((command.RemoveItem.ServerHandle, command.RemoveItem.ItemHandle));
break;
case MxCommandKind.RemoveItemBulk:
RemoveItems(command.RemoveItemBulk.ServerHandle, command.RemoveItemBulk.ItemHandles);
break;
case MxCommandKind.UnsubscribeBulk:
RemoveItems(command.UnsubscribeBulk.ServerHandle, command.UnsubscribeBulk.ItemHandles);
break;
}
}
}
/// <summary>
/// Executes a bulk add-item command for the specified server and tag addresses.
/// </summary>
@@ -641,6 +730,40 @@ public sealed class GatewaySession
}
}
private void TrackItem(
int serverHandle,
int itemHandle,
string tagAddress)
{
if (itemHandle == 0 || string.IsNullOrWhiteSpace(tagAddress))
{
return;
}
_items[(serverHandle, itemHandle)] = new SessionItemRegistration(serverHandle, itemHandle, tagAddress);
}
private void TrackBulkItems(BulkSubscribeReply reply)
{
foreach (SubscribeResult result in reply.Results)
{
if (result.WasSuccessful)
{
TrackItem(result.ServerHandle, result.ItemHandle, result.TagAddress);
}
}
}
private void RemoveItems(
int serverHandle,
IEnumerable<int> itemHandles)
{
foreach (int itemHandle in itemHandles)
{
_items.Remove((serverHandle, itemHandle));
}
}
private void DetachEventSubscriber()
{
lock (_syncRoot)