Skip to content

feat(ws-controller): network topology service + get_network_topology API (schema 13) - #832

Open
MindFreeze wants to merge 13 commits into
matter-js:mainfrom
MindFreeze:matter-network-topology-service
Open

feat(ws-controller): network topology service + get_network_topology API (schema 13)#832
MindFreeze wants to merge 13 commits into
matter-js:mainfrom
MindFreeze:matter-network-topology-service

Conversation

@MindFreeze

@MindFreeze MindFreeze commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Part of the Matter network visualization work for OpenHomeFoundation roadmap #210. Follows #829 (the shared topology-derivation extraction), which this builds on.

Derives the full Matter network graph (Thread mesh + Wi-Fi star) server-side and exposes it over the WebSocket API, so downstream consumers (Home Assistant core / frontend) can relay a ready-made topology instead of each re-deriving it from raw cluster attributes and lacking the server-only mDNS Border Router data.

What's here

  • NetworkTopologyService — builds the graph from the shared ws-client derivation pipeline plus the passively-discovered Border Router registry. Conventions mirror ThreadDiagnosticsService (injected Pick<> deps, an Observable event, testable without a controller). Hybrid update model: debounced change-driven rebuilds, emit-only-when-changed (hash compare excluding the timestamp), and a slow periodic rebuild for sleepy-device drift. refresh() re-reads Thread neighbor/route tables (and Wi-Fi diagnostics) from online nodes, concurrency-capped under an overall deadline.
  • get_network_topology { refresh? } command + network_topology_updated event, bumping SCHEMA_VERSION to 13 (min supported stays 11). The event is delivered only to connections that have issued the command, mirroring the schema-12 thread_diagnostics_updated opt-in so pre-13 clients never receive an event type they didn't subscribe to.
  • Wire types (NetworkTopology / node / connection, snake_case): both observed Thread link directions are preserved, with a strongest-direction summary strength for simple renderers.

Scope

This first iteration derives Thread links from the nodes' own ThreadNetworkDiagnostics (neighbor + route tables) and classifies externals against the BR registry. The richer MeshCoP diagnostic enrichment (route64 / childTable → router-to-router links and diagnostic-only mesh nodes) is a planned follow-up; the wire model already accommodates it.

@MindFreeze

Copy link
Copy Markdown
Contributor Author

I'll keep this in draft until I have tested the whole stack. May need to tweak the API

@MindFreeze
MindFreeze force-pushed the matter-network-topology-service branch 3 times, most recently from 8e3ec33 to d46276f Compare July 9, 2026 14:32
…ogy API (schema 13)

Derive the full Matter network graph (Thread mesh + Wi-Fi star) server-side and
expose it over the WebSocket API.

- NetworkTopologyService builds the graph from the shared ws-client derivation
  pipeline plus the mDNS Border Router registry. Hybrid update model: debounced
  change-driven rebuilds, emit-only-when-changed (hash excluding collected_at),
  and a slow periodic rebuild for sleepy-device drift. refresh() re-reads Thread
  neighbor/route tables (and Wi-Fi diagnostics) from online nodes, concurrency-
  capped under an overall deadline.
- get_network_topology {refresh?} command and network_topology_updated event,
  bumping SCHEMA_VERSION to 13 (min stays 11). The event is delivered only to
  connections that have issued the command, mirroring the schema-12
  thread_diagnostics_updated opt-in so pre-13 clients never see a new event type.
- NetworkTopology / node / connection wire types (snake_case), with both observed
  Thread link directions preserved and a strongest-direction summary strength.

This first iteration derives Thread links from the nodes' own diagnostics and
classifies externals against the BR registry; the richer MeshCoP enrichment
(route64 / childTable) is a planned follow-up the wire model already accommodates.
@MindFreeze
MindFreeze force-pushed the matter-network-topology-service branch from d46276f to 08b8536 Compare July 22, 2026 04:43
Copilot AI review requested due to automatic review settings July 22, 2026 04:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a server-side network-topology derivation service to the ws-controller layer and exposes it via a schema-13 WebSocket API so clients (e.g. Home Assistant) can consume a ready-made Matter network graph instead of re-deriving it.

