fix(galaxy.browser): volatile _disposed, RootAsync gate, O(1) child hint

This commit is contained in:
Joseph Doherty
2026-05-28 15:51:31 -04:00
parent 56be42913c
commit b869af2b3d
@@ -20,7 +20,9 @@ internal sealed class GalaxyBrowseSession : IBrowseSession
private readonly GalaxyRepositoryClient _client;
private readonly ConcurrentDictionary<string, GalaxyObject> _byTagName = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<int, GalaxyObject> _byGobjectId = new();
private bool _disposed;
private volatile bool _disposed;
private readonly SemaphoreSlim _rootGate = new(1, 1);
private HashSet<int> _hasChildrenSet = new();
/// <summary>Opaque token identifying this session in the AdminUI registry.</summary>
public Guid Token { get; } = Guid.NewGuid();
@@ -48,25 +50,37 @@ internal sealed class GalaxyBrowseSession : IBrowseSession
public async Task<IReadOnlyList<BrowseNode>> RootAsync(CancellationToken cancellationToken)
{
ObjectDisposedException.ThrowIf(_disposed, this);
var all = await _client.DiscoverHierarchyAsync(
new DiscoverHierarchyOptions { IncludeAttributes = false }, cancellationToken)
.ConfigureAwait(false);
// Populate caches so later ExpandAsync calls can resolve children in-memory.
foreach (var obj in all)
await _rootGate.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
_byTagName[obj.TagName] = obj;
_byGobjectId[obj.GobjectId] = obj;
var all = await _client.DiscoverHierarchyAsync(
new DiscoverHierarchyOptions { IncludeAttributes = false }, cancellationToken)
.ConfigureAwait(false);
// Populate caches so later ExpandAsync calls can resolve children in-memory.
foreach (var obj in all)
{
_byTagName[obj.TagName] = obj;
_byGobjectId[obj.GobjectId] = obj;
}
// Precompute the set of GobjectIds that appear as a parent — used by
// Project to compute HasChildrenHint in O(1) instead of O(n²).
_hasChildrenSet = new HashSet<int>(_byGobjectId.Values.Select(o => o.ParentGobjectId));
// Roots are objects whose parent isn't part of the returned set (typically
// ParentGobjectId == 0 for WinPlatforms / top-level Areas).
var roots = all
.Where(o => o.ParentGobjectId == 0 || !_byGobjectId.ContainsKey(o.ParentGobjectId))
.ToList();
LastUsedUtc = DateTime.UtcNow;
return Project(roots);
}
finally
{
_rootGate.Release();
}
// Roots are objects whose parent isn't part of the returned set (typically
// ParentGobjectId == 0 for WinPlatforms / top-level Areas).
var roots = all
.Where(o => o.ParentGobjectId == 0 || !_byGobjectId.ContainsKey(o.ParentGobjectId))
.ToList();
LastUsedUtc = DateTime.UtcNow;
return Project(roots);
}
/// <summary>
@@ -146,7 +160,7 @@ internal sealed class GalaxyBrowseSession : IBrowseSession
_byGobjectId[obj.GobjectId] = obj;
var displayName = !string.IsNullOrEmpty(obj.ContainedName) ? obj.ContainedName : obj.TagName;
var hasChildrenHint = _byGobjectId.Values.Any(o => o.ParentGobjectId == obj.GobjectId);
var hasChildrenHint = _hasChildrenSet.Contains(obj.GobjectId);
result.Add(new BrowseNode(
NodeId: obj.TagName,
DisplayName: displayName,
@@ -179,6 +193,7 @@ internal sealed class GalaxyBrowseSession : IBrowseSession
{
if (_disposed) return;
_disposed = true;
_rootGate.Dispose();
try
{
await _session.DisposeAsync().ConfigureAwait(false);