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 GalaxyRepositoryClient _client;
private readonly ConcurrentDictionary<string, GalaxyObject> _byTagName = new(StringComparer.Ordinal); private readonly ConcurrentDictionary<string, GalaxyObject> _byTagName = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<int, GalaxyObject> _byGobjectId = new(); 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> /// <summary>Opaque token identifying this session in the AdminUI registry.</summary>
public Guid Token { get; } = Guid.NewGuid(); public Guid Token { get; } = Guid.NewGuid();
@@ -48,6 +50,9 @@ internal sealed class GalaxyBrowseSession : IBrowseSession
public async Task<IReadOnlyList<BrowseNode>> RootAsync(CancellationToken cancellationToken) public async Task<IReadOnlyList<BrowseNode>> RootAsync(CancellationToken cancellationToken)
{ {
ObjectDisposedException.ThrowIf(_disposed, this); ObjectDisposedException.ThrowIf(_disposed, this);
await _rootGate.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
var all = await _client.DiscoverHierarchyAsync( var all = await _client.DiscoverHierarchyAsync(
new DiscoverHierarchyOptions { IncludeAttributes = false }, cancellationToken) new DiscoverHierarchyOptions { IncludeAttributes = false }, cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
@@ -59,6 +64,10 @@ internal sealed class GalaxyBrowseSession : IBrowseSession
_byGobjectId[obj.GobjectId] = 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 // Roots are objects whose parent isn't part of the returned set (typically
// ParentGobjectId == 0 for WinPlatforms / top-level Areas). // ParentGobjectId == 0 for WinPlatforms / top-level Areas).
var roots = all var roots = all
@@ -68,6 +77,11 @@ internal sealed class GalaxyBrowseSession : IBrowseSession
LastUsedUtc = DateTime.UtcNow; LastUsedUtc = DateTime.UtcNow;
return Project(roots); return Project(roots);
} }
finally
{
_rootGate.Release();
}
}
/// <summary> /// <summary>
/// Returns the direct children of the cached Galaxy object identified by /// Returns the direct children of the cached Galaxy object identified by
@@ -146,7 +160,7 @@ internal sealed class GalaxyBrowseSession : IBrowseSession
_byGobjectId[obj.GobjectId] = obj; _byGobjectId[obj.GobjectId] = obj;
var displayName = !string.IsNullOrEmpty(obj.ContainedName) ? obj.ContainedName : obj.TagName; 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( result.Add(new BrowseNode(
NodeId: obj.TagName, NodeId: obj.TagName,
DisplayName: displayName, DisplayName: displayName,
@@ -179,6 +193,7 @@ internal sealed class GalaxyBrowseSession : IBrowseSession
{ {
if (_disposed) return; if (_disposed) return;
_disposed = true; _disposed = true;
_rootGate.Dispose();
try try
{ {
await _session.DisposeAsync().ConfigureAwait(false); await _session.DisposeAsync().ConfigureAwait(false);