feat(lmxproxy): phase 2 — host core (MxAccessClient, SessionManager, SubscriptionManager)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-03-21 23:58:17 -04:00
parent 0d63fb1105
commit 64c92c63e5
15 changed files with 1834 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Serilog;
using ZB.MOM.WW.LmxProxy.Host.Domain;
namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
{
public sealed partial class MxAccessClient
{
/// <summary>
/// Subscribes to value changes for the specified addresses.
/// Stores subscription state for reconnect replay.
/// </summary>
public async Task<IAsyncDisposable> SubscribeAsync(
IEnumerable<string> addresses,
Action<string, Vtq> callback,
CancellationToken ct = default)
{
if (!IsConnected)
throw new InvalidOperationException("Not connected to MxAccess");
var addressList = addresses.ToList();
await _staThread.DispatchAsync(() =>
{
foreach (var address in addressList)
{
SubscribeInternal(address);
// Store for reconnect replay
lock (_lock)
{
_storedSubscriptions[address] = callback;
}
}
});
Log.Information("Subscribed to {Count} tags", addressList.Count);
return new SubscriptionHandle(this, addressList, callback);
}
/// <summary>
/// Unsubscribes specific addresses.
/// </summary>
internal async Task UnsubscribeAsync(IEnumerable<string> addresses)
{
var addressList = addresses.ToList();
await _staThread.DispatchAsync(() =>
{
foreach (var address in addressList)
{
UnsubscribeInternal(address);
lock (_lock)
{
_storedSubscriptions.Remove(address);
}
}
});
Log.Information("Unsubscribed from {Count} tags", addressList.Count);
}
/// <summary>
/// Recreates all stored subscriptions after a reconnect.
/// Does not re-store them (they're already stored).
/// </summary>
private async Task RecreateStoredSubscriptionsAsync()
{
Dictionary<string, Action<string, Vtq>> subscriptions;
lock (_lock)
{
if (_storedSubscriptions.Count == 0) return;
subscriptions = new Dictionary<string, Action<string, Vtq>>(_storedSubscriptions);
}
Log.Information("Recreating {Count} stored subscriptions after reconnect", subscriptions.Count);
await _staThread.DispatchAsync(() =>
{
foreach (var kvp in subscriptions)
{
try
{
SubscribeInternal(kvp.Key);
}
catch (Exception ex)
{
Log.Warning(ex, "Failed to recreate subscription for {Address}", kvp.Key);
}
}
});
}
// ── Internal COM calls (execute on STA thread) ──────────
/// <summary>
/// Registers a tag subscription with MxAccess COM API (AddItem + AdviseSupervisory).
/// Must be called on the STA thread.
/// </summary>
private void SubscribeInternal(string address)
{
// The exact MxAccess COM API call is:
// var itemHandle = _lmxProxy.AddItem(_connectionHandle, address);
// _lmxProxy.AdviseSupervisory(_connectionHandle, itemHandle);
//
// Consult src-reference/Implementation/MxAccessClient.Subscription.cs
throw new NotImplementedException(
"SubscribeInternal must be implemented using ArchestrA.MXAccess COM API. " +
"See src-reference/Implementation/MxAccessClient.Subscription.cs for the exact pattern.");
}
/// <summary>
/// Unregisters a tag subscription from MxAccess COM API (UnAdvise + RemoveItem).
/// Must be called on the STA thread.
/// </summary>
private void UnsubscribeInternal(string address)
{
// The exact MxAccess COM API call is:
// _lmxProxy.UnAdvise(_connectionHandle, itemHandle);
// _lmxProxy.RemoveItem(_connectionHandle, itemHandle);
throw new NotImplementedException(
"UnsubscribeInternal must be implemented using ArchestrA.MXAccess COM API.");
}
/// <summary>
/// Disposable subscription handle that unsubscribes on disposal.
/// </summary>
private sealed class SubscriptionHandle : IAsyncDisposable
{
private readonly MxAccessClient _client;
private readonly List<string> _addresses;
private readonly Action<string, Vtq> _callback;
private bool _disposed;
public SubscriptionHandle(MxAccessClient client, List<string> addresses, Action<string, Vtq> callback)
{
_client = client;
_addresses = addresses;
_callback = callback;
}
public async ValueTask DisposeAsync()
{
if (_disposed) return;
_disposed = true;
await _client.UnsubscribeAsync(_addresses);
}
}
}
}