141 lines
5.2 KiB
C#
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);
|