AyCode.Core/AyCode.Core/Serializers/ReferenceTracker.cs

139 lines
4.3 KiB
C#

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace AyCode.Core.Serializers;
/// <summary>
/// Shared reference tracking logic for serialization.
/// Tracks object references to enable $id/$ref handling for circular references.
/// </summary>
public sealed class SerializationReferenceTracker
{
private readonly Dictionary<object, int> _scanOccurrences;
private readonly Dictionary<object, string> _writtenRefs;
private readonly HashSet<object> _multiReferenced;
private int _nextId;
public SerializationReferenceTracker(int initialCapacity = 32)
{
_scanOccurrences = new(initialCapacity, ReferenceEqualityComparer.Instance);
_writtenRefs = new(initialCapacity, ReferenceEqualityComparer.Instance);
_multiReferenced = new(initialCapacity, ReferenceEqualityComparer.Instance);
_nextId = 1;
}
/// <summary>
/// Tracks an object during the scanning phase.
/// Returns true if this is the first occurrence (should continue scanning children).
/// Returns false if object was seen before (multi-referenced).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TrackForScanning(object obj)
{
ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists);
if (exists)
{
count++;
_multiReferenced.Add(obj);
return false;
}
count = 1;
return true;
}
/// <summary>
/// Checks if an object should have an $id written and returns the id.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ShouldWriteId(object obj, out string id)
{
if (_multiReferenced.Contains(obj) && !_writtenRefs.ContainsKey(obj))
{
id = _nextId++.ToString();
return true;
}
id = "";
return false;
}
/// <summary>
/// Marks an object as written with its assigned id.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MarkAsWritten(object obj, string id) => _writtenRefs[obj] = id;
/// <summary>
/// Tries to get an existing reference id for an object.
/// If found, a $ref should be written instead of the full object.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetExistingRef(object obj, out string refId)
{
return _writtenRefs.TryGetValue(obj, out refId!);
}
/// <summary>
/// Clears all tracking data for reuse.
/// </summary>
public void Clear()
{
_scanOccurrences.Clear();
_writtenRefs.Clear();
_multiReferenced.Clear();
_nextId = 1;
}
}
/// <summary>
/// Shared reference tracking logic for deserialization.
/// Resolves $id/$ref references during deserialization.
/// </summary>
public sealed class DeserializationReferenceTracker
{
private Dictionary<string, object>? _idToObject;
/// <summary>
/// Registers an object with its $id.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterObject(string id, object obj)
{
_idToObject ??= new Dictionary<string, object>(64, StringComparer.Ordinal);
_idToObject[id] = obj;
}
/// <summary>
/// Tries to get a referenced object by its $id.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetReferencedObject(string id, out object? obj)
{
if (_idToObject != null)
return _idToObject.TryGetValue(id, out obj);
obj = null;
return false;
}
/// <summary>
/// Clears all tracking data for reuse.
/// </summary>
public void Clear()
{
_idToObject?.Clear();
}
}
/// <summary>
/// Reference equality comparer for object identity comparison.
/// Used for reference tracking dictionaries.
/// </summary>
public sealed class ReferenceEqualityComparer : IEqualityComparer<object>
{
public static readonly ReferenceEqualityComparer Instance = new();
private ReferenceEqualityComparer() { }
public new bool Equals(object? x, object? y) => ReferenceEquals(x, y);
public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj);
}