using System.Collections.Concurrent;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.Mqtt;
///
/// Thread-safe last-observed-value store backing for MQTT/Sparkplug B.
/// MQTT is a subscribe-first protocol — the driver holds one live broker connection and values
/// arrive pushed, not polled. But the OPC UA server still issues polled IReadable.ReadAsync
/// batches, so this cache is the bridge: the subscription path (message handler) calls
/// with the newest observed value per reference, and the read path serves
/// the most recent one via .
///
///
/// Keyed by RawPath — the v3 driver-reference identity (see
/// EquipmentTagRefResolver<TDef> and ,
/// which is the RawPath) — never a topic/JSON-path-derived key. The parameter is
/// therefore named rawPath throughout rather than "topic" or "key" to keep that identity
/// fact visible at every call site.
///
public sealed class LastValueCache
{
// OPC UA BadWaitingForInitialData (0x80320000) — the repo-wide convention for "no value has
// been observed yet" (mirrored by CalculationDriver before its first evaluation,
// VirtualTagEngine for a freshly-materialised node, and OtOpcUaNodeManager /
// AddressSpaceApplier for a just-deployed variable). MQTT is subscribe-first, so an unseen
// RawPath is in exactly that state until its first publish arrives. Deliberately NOT
// GoodNoData — that code is reserved (see NullHistorianDataSource, OtOpcUaNodeManager
// HistoryRead paths) for "the historian window held no samples", a different question from
// "has this live reference ever been observed".
private const uint BadWaitingForInitialData = 0x80320000u;
private readonly ConcurrentDictionary _values = new();
///
/// Record the newest observed value for . Called from the MQTT
/// subscription/message-handling path. A null or empty
/// is a no-op — never throws.
///
/// The RawPath identifying the tag.
/// The newest observed value/quality/timestamps for that tag.
public void Update(string rawPath, DataValueSnapshot snapshot)
{
if (string.IsNullOrEmpty(rawPath)) return;
_values[rawPath] = snapshot;
}
///
/// Read the last observed value for . Never throws: a
/// null/empty or a RawPath never observed both return a
/// snapshot rather than an exception, so a batch read
/// covering many references degrades per-reference instead of failing the whole call.
///
/// The RawPath identifying the tag.
/// The last observed snapshot, or a BadWaitingForInitialData snapshot if none has been observed.
public DataValueSnapshot Read(string rawPath)
{
if (!string.IsNullOrEmpty(rawPath) && _values.TryGetValue(rawPath, out var snapshot))
return snapshot;
return new DataValueSnapshot(null, BadWaitingForInitialData, null, DateTime.UtcNow);
}
}