Changes:

  • Introduces NetworkTopologyService to build and refresh a combined Thread mesh + Wi‑Fi star topology and emit updates when the derived graph changes.
  • Adds get_network_topology { refresh? } and network_topology_updated (opt-in per connection) and bumps WebSocket schema to 13.
  • Extends wire models (@matter-server/ws-client) with NetworkTopology* types and updates integration tests + schema changelog.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
python_client/tests/test_integration.py Updates expected schema_version to 13 in Python integration tests.
packages/ws-controller/test/WsBackpressureReproTest.ts Updates controller stubs to include networkTopology observable.
packages/ws-controller/test/WebSocketRequestSerializationTest.ts Updates controller stubs to include networkTopology observable.
packages/ws-controller/test/WebSocketCredentialsApiTest.ts Adds coverage for get_network_topology and per-connection opt-in event delivery; updates schema assertions.
packages/ws-controller/test/SetThreadDatasetCredentialsSyncTest.ts Updates controller stub to include networkTopology service.
packages/ws-controller/test/NetworkTopologyServiceTest.ts Adds unit tests for topology derivation, debounced emission, refresh fanout, and stop behavior.
packages/ws-controller/src/server/WebSocketControllerHandler.ts Bumps schema to 13; adds command handling and opt-in gated network_topology_updated event sending.
packages/ws-controller/src/controller/NetworkTopologyService.ts New service: builds topology snapshots, refreshes diagnostics under concurrency cap/deadline, emits only on change.
packages/ws-controller/src/controller/MatterController.ts Wires a lazily constructed networkTopology service into the controller and stops it on shutdown.
packages/ws-client/src/models/model.ts Adds schema-13 wire types for NetworkTopology + new command/event definitions.
packages/matter-server/test/IntegrationTest.ts Updates expected schema_version to 13 in JS integration tests.
docs/websocket-api-schema-changelog.md Documents schema 13 changes (command/event + opt-in behavior).

Comment thread packages/ws-controller/src/server/WebSocketControllerHandler.ts Outdated
Comment thread packages/ws-controller/src/server/WebSocketControllerHandler.ts
Comment thread packages/ws-controller/src/controller/NetworkTopologyService.ts
Comment thread packages/ws-controller/src/controller/NetworkTopologyService.ts
- Register the per-connection topologyUpdated observer lazily on first
  get_network_topology, so connections that never request topology don't
  instantiate the service (timers, event subscriptions) via the lazy getter.
- Stop starting new attribute reads once the refresh() deadline expires;
  in-flight reads still complete (no cancellation for reads) and land in
  the cache for the next rebuild.
- Schedule a rebuild on Border Router `updated` events too, so a BR's
  network name / metadata resolving later is reflected without waiting for
  the periodic rebuild.
Copilot AI review requested due to automatic review settings July 22, 2026 04:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Comment thread packages/ws-client/src/models/model.ts
Comment thread packages/ws-client/src/models/model.ts
Comment thread docs/websocket-api-schema-changelog.md Outdated
The synthetic Wi-Fi AP id is `ap_<BSSID>` with colons removed (e.g.
`ap_112233445566`); the model doc comments and schema changelog said
`ap_<BSSID>` without noting the normalization.
Copilot AI review requested due to automatic review settings July 22, 2026 04:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Comment thread packages/ws-controller/src/controller/NetworkTopologyService.ts
Comment thread packages/ws-controller/test/WsBackpressureReproTest.ts Outdated
Comment thread packages/ws-controller/test/WebSocketRequestSerializationTest.ts Outdated
- hashTopology() now sorts nodes (by id) and connections (by source/target/
  network) before hashing. The graph is a set, but build order isn't stable
  (listNodes iteration, neighbor-table arrival order), so an unsorted hash
  re-emitted network_topology_updated on pure reorderings.
- Remove the now-dead networkTopology stubs from the three controller-handler
  tests that never issue get_network_topology: with the observer registered
  lazily on opt-in, register() only eagerly subscribes to thread-diagnostics.
  Comments corrected to match.
