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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user