using System.Collections.Concurrent;
namespace ZB.MOM.WW.OtOpcUa.Core.Authorization;
///
/// Process-singleton cache of instances keyed on
/// (ClusterId, GenerationId). Hot-path evaluation reads
/// without awaiting DB access; the cache is populated
/// out-of-band on publish + on first reference via
/// .
///
///
/// Per decision #148 and Phase 6.2 Stream B.4 the cache is generation-sealed: once a
/// trie is installed for (ClusterId, GenerationId) the entry is immutable. When a
/// new generation publishes, the caller calls with the new trie
/// + the cache atomically updates its "current generation" pointer for that cluster.
/// Older generations are retained so an in-flight request evaluating the prior generation
/// still succeeds — GC via .
///
public sealed class PermissionTrieCache
{
private readonly ConcurrentDictionary _byCluster =
new(StringComparer.OrdinalIgnoreCase);
/// Install a trie for a cluster + make it the current generation.
public void Install(PermissionTrie trie)
{
ArgumentNullException.ThrowIfNull(trie);
_byCluster.AddOrUpdate(trie.ClusterId,
_ => ClusterEntry.FromSingle(trie),
(_, existing) => existing.WithAdditional(trie));
}
/// Get the current-generation trie for a cluster; null when nothing installed.
public PermissionTrie? GetTrie(string clusterId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(clusterId);
return _byCluster.TryGetValue(clusterId, out var entry) ? entry.Current : null;
}
/// Get a specific (cluster, generation) trie; null if that pair isn't cached.
public PermissionTrie? GetTrie(string clusterId, long generationId)
{
if (!_byCluster.TryGetValue(clusterId, out var entry)) return null;
return entry.Tries.TryGetValue(generationId, out var trie) ? trie : null;
}
/// The generation id the shortcut currently serves for a cluster.
public long? CurrentGenerationId(string clusterId)
=> _byCluster.TryGetValue(clusterId, out var entry) ? entry.Current.GenerationId : null;
/// Drop every cached trie for one cluster.
public void Invalidate(string clusterId) => _byCluster.TryRemove(clusterId, out _);
///
/// Retain only the most-recent generations for a cluster.
/// No-op when there's nothing to drop.
///
public void Prune(string clusterId, int keepLatest = 3)
{
if (keepLatest < 1) throw new ArgumentOutOfRangeException(nameof(keepLatest), keepLatest, "keepLatest must be >= 1");
if (!_byCluster.TryGetValue(clusterId, out var entry)) return;
if (entry.Tries.Count <= keepLatest) return;
var keep = entry.Tries
.OrderByDescending(kvp => kvp.Key)
.Take(keepLatest)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
_byCluster[clusterId] = new ClusterEntry(entry.Current, keep);
}
/// Diagnostics counter: number of cached (cluster, generation) tries.
public int CachedTrieCount => _byCluster.Values.Sum(e => e.Tries.Count);
private sealed record ClusterEntry(PermissionTrie Current, IReadOnlyDictionary Tries)
{
public static ClusterEntry FromSingle(PermissionTrie trie) =>
new(trie, new Dictionary { [trie.GenerationId] = trie });
public ClusterEntry WithAdditional(PermissionTrie trie)
{
var next = new Dictionary(Tries) { [trie.GenerationId] = trie };
// The highest generation wins as "current" — handles out-of-order installs.
var current = trie.GenerationId >= Current.GenerationId ? trie : Current;
return new ClusterEntry(current, next);
}
}
}