Copilot AI review requested due to automatic review settings July 22, 2026 05:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Comment thread packages/ws-controller/src/controller/NetworkTopologyService.ts
refresh() only checked #stopped at entry, so a stop() during the awaited
read fan-out still reached #emitIfChanged and broadcast topologyUpdated
after shutdown. Re-check #stopped after the await; the caller still gets
the built snapshot, but a stopped service no longer emits.
Copilot AI review requested due to automatic review settings July 22, 2026 05:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Comment thread packages/ws-controller/src/controller/NetworkTopologyService.ts
Comment thread packages/ws-controller/src/controller/NetworkTopologyService.ts
…olated

Interpolating the caught error into the message drops the stack trace (and
can render [object Object]). Pass it as a separate logger argument, matching
the handler's logging convention, so refresh/rebuild failures are diagnosable.
Copilot AI review requested due to automatic review settings July 22, 2026 05:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Comment thread packages/ws-controller/src/controller/NetworkTopologyService.ts Outdated
Comment thread packages/ws-controller/test/NetworkTopologyServiceTest.ts Outdated
Edge/unknown matching falls back to RLOC16 when extended-address matching
fails, and RLOC16 changes when a device re-attaches to the mesh — so
refresh() must re-read 0/53/64 alongside the neighbor/route tables. The
path constants are now exported and imported by the test, so the two
copies can't drift.
Copilot AI review requested due to automatic review settings July 23, 2026 05:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings July 30, 2026 14:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.

@Apollon77 Apollon77 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review pass. Solid shape overall — the shared ws-client derivation reuse, the lazy per-connection observer, the sorted hash, and the stop-mid-refresh handling are all right. One functional blocker plus a few robustness items, inline.

Blocker: refresh never actually refreshes — the attribute read is issued with fabricFiltered=false, which matter.js discards for state integration, so the rebuild sees the same stale cache. Details on MatterController.ts.

Not inline-able, one more item: with --disable-thread-diagnostics the BorderRouterRegistry is constructed but never started, so list() stays empty and every Thread neighbour classifies as thread_unknown — the graph silently loses all border_router nodes. Worth a line in the schema changelog (or a flag in the payload) so consumers don't read the absence as "no BRs on the network".

Comment thread packages/ws-controller/src/controller/MatterController.ts Outdated
Comment thread packages/ws-controller/src/controller/MatterController.ts
Comment thread packages/ws-controller/src/controller/MatterController.ts
Comment thread packages/ws-controller/src/controller/NetworkTopologyService.ts
Comment thread packages/ws-controller/src/controller/NetworkTopologyService.ts
Comment thread packages/ws-controller/src/controller/NetworkTopologyService.ts Outdated
Comment thread packages/ws-controller/src/controller/NetworkTopologyService.ts Outdated
}

/** WiFi RSSI → strength (ports the dashboard's -70/-85 dBm thresholds). Unknown RSSI is treated as medium. */
function rssiToStrength(rssi: number | null): TopologyStrength {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: an unknown RSSI reporting as "medium" is indistinguishable on the wire from a genuinely medium link, and strength is a required field, so consumers can't detect the synthetic value. Consider "none", or make the direction info absent when there is no measurement.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changed — left as your deliberate parity with the dashboard thresholds. Flagging only so the synthetic value is a known property of the wire contract. Leaving open for your call.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll change it to none

@Apollon77 Apollon77 Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on 7596196: mapping an unmeasured RSSI to "none" fixes the undetectable-synthetic-value problem but lands on the one value that already means something else. mapThreadPair drops Thread pairs whose every observed direction is "none", so a Thread edge can never be "none" on the wire, and consumers use exactly that convention as a liveness test — the dashboard filters on signalLevel !== "none" in network-utils.ts:488, thread-graph.ts:466-467, :576 and :655. A Wi-Fi station whose RSSI cannot be read would ship an edge that any such consumer hides, leaving a live station rendered unlinked from its AP.

Proposed fix, prepared as a commit: TopologyStrength gains "unknown" ("no measurement, link presumed up"), rssiToStrength(null) returns it, and the changelog + TopologyStrength doc spell out the none vs unknown split. Your source_to_target: undefined for the unmeasured case is kept as-is. Schema 13 is unreleased so the union is free to grow, and #917 models strength as an open string set, so the Python client parses it unchanged.

Dashboard visualisation is unaffected: it never imports the wire NetworkTopology/TopologyStrength types — it derives its own graph and colours Wi-Fi edges from raw RSSI via getSignalColorFromRssi (null → medium colour). Worth aligning when the dashboard switches to consuming the server-derived graph: unknown should render neutral-but-live there rather than reusing the medium colour.

Gates on the prepared commit: format/lint/build clean, ws-controller 353/353, ws-client 79/79, dashboard 153/153.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed as b3a91ed.

Comment thread packages/ws-controller/test/NetworkTopologyServiceTest.ts
The topology refresh read attributes with fabricFiltered=false. matter.js only
integrates a read into the subscribed datasource when the read's filter matches
the subscription's (default: filtered), so those reads were discarded — no cache
write, no attributeChanged — and the rebuild that followed saw exactly the stale
values the refresh was issued to replace. The read path now mirrors the
dashboard's update-connections fix.

Alongside that:

- Concurrent refresh callers share one in-flight run, so N clients requesting a
  refresh at once no longer multiply the per-node radio traffic.
- Externals whose every observed link is dead (LQI 0) are dropped instead of
  shipping as graph nodes with no edges.
- RoutingRole and NetworkName join the refresh path set, and a node's own
  NetworkName is preferred over the Border Router registry lookup.
- The lazy networkTopology getter throws once the controller is stopped rather
  than starting a service nothing will shut down again; the WS opt-in bails
  during shutdown so a late request still gets its reply.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 30, 2026 14:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

packages/ws-controller/src/controller/NetworkTopologyService.ts:52

  • THREAD_REFRESH_PATHS is missing 0/53/4 (ThreadNetworkDiagnostics ExtendedPanId), but the topology build uses getThreadExtendedPanId() to populate ext_pan_id and for Thread edge/unknown resolution. A get_network_topology refresh can therefore rebuild from stale ext_pan_id values after a node re-attaches to a different Thread network.
 * Thread neighbor + route tables, interface list, own RLOC16, routing role and network name —
 * the inputs to Thread edge derivation and to the built node. RLOC16 (0/53/64) is included
 * because edge/unknown matching falls back to it when extended-address matching fails, and it
 * changes — like the routing role (0/53/1) — when a device re-attaches to the mesh.
 */
export const THREAD_REFRESH_PATHS = ["0/53/7", "0/53/8", "0/51/0", "0/53/64", "0/53/1", "0/53/2"];
/** WiFi BSSID + channel + RSSI — the inputs to the WiFi star. */

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

- rssiToStrength: an unmeasured RSSI maps to "none" (no measurement) instead of
  a synthetic "medium" a consumer couldn't tell from a real medium link; the
  Wi-Fi connection then carries no source_to_target direction detail
- THREAD_REFRESH_PATHS: add ExtendedPanId (0/53/4) so a refresh re-reads it —
  it changes when a node re-attaches to a different Thread network and feeds
  ext_pan_id and network resolution
Copilot AI review requested due to automatic review settings July 30, 2026 15:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

packages/ws-controller/src/controller/NetworkTopologyService.ts:291

  • refresh() can resolve and clear #refreshInFlight as soon as the overall deadline elapses, even though the read fan-out started by runWithConcurrency(...) is still running in the background (because #runWithDeadline uses Promise.race). This means a second refresh() call can start a new fan-out while reads from the previous refresh are still in flight, multiplying radio traffic and bypassing the intended single in-flight refresh / concurrency cap.
    async refresh(): Promise<NetworkTopology> {
        if (this.#stopped) return this.#build();
        if (this.#refreshInFlight !== undefined) return this.#refreshInFlight;

        const inFlight = this.#runRefresh().finally(() => {
            if (this.#refreshInFlight === inFlight) this.#refreshInFlight = undefined;
        });
        this.#refreshInFlight = inFlight;
        return inFlight;
    }

    async #runRefresh(): Promise<NetworkTopology> {
        const tasks: Array<() => Promise<void>> = [];
        for (const node of this.#opts.listNodes()) {
            if (node.available === false) continue;
            const networkType = getNetworkType(node);
            const paths =
                networkType === "thread"
                    ? THREAD_REFRESH_PATHS
                    : networkType === "wifi"
                      ? WIFI_REFRESH_PATHS
                      : undefined;
            if (paths === undefined) continue;
            const nodeId = node.node_id;
            tasks.push(() =>
                this.#opts
                    .readAttributes(nodeId, paths)
                    .catch(err => logger.debug(`refresh read failed node=${nodeId}`, err)),
            );
        }

        await this.#runWithDeadline(tasks);

        // stop() may have landed while awaiting the reads; a stopped service must not emit.
        const topology = this.#build();
        if (!this.#stopped) this.#emitIfChanged(topology);
        return topology;
    }

    /** Cancel timers and unsubscribe. Idempotent. */
    stop(): void {
        this.#stopped = true;
        this.#observers.close();
        if (this.#debounceTimer !== undefined) {
            clearTimeout(this.#debounceTimer);
            this.#debounceTimer = undefined;
        }
        if (this.#periodicTimer !== undefined) {
            clearInterval(this.#periodicTimer);
            this.#periodicTimer = undefined;
        }
    }

    #scheduleRebuild(): void {
        if (this.#stopped) return;
        if (this.#debounceTimer !== undefined) clearTimeout(this.#debounceTimer);
        this.#debounceTimer = setTimeout(() => {
            this.#debounceTimer = undefined;
            this.#rebuildAndEmit();
        }, this.#debounceMs);
        this.#debounceTimer.unref?.();
    }

    #rebuildAndEmit(): void {
        if (this.#stopped) return;
        try {
            this.#emitIfChanged(this.#build());
        } catch (err) {
            logger.warn("topology rebuild failed", err);
        }
    }

    #emitIfChanged(topology: NetworkTopology): void {
        const hash = hashTopology(topology);
        if (hash === this.#lastHash) return;
        this.#lastHash = hash;
        logger.debug(`topology changed: ${topology.nodes.length} nodes, ${topology.connections.length} connections`);
        this.events.topologyUpdated.emit(topology);
    }

    async #runWithDeadline(tasks: Array<() => Promise<void>>): Promise<void> {
        if (tasks.length === 0) return;
        let expired = false;
        let timer: NodeJS.Timeout | undefined;
        const deadline = new Promise<void>(resolve => {
            timer = setTimeout(() => {
                expired = true;
                resolve();
            }, this.#refreshTimeoutMs);
            timer.unref?.();
        });
        try {
            await Promise.race([runWithConcurrency(tasks, this.#refreshConcurrency, () => expired), deadline]);
        } finally {
            if (timer !== undefined) clearTimeout(timer);
        }
    }

…link

"none" is the dead-link level: mapThreadPair drops Thread pairs whose every
observed direction is "none", and consumers treat it the same way (the dashboard
filters on signalLevel !== "none" to decide a link is live). Reporting a Wi-Fi
station with an unreadable RSSI as "none" therefore hides a live station from its
AP. TopologyStrength gains "unknown" for "no measurement, link presumed up";
schema 13 is unreleased, and the Python client models strength as an open string
set, so neither side breaks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 30, 2026 16:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

packages/ws-controller/src/server/WebSocketControllerHandler.ts:103

  • get_network_topology responses can be large (potentially hundreds of nodes/edges). Right now the debug log prints the full result payload for this command, which can significantly bloat logs and add overhead. Consider skipping message-content logging for get_network_topology, similar to start_listening, and only log the command + message id.
// Issuing this (schema 13) opts the connection in to `network_topology_updated`, mirroring the
// thread-diagnostics opt-in: pre-schema-13 clients never subscribed, so they must not receive it.
const NETWORK_TOPOLOGY_OPT_IN_COMMANDS = new Set(["get_network_topology"]);

const skipMessageContentInLogFor = ["start_listening"];

The full topology graph (hundreds of nodes/edges) bloats the debug log with no
diagnostic value, like the start_listening node dump already does.
Copilot AI review requested due to automatic review settings July 31, 2026 05:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

Suppressed comments (1)

packages/ws-controller/src/server/WebSocketControllerHandler.ts:598

  • ensureTopologyObserver() can throw (e.g. MatterController.networkTopology throws once the controller is stopped). Because it runs inside the message handler before sendReliable(...), an exception here would prevent the server from sending the response frame (including an error response) and could surface as an unhandled request failure on the client. Wrap the opt-in in a try/catch so the response is always sent even if enabling the observer fails.
                            if (reqTopology) {
                                ensureTopologyObserver();
                            }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants