# Client Lazy-Browse Walker Helpers + Per-Language Tests Date: 2026-05-28 Status: approved, ready for implementation plan ## Problem The `BrowseChildren` RPC shipped (branch `feat/lazy-browse-children`, merged or pending merge), but each language client exposes only the raw generated gRPC stub. Callers must hand-write recursion, sibling pagination, and NotFound translation themselves. Only one client (.NET) has a smoke test, and it is skippable. This work adds a small high-level walker to each client and unit tests so callers can build OPC UA-style browse trees without re-implementing the same plumbing five times. ## Scope Each of the five clients (.NET, Python, Rust, Go, Java) gains: 1. A low-level `BrowseChildren*Async` wrapper on the existing `GalaxyRepositoryClient`, mirroring the existing `DiscoverHierarchy*Async` shape. 2. A high-level `LazyBrowseNode` type plus a `BrowseAsync` factory. 3. Five unit tests against the language's existing fake-transport fixture. Plus a one-time toolchain bootstrap so the Java client builds locally on the macOS dev host (Homebrew install of Temurin 21 + Gradle). ## Architecture `LazyBrowseNode` is shared in shape across languages: ```text LazyBrowseNode { Object GalaxyObject (immutable, from server) HasChildrenHint bool (server's child_has_children value) Children list (empty until Expand) IsExpanded bool ExpandAsync(ct) Task (idempotent; no-op after first call) } ``` `GalaxyRepositoryClient.BrowseAsync(parent?, ct)` returns a list of root `LazyBrowseNode`s. Empty `parent` means structural roots. Each returned node is unexpanded; the caller invokes `ExpandAsync` to fetch direct children. After expand, `Children` is a list of further `LazyBrowseNode`s. **Pagination is hidden.** `ExpandAsync` walks `next_page_token` internally until all siblings of this parent are gathered. Callers see one flat `Children` list. **Errors:** server `NotFound` becomes a language-idiomatic typed error (`MxGatewayException` in .NET, `GalaxyNotFoundError` in Python, `GalaxyError::NotFound` in Rust, typed error in Go, `GalaxyNotFoundException` in Java). **Filters:** `BrowseAsync` accepts a `BrowseChildrenOptions` (or language-equivalent) mirroring the existing `DiscoverHierarchyOptions`. The same options apply to every `ExpandAsync` call rooted from that factory call — stored on the node so child expansions inherit them. ## Per-language API Each language adapts to its own idioms; the structure is parallel. ### .NET (`clients/dotnet/ZB.MOM.WW.MxGateway.Client/GalaxyRepositoryClient.cs`) ```csharp public sealed class LazyBrowseNode { public GalaxyObject Object { get; } public bool HasChildrenHint { get; } public IReadOnlyList Children { get; } public bool IsExpanded { get; } public Task ExpandAsync(CancellationToken ct = default); } public Task> BrowseAsync( BrowseChildrenOptions? options = null, CancellationToken ct = default); public Task BrowseChildrenRawAsync( BrowseChildrenRequest request, CancellationToken ct = default); ``` ### Python (`clients/python/src/zb_mom_ww_mxgateway/galaxy.py`) ```python @dataclass class LazyBrowseNode: object: GalaxyObject has_children_hint: bool children: list["LazyBrowseNode"] is_expanded: bool async def expand(self) -> None: ... async def browse( self, options: BrowseChildrenOptions | None = None, ) -> list[LazyBrowseNode]: ... ``` ### Rust (`clients/rust/src/galaxy.rs`) ```rust pub struct LazyBrowseNode { /* private fields; Arc> for Children */ } impl LazyBrowseNode { pub fn object(&self) -> &GalaxyObject; pub fn has_children_hint(&self) -> bool; pub fn children(&self) -> Vec; // cloned snapshot pub fn is_expanded(&self) -> bool; pub async fn expand(&self) -> Result<(), GalaxyError>; } pub async fn browse( &self, options: Option, ) -> Result, GalaxyError>; ``` ### Go (`clients/go/mxgateway/galaxy.go`) ```go type LazyBrowseNode struct { /* unexported */ } func (n *LazyBrowseNode) Object() *pb.GalaxyObject func (n *LazyBrowseNode) HasChildrenHint() bool func (n *LazyBrowseNode) Children() []*LazyBrowseNode func (n *LazyBrowseNode) IsExpanded() bool func (n *LazyBrowseNode) Expand(ctx context.Context) error func (c *Client) Browse( ctx context.Context, opts *BrowseChildrenOptions, ) ([]*LazyBrowseNode, error) ``` ### Java (`clients/java/zb-mom-ww-mxgateway-client/`) ```java public final class LazyBrowseNode { public GalaxyObject getObject(); public boolean hasChildrenHint(); public List getChildren(); public boolean isExpanded(); public CompletableFuture expandAsync(); } public CompletableFuture> browseAsync( BrowseChildrenOptions options); ``` If the existing Java client surface is synchronous, mirror that — both sync and async variants are acceptable as long as the choice matches the client's existing convention. ## Tests Each language adds these six facts against its existing fake-transport fixture (`FakeGalaxyRepositoryTransport` in .NET, the equivalent in each other client): | # | Test | Purpose | |---|------|---------| | 1 | `Browse_NoParent_ReturnsRoots` | factory returns roots, each unexpanded, hint reflects fake's `child_has_children` | | 2 | `Expand_PopulatesChildrenAndMarksExpanded` | one ExpandAsync call fires one BrowseChildren RPC; Children populated; IsExpanded flips | | 3 | `Expand_CalledTwice_NoSecondRpc` | idempotency — fake records RPC count == 1 | | 4 | `Expand_UnknownParent_ThrowsGalaxyNotFound` | server NotFound surfaces as language-typed error | | 5 | `Expand_MultiPageSiblings_GathersAllPages` | fake returns NextPageToken on first call; helper walks pages until empty; flat Children list | | 6 | `Browse_WithFilter_ForwardsToRequest` | options propagate into the wire request (`tag_name_glob` etc.) | No new live-only tests in this batch. The existing `BrowseChildrenSmokeTests` in .NET covers wire compatibility. ## Java toolchain bootstrap The macOS dev host lacks a JVM. Install via Homebrew (one-time): ```bash brew install temurin@21 brew install gradle ``` Verify: ```bash java -version # expect 21.x gradle --version # expect 8.x or 9.x ``` If the Temurin formula does not auto-link `JAVA_HOME`, add to shell init: ```bash export JAVA_HOME="$(/usr/libexec/java_home -v 21)" ``` Then verify the existing Java client builds against its committed generated tree: ```bash cd clients/java gradle build -x test ``` If build succeeds, regenerate protos to pick up `BrowseChildren`: ```bash gradle generateProto ``` This produces the new Java RPC stubs that the walker work depends on. **Failure path:** if Homebrew installs but `gradle build` fails for an environmental reason (e.g., proto plugin version mismatch), fall back to "defer Java" — implement the other four clients and document that Java walker work waits for the Windows host. Do not spend more than ~30 minutes debugging local Java issues; the Windows host already builds the Java client cleanly. ## Documentation updates Each client's `README.md` "Browsing lazily" snippet (added in commit `0d6193c`) gets one short example block showing the high-level walker in addition to the existing raw-RPC snippet. Approximately three sentences plus a 5-line code block per language. No changes to `gateway.md`, `docs/GalaxyRepository.md`, or `docs/DesignDecisions.md` — those describe the wire contract; the walkers are client-side ergonomics, not part of the wire surface. ## Non-goals - Async iterator / streaming walker (rejected in brainstorming — encourages eager-to-completion consumption that defeats laziness). - Explicit `RefreshAsync` on `LazyBrowseNode` (single-shot expand is enough; caller invalidates the tree by re-calling `BrowseAsync`). - Tree-builder helpers that pre-fetch the whole hierarchy (that's just `DiscoverHierarchy` with extra round-trips). - Server changes — the wire contract is final. - Cross-client integration test runner — each client tests in isolation. - Java regen on Mac if Homebrew install fails — defer to Windows host.