using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy; namespace ZB.MOM.WW.MxGateway.Client; /// /// One node in a lazy-loaded Galaxy browse tree. Holds the underlying /// and exposes to fetch /// its direct children on demand. Expansion is one-shot: a second call is a /// no-op. Pagination of large sibling sets is handled internally. /// public sealed class LazyBrowseNode { private readonly GalaxyRepositoryClient _client; private readonly BrowseChildrenOptions _options; // Client.Dotnet-027 (Won't Fix): this gate is used only via WaitAsync/Release and // never via AvailableWaitHandle, so SemaphoreSlim allocates no kernel wait handle — // it holds no unmanaged/OS handle to leak. It is pure managed memory whose lifetime // is the node's, so the type is intentionally not IDisposable: making it disposable // would push per-node disposal onto every tree consumer (thousands of nodes) for no // resource benefit. private readonly SemaphoreSlim _expandLock = new(1, 1); // Published once, under _expandLock, when expansion completes. Lock-free readers // see either the empty pre-expansion snapshot or the fully-populated post-expansion // snapshot — never a partially-filled list — because the snapshot is built in a local // and handed off via Volatile.Write (release) paired with Volatile.Read (acquire). private IReadOnlyList _children = []; private volatile bool _isExpanded; internal LazyBrowseNode( GalaxyRepositoryClient client, GalaxyObject @object, bool hasChildrenHint, BrowseChildrenOptions options) { _client = client; Object = @object; HasChildrenHint = hasChildrenHint; _options = options; } /// The underlying Galaxy object for this node. public GalaxyObject Object { get; } /// True when the server reports this node has at least one matching descendant. public bool HasChildrenHint { get; } /// Direct children loaded by ; empty until then. public IReadOnlyList Children => Volatile.Read(ref _children); /// True after the first call completes. public bool IsExpanded => _isExpanded; /// /// Fetches direct children from the gateway and populates . /// Idempotent: subsequent calls are no-ops. /// /// /// Thread-safe: concurrent callers see exactly one fetch; subsequent callers /// (after the first completes) return immediately. and /// may be read concurrently with an in-flight /// on another thread; the populated children are /// published as an immutable snapshot under a release barrier, so a reader that /// observes as always sees the /// fully-populated , and a reader never enumerates a /// partially-built list. /// /// Token to observe for cancellation. public async Task ExpandAsync(CancellationToken cancellationToken = default) { if (_isExpanded) { return; } await _expandLock.WaitAsync(cancellationToken).ConfigureAwait(false); try { if (_isExpanded) { return; } // Accumulate into a local list, never the published field, so a lock-free // reader can never observe a half-populated collection or enumerate a list // that is being mutated mid-append. List children = []; string pageToken = string.Empty; HashSet seenPageTokens = new(StringComparer.Ordinal); do { BrowseChildrenRequest request = GalaxyRepositoryClient.BuildBrowseChildrenRequest(_options); request.ParentGobjectId = Object.GobjectId; request.PageToken = pageToken; BrowseChildrenReply reply = await _client .BrowseChildrenRawAsync(request, cancellationToken) .ConfigureAwait(false); for (int i = 0; i < reply.Children.Count; i++) { bool hint = i < reply.ChildHasChildren.Count && reply.ChildHasChildren[i]; children.Add(new LazyBrowseNode(_client, reply.Children[i], hint, _options)); } pageToken = reply.NextPageToken; if (!string.IsNullOrWhiteSpace(pageToken) && !seenPageTokens.Add(pageToken)) { throw new MxGatewayException( $"Galaxy BrowseChildren returned a repeated page token '{pageToken}'."); } } while (!string.IsNullOrWhiteSpace(pageToken)); // Publish the completed, immutable snapshot (release) before marking the node // expanded (the volatile write below). A reader that observes IsExpanded == true // is guaranteed to also observe the fully-populated Children. Volatile.Write(ref _children, children); _isExpanded = true; } finally { _expandLock.Release(); } } }