Files
git.stella-ops.org/src/Concelier/__Libraries/StellaOps.Concelier.Merge/Services/AliasGraphResolver.cs
2026-02-01 21:37:40 +02:00

141 lines
5.2 KiB
C#

using StellaOps.Concelier.Storage.Aliases;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Concelier.Merge.Services;
public sealed class AliasGraphResolver
{
private readonly IAliasStore _aliasStore;
public AliasGraphResolver(IAliasStore aliasStore)
{
_aliasStore = aliasStore ?? throw new ArgumentNullException(nameof(aliasStore));
}
public async Task<AliasIdentityResult> ResolveAsync(string advisoryKey, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(advisoryKey);
var aliases = await _aliasStore.GetByAdvisoryAsync(advisoryKey, cancellationToken).ConfigureAwait(false);
var collisions = new List<AliasCollision>();
foreach (var alias in aliases)
{
var candidates = await _aliasStore.GetByAliasAsync(alias.Scheme, alias.Value, cancellationToken).ConfigureAwait(false);
var advisoryKeys = candidates
.Select(static candidate => candidate.AdvisoryKey)
.Where(static key => !string.IsNullOrWhiteSpace(key))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
if (advisoryKeys.Length <= 1)
{
continue;
}
collisions.Add(new AliasCollision(alias.Scheme, alias.Value, advisoryKeys));
}
var unique = new Dictionary<string, AliasCollision>(StringComparer.Ordinal);
foreach (var collision in collisions)
{
var key = $"{collision.Scheme}\u0001{collision.Value}";
if (!unique.ContainsKey(key))
{
unique[key] = collision;
}
}
var distinctCollisions = unique.Values.ToArray();
return new AliasIdentityResult(advisoryKey, aliases, distinctCollisions);
}
public async Task<AliasComponent> BuildComponentAsync(string advisoryKey, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrEmpty(advisoryKey);
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var queue = new Queue<string>();
var collisionMap = new Dictionary<string, AliasCollision>(StringComparer.Ordinal);
var aliasCache = new Dictionary<string, IReadOnlyList<AliasRecord>>(StringComparer.OrdinalIgnoreCase);
queue.Enqueue(advisoryKey);
while (queue.Count > 0)
{
cancellationToken.ThrowIfCancellationRequested();
var current = queue.Dequeue();
if (!visited.Add(current))
{
continue;
}
var aliases = await GetAliasesAsync(current, cancellationToken, aliasCache).ConfigureAwait(false);
aliasCache[current] = aliases;
foreach (var alias in aliases)
{
var aliasRecords = await GetAdvisoriesForAliasAsync(alias.Scheme, alias.Value, cancellationToken).ConfigureAwait(false);
var advisoryKeys = aliasRecords
.Select(static record => record.AdvisoryKey)
.Where(static key => !string.IsNullOrWhiteSpace(key))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
if (advisoryKeys.Length <= 1)
{
continue;
}
foreach (var candidate in advisoryKeys)
{
if (!visited.Contains(candidate))
{
queue.Enqueue(candidate);
}
}
var collision = new AliasCollision(alias.Scheme, alias.Value, advisoryKeys);
var key = $"{collision.Scheme}\u0001{collision.Value}";
collisionMap.TryAdd(key, collision);
}
}
var aliasMap = new Dictionary<string, IReadOnlyList<AliasRecord>>(aliasCache, StringComparer.OrdinalIgnoreCase);
return new AliasComponent(advisoryKey, visited.ToArray(), collisionMap.Values.ToArray(), aliasMap);
}
private async Task<IReadOnlyList<AliasRecord>> GetAliasesAsync(
string advisoryKey,
CancellationToken cancellationToken,
IDictionary<string, IReadOnlyList<AliasRecord>> cache)
{
if (cache.TryGetValue(advisoryKey, out var cached))
{
return cached;
}
var aliases = await _aliasStore.GetByAdvisoryAsync(advisoryKey, cancellationToken).ConfigureAwait(false);
cache[advisoryKey] = aliases;
return aliases;
}
private Task<IReadOnlyList<AliasRecord>> GetAdvisoriesForAliasAsync(
string scheme,
string value,
CancellationToken cancellationToken)
=> _aliasStore.GetByAliasAsync(scheme, value, cancellationToken);
}
public sealed record AliasIdentityResult(string AdvisoryKey, IReadOnlyList<AliasRecord> Aliases, IReadOnlyList<AliasCollision> Collisions);
public sealed record AliasComponent(
string SeedAdvisoryKey,
IReadOnlyList<string> AdvisoryKeys,
IReadOnlyList<AliasCollision> Collisions,
IReadOnlyDictionary<string, IReadOnlyList<AliasRecord>> AliasMap);