using System;
using System.Collections.Generic;
using System.Threading;
using Google.Protobuf.Collections;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
///
/// Per-session cache of the most recent OnWriteComplete status rows
/// for each (server handle, item handle) pair. Written by the MXAccess
/// event sink as completion callbacks arrive; read by the write command
/// executor so a WriteSecured/WriteSecured2 reply can carry the correlated
/// MXAccess outcome instead of proving command acceptance only.
///
///
/// Same threading posture as : writers and
/// readers run on the worker's STA thread (COM dispatches events on the
/// apartment thread; commands also execute on the STA), so no internal
/// locking is required. A single sync root keeps it nominally thread-safe
/// for tests that drive it from a non-STA thread.
///
public sealed class MxAccessWriteCompletionCache
{
private readonly Dictionary entries = new();
private readonly object syncRoot = new();
/// Records the status rows of a fresh OnWriteComplete callback for the given handle pair.
/// MXAccess server handle.
/// MXAccess item handle.
/// Status rows from the mapped OnWriteComplete event; cloned before storing.
public void Record(
int serverHandle,
int itemHandle,
RepeatedField statuses)
{
if (statuses is null)
{
throw new ArgumentNullException(nameof(statuses));
}
lock (syncRoot)
{
long key = CreateItemKey(serverHandle, itemHandle);
ulong version = entries.TryGetValue(key, out CompletionEntry existing)
? existing.Version + 1
: 1UL;
entries[key] = new CompletionEntry(version, statuses.Clone());
}
}
/// Returns the current completion version for a handle pair, or 0 if none was recorded.
/// MXAccess server handle.
/// MXAccess item handle.
/// The current completion version, or 0 if no completion was recorded.
public ulong CurrentVersion(
int serverHandle,
int itemHandle)
{
lock (syncRoot)
{
return entries.TryGetValue(CreateItemKey(serverHandle, itemHandle), out CompletionEntry existing)
? existing.Version
: 0UL;
}
}
///
/// Polls for a completion newer than until it
/// arrives or the deadline elapses, calling on every
/// poll iteration so the worker's STA can dispatch the inbound MXAccess
/// OnWriteComplete message. Same loop shape as
/// .
///
/// MXAccess server handle.
/// MXAccess item handle.
/// Version snapshot captured before the write COM call.
/// Absolute UTC deadline.
/// Action that pumps any pending Windows messages.
/// The recorded status rows if a completion arrived before the deadline; empty otherwise.
/// How long to sleep between pump cycles. Default 5 ms.
/// if a completion newer than arrived before the deadline; otherwise .
public bool TryWaitForCompletion(
int serverHandle,
int itemHandle,
ulong sinceVersion,
DateTime deadlineUtc,
Action pumpStep,
out RepeatedField statuses,
int pollIntervalMs = 5)
{
if (pumpStep is null)
{
throw new ArgumentNullException(nameof(pumpStep));
}
while (true)
{
pumpStep();
lock (syncRoot)
{
if (entries.TryGetValue(CreateItemKey(serverHandle, itemHandle), out CompletionEntry entry)
&& entry.Version > sinceVersion)
{
statuses = entry.Statuses;
return true;
}
}
if (DateTime.UtcNow >= deadlineUtc)
{
statuses = new RepeatedField();
return false;
}
Thread.Sleep(pollIntervalMs);
}
}
private static long CreateItemKey(
int serverHandle,
int itemHandle)
{
return ((long)serverHandle << 32) | (uint)itemHandle;
}
///
/// Snapshot of the most recent OnWriteComplete status rows for a handle
/// pair. increments by one on every
/// call so the write executor can detect "a new
/// completion arrived since I captured my baseline".
///
///
/// Plain readonly struct (not a record) so this compiles under the
/// worker's net48 target, which lacks IsExternalInit.
///
private readonly struct CompletionEntry
{
public CompletionEntry(
ulong version,
RepeatedField statuses)
{
Version = version;
Statuses = statuses;
}
public ulong Version { get; }
public RepeatedField Statuses { get; }
}
}