using System.Collections.Concurrent; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Server.History; /// /// Default implementation. /// public sealed class HistoryRouter : IHistoryRouter { private readonly ConcurrentDictionary _registry = new(StringComparer.OrdinalIgnoreCase); private bool _disposed; /// public void Register(string fullReferencePrefix, IHistorianDataSource source) { ObjectDisposedException.ThrowIf(_disposed, this); ArgumentNullException.ThrowIfNull(fullReferencePrefix); ArgumentNullException.ThrowIfNull(source); if (!_registry.TryAdd(fullReferencePrefix, source)) { throw new InvalidOperationException( $"A historian data source is already registered for prefix '{fullReferencePrefix}'."); } } /// public IHistorianDataSource? Resolve(string fullReference) { ObjectDisposedException.ThrowIf(_disposed, this); ArgumentNullException.ThrowIfNull(fullReference); // Longest-prefix match. Sources are typically a handful per server, so a linear // scan is fine and avoids building a trie for a low-cardinality registry. IHistorianDataSource? best = null; var bestPrefixLength = -1; foreach (var (prefix, source) in _registry) { if (fullReference.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) && prefix.Length > bestPrefixLength) { best = source; bestPrefixLength = prefix.Length; } } return best; } /// /// Disposes every registered source and prevents further registrations or /// resolutions. Sources may not all be disposable — null-safe disposal pattern. /// public void Dispose() { if (_disposed) return; _disposed = true; foreach (var source in _registry.Values) { try { source.Dispose(); } catch { /* best-effort — server shutdown should not throw on a misbehaving source */ } } _registry.Clear(); } }