121 lines
5.0 KiB
C#
121 lines
5.0 KiB
C#
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Client;
|
|
|
|
/// <summary>
|
|
/// One node in a lazy-loaded Galaxy browse tree. Holds the underlying
|
|
/// <see cref="GalaxyObject"/> and exposes <see cref="ExpandAsync"/> 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.
|
|
/// </summary>
|
|
public sealed class LazyBrowseNode
|
|
{
|
|
private readonly GalaxyRepositoryClient _client;
|
|
private readonly BrowseChildrenOptions _options;
|
|
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<LazyBrowseNode> _children = [];
|
|
private volatile bool _isExpanded;
|
|
|
|
internal LazyBrowseNode(
|
|
GalaxyRepositoryClient client,
|
|
GalaxyObject @object,
|
|
bool hasChildrenHint,
|
|
BrowseChildrenOptions options)
|
|
{
|
|
_client = client;
|
|
Object = @object;
|
|
HasChildrenHint = hasChildrenHint;
|
|
_options = options;
|
|
}
|
|
|
|
/// <summary>The underlying Galaxy object for this node.</summary>
|
|
public GalaxyObject Object { get; }
|
|
|
|
/// <summary>True when the server reports this node has at least one matching descendant.</summary>
|
|
public bool HasChildrenHint { get; }
|
|
|
|
/// <summary>Direct children loaded by <see cref="ExpandAsync"/>; empty until then.</summary>
|
|
public IReadOnlyList<LazyBrowseNode> Children => Volatile.Read(ref _children);
|
|
|
|
/// <summary>True after the first <see cref="ExpandAsync"/> call completes.</summary>
|
|
public bool IsExpanded => _isExpanded;
|
|
|
|
/// <summary>
|
|
/// Fetches direct children from the gateway and populates <see cref="Children"/>.
|
|
/// Idempotent: subsequent calls are no-ops.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Thread-safe: concurrent callers see exactly one fetch; subsequent callers
|
|
/// (after the first completes) return immediately. <see cref="Children"/> and
|
|
/// <see cref="IsExpanded"/> may be read concurrently with an in-flight
|
|
/// <see cref="ExpandAsync"/> on another thread; the populated children are
|
|
/// published as an immutable snapshot under a release barrier, so a reader that
|
|
/// observes <see cref="IsExpanded"/> as <see langword="true"/> always sees the
|
|
/// fully-populated <see cref="Children"/>, and a reader never enumerates a
|
|
/// partially-built list.
|
|
/// </remarks>
|
|
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
|
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<LazyBrowseNode> children = [];
|
|
string pageToken = string.Empty;
|
|
HashSet<string> 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();
|
|
}
|
|
}
|
|
}
|