using System.Collections.Concurrent; namespace AcDream.App.Rendering.Residency; /// /// Worker-safe, coalescing observation seam. The single update-thread owner /// drains immutable observations into . /// internal sealed class ResidencyObservationJournal { private readonly ConcurrentDictionary< (AssetReference Asset, ResidencyObservationKind Kind), ResidencyObservation> _pending = new(); private readonly int _maximumEntries; public ResidencyObservationJournal(int maximumEntries) { ArgumentOutOfRangeException.ThrowIfLessThan(maximumEntries, 1); _maximumEntries = maximumEntries; } public int Count => _pending.Count; public bool TryPublish(in ResidencyObservation observation) { if (!observation.Asset.IsValid) throw new ArgumentException( "Residency observations require a valid asset reference.", nameof(observation)); observation.Charges.Validate(); var key = (observation.Asset, observation.Kind); while (true) { if (_pending.TryGetValue(key, out ResidencyObservation current)) { // Network/decode callbacks may arrive concurrently. Preserve // the newest immutable fact instead of letting a late older // callback overwrite it. if (observation.Frame < current.Frame) return true; if (_pending.TryUpdate(key, observation, current)) return true; continue; } if (_pending.Count >= _maximumEntries) return false; if (_pending.TryAdd(key, observation)) return true; } } public int DrainTo(List destination) { ArgumentNullException.ThrowIfNull(destination); int start = destination.Count; foreach (KeyValuePair< (AssetReference Asset, ResidencyObservationKind Kind), ResidencyObservation> pair in _pending) { // Remove only the exact value enumerated. If a worker replaced it // after enumeration, the newer value remains for the next drain. if (((ICollection>)_pending).Remove(pair)) { destination.Add(pair.Value); } } return destination.Count - start; } }