feat: thread BrowseNext continuation token through actor + BrowseService (T15)

This commit is contained in:
Joseph Doherty
2026-06-18 02:43:25 -04:00
parent d5e7e897c0
commit 9ec2450ad5
5 changed files with 79 additions and 5 deletions
@@ -40,6 +40,7 @@ public sealed class BrowseService : IBrowseService
string siteId,
string connectionName,
string? parentNodeId,
string? continuationToken = null,
CancellationToken cancellationToken = default)
{
// CentralUI-side role guard — sites don't enforce envelope-level roles,
@@ -57,7 +58,7 @@ public sealed class BrowseService : IBrowseService
{
return await _communication.BrowseNodeAsync(
siteId,
new BrowseNodeCommand(connectionName, parentNodeId),
new BrowseNodeCommand(connectionName, parentNodeId, continuationToken),
cancellationToken);
}
catch (TimeoutException ex)
@@ -28,11 +28,13 @@ public interface IBrowseService
/// <param name="siteId">The target site identifier.</param>
/// <param name="connectionName">Name of the site-local data connection to browse against — the site's <c>DataConnectionManagerActor</c> indexes its children by name.</param>
/// <param name="parentNodeId">Node to browse, or <c>null</c> to browse from the server root.</param>
/// <param name="continuationToken">Opaque cursor from a prior <see cref="BrowseNodeResult.ContinuationToken"/> to fetch the NEXT page under <paramref name="parentNodeId"/>; <c>null</c> for the first page.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a <see cref="BrowseNodeResult"/> containing child nodes or a <see cref="BrowseFailure"/> on error.</returns>
Task<BrowseNodeResult> BrowseChildrenAsync(
string siteId,
string connectionName,
string? parentNodeId,
string? continuationToken = null,
CancellationToken cancellationToken = default);
}
@@ -15,14 +15,30 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
/// </remarks>
/// <param name="ConnectionName">Name of the site-local data connection to browse against.</param>
/// <param name="ParentNodeId">Node to browse, or null to browse from the server root (ObjectsFolder).</param>
/// <param name="ContinuationToken">
/// Opaque adapter cursor for fetching the NEXT page of children under
/// <see cref="ParentNodeId"/>. Null on the first request; on a subsequent
/// request carry back the token from the prior <see cref="BrowseNodeResult.ContinuationToken"/>.
/// Additive (appended last) so positional construction stays source-compatible.
/// </param>
public record BrowseNodeCommand(
string ConnectionName,
string? ParentNodeId);
string? ParentNodeId,
string? ContinuationToken = null);
/// <param name="Children">Immediate children resolved for the browsed node (this page only).</param>
/// <param name="Truncated">True when the result was clipped (frame-budget cap or adapter-reported truncation).</param>
/// <param name="Failure">Structured failure, or null on success.</param>
/// <param name="ContinuationToken">
/// Opaque adapter cursor for the NEXT page when more children remain, or null
/// when this is the final page. Surface it back in a follow-up
/// <see cref="BrowseNodeCommand.ContinuationToken"/>. Additive (appended last).
/// </param>
public record BrowseNodeResult(
IReadOnlyList<BrowseNode> Children,
bool Truncated,
BrowseFailure? Failure);
BrowseFailure? Failure,
string? ContinuationToken = null);
public record BrowseFailure(
BrowseFailureKind Kind,
@@ -1163,14 +1163,16 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
_log.Debug("[{0}] Browsing children of {1}", _connectionName, command.ParentNodeId ?? "(root)");
browsable.BrowseChildrenAsync(command.ParentNodeId).ContinueWith(t =>
browsable.BrowseChildrenAsync(command.ParentNodeId, command.ContinuationToken).ContinueWith(t =>
{
if (t.IsCompletedSuccessfully)
{
// Bound the reply to stay under Akka's remote frame size before it
// crosses the site→central boundary (see CapBrowseChildren).
var (children, truncated) = CapBrowseChildren(t.Result.Children, t.Result.Truncated);
return new BrowseNodeResult(children, truncated, Failure: null);
// Carry the adapter's continuation cursor through so the UI can ask
// for the next page (BrowseNext). Null when this is the final page.
return new BrowseNodeResult(children, truncated, Failure: null, t.Result.ContinuationToken);
}
var baseEx = t.Exception?.GetBaseException();