Skip to content

frontend: add Zenoh singleton#3820

Merged
patrickelectric merged 2 commits intobluerobotics:masterfrom
nicoschmdt:zenoh-singleton-frontend
Mar 10, 2026
Merged

frontend: add Zenoh singleton#3820
patrickelectric merged 2 commits intobluerobotics:masterfrom
nicoschmdt:zenoh-singleton-frontend

Conversation

@nicoschmdt
Copy link
Copy Markdown
Contributor

@nicoschmdt nicoschmdt commented Mar 5, 2026

Summary by Sourcery

Introduce a shared Zenoh session manager and update frontend components to use it instead of creating and closing their own sessions.

New Features:

  • Add a ZenohManager singleton that provides a shared Zenoh WebSocket URL helper and session promise for the frontend.

Enhancements:

  • Refactor cloud file sync, Zenoh inspector, Zenoh network view, and console logger to obtain Zenoh sessions via the shared manager and stop closing sessions locally to reuse the shared connection.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Mar 5, 2026

Reviewer's Guide

Introduce a shared Zenoh session manager singleton and refactor several components to obtain sessions through it instead of creating and closing their own websocket connections, simplifying connection logic and avoiding duplicate sessions.

Sequence diagram for shared Zenoh session acquisition via singleton

sequenceDiagram

participant CloudTrayMenu
participant ZenohNetwork
participant ZenohManager
participant ZenohSession

CloudTrayMenu->>ZenohManager: getSession()
alt sessionPromise is null
  ZenohManager->>ZenohManager: getWebsocketUrl()
  ZenohManager->>ZenohManager: create Config(url)
  ZenohManager->>ZenohSession: Session.open(config)
  ZenohSession-->>ZenohManager: Session
  ZenohManager->>ZenohManager: store Promise_Session_
else sessionPromise exists
  ZenohManager->>ZenohManager: reuse existing Promise_Session_
end
ZenohManager-->>CloudTrayMenu: Promise_Session_

ZenohNetwork->>ZenohManager: getSession()
ZenohManager-->>ZenohNetwork: same Promise_Session_

CloudTrayMenu->>CloudTrayMenu: await getSession()
CloudTrayMenu->>CloudTrayMenu: use session for file sync

ZenohNetwork->>ZenohNetwork: await getSession()
ZenohNetwork->>ZenohNetwork: use session for topology discovery
Loading

Class diagram for ZenohManager singleton and consumers

classDiagram

class ZenohManager {
  -instance ZenohManager
  -sessionPromise Promise_Session_
  -ZenohManager()
  +getInstance() ZenohManager
  +getWebsocketUrl() string
  +getSession() Promise_Session_
}

class CloudTrayMenu {
  -file_sync_session Session
  +setupFileSync() Promise_boolean_
  +disconnectCloud() Promise_void_
}

class ZenohNetwork {
  -session Session
  +setupZenoh() Promise_void_
  +refreshNetwork() Promise_void_
  +disconnectZenoh() Promise_void_
}

class ConsoleLogger {
  -session Session
  +init() Promise_void_
  +shutdown() Promise_void_
}

class ZenohInspector {
  -session Session
  -subscriber Subscriber
  -liveliness_subscriber Subscriber
  +setupZenoh() Promise_void_
  +disconnectZenoh() Promise_void_
}

ZenohManager <.. CloudTrayMenu : uses_getSession
ZenohManager <.. ZenohNetwork : uses_getSession
ZenohManager <.. ConsoleLogger : uses_getSession
ZenohManager <.. ZenohInspector : uses_getSession
Loading

File-Level Changes

Change Details Files
Introduce a ZenohManager singleton that lazily opens and caches a single zenoh-ts Session and exposes it via a getSession() helper used across the frontend.
  • Create ZenohManager class with a private static instance and sessionPromise fields to cache a single Session.open() call.
  • Add getWebsocketUrl() helper to centralize websocket URL construction based on window.location.
  • Expose a default zenoh instance from the new library for consumers to call zenoh.getSession().
core/frontend/src/libs/zenoh/index.ts
Refactor CloudTrayMenu to obtain the file sync Zenoh session from the shared zenoh singleton instead of managing its own connection lifecycle and promise state.
  • Remove direct usage of Config and Session.open in favor of calling zenoh.getSession() when establishing file_sync_session.
  • Delete the file_sync_session_promise state and the associated logic for tracking in-progress session creation.
  • Simplify cleanup to only null out file_sync_session without explicitly closing the underlying session, leaving lifecycle to the singleton.
core/frontend/src/components/cloud/CloudTrayMenu.vue
Refactor ZenohNetwork inspector component to reuse the shared zenoh session and simplify connect/disconnect logic.
  • Replace manual websocket URL construction and Session.open with zenoh.getSession() in setupZenoh().
  • Remove explicit session.close() calls on refresh and disconnect, instead just clearing the local session reference.
  • Keep topology discovery and graph update logic intact while relying on the singleton for connection management.
core/frontend/src/components/zenoh-inspector/ZenohNetwork.vue
Update ConsoleLogger to use the shared Zenoh session singleton for publishing logs and simplify teardown.
  • Replace local Config/Session.open code with zenoh.getSession() when enabling logging.
  • Remove explicit session.close() and only clear the local session reference during disable(), keeping console/window interception cleanup unchanged.
core/frontend/src/libs/console-logger.ts
Refactor ZenohInspector component to get its Zenoh session from the singleton and rely on it for connection lifecycle while preserving message subscription logic.
  • Replace direct Config and Session.open usage with zenoh.getSession() inside setupZenoh().
  • Remove explicit session.close() from disconnectZenoh(), instead undeclaring subscribers and nulling local references.
  • Leave subscription setup for regular and liveliness subscribers unchanged aside from the session acquisition method.
core/frontend/src/components/zenoh-inspector/ZenohInspector.vue

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • The ZenohManager never closes the underlying Session and doesn’t expose any teardown/reset, so components that previously closed their sessions now leak a long‑lived connection; consider adding a close()/reset() API and using it from callers that explicitly disconnect.
  • If Session.open rejects, sessionPromise remains a rejected promise and all subsequent getSession() calls will keep failing; it would be safer to catch failures and clear sessionPromise so a later retry can succeed.
  • ZenohManager.getWebsocketUrl() directly accesses window, which can break SSR/tests; consider guarding for non‑browser environments or injecting the URL from the outside when needed.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The ZenohManager never closes the underlying Session and doesn’t expose any teardown/reset, so components that previously closed their sessions now leak a long‑lived connection; consider adding a `close()`/`reset()` API and using it from callers that explicitly disconnect.
- If `Session.open` rejects, `sessionPromise` remains a rejected promise and all subsequent `getSession()` calls will keep failing; it would be safer to catch failures and clear `sessionPromise` so a later retry can succeed.
- `ZenohManager.getWebsocketUrl()` directly accesses `window`, which can break SSR/tests; consider guarding for non‑browser environments or injecting the URL from the outside when needed.

## Individual Comments

### Comment 1
<location path="core/frontend/src/libs/zenoh/index.ts" line_range="6-15" />
<code_context>
+class ZenohManager {
+  private static instance: ZenohManager
+
+  private sessionPromise: Promise<Session> | null = null
+
+  private constructor() {}
+
+  public static getInstance(): ZenohManager {
+    if (!ZenohManager.instance) {
+      ZenohManager.instance = new ZenohManager()
+    }
+    return ZenohManager.instance
+  }
+
+  public static getWebsocketUrl(): string {
+    const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
+    return `${protocol}://${window.location.host}/zenoh-api/`
+  }
+
+  public getSession(): Promise<Session> {
+    if (!this.sessionPromise) {
+      const url = ZenohManager.getWebsocketUrl()
+      const config = new Config(url)
+      this.sessionPromise = Session.open(config)
+    }
+    return this.sessionPromise
+  }
+}
</code_context>
<issue_to_address>
**issue (bug_risk):** Handle failed Session.open calls so a rejected promise isn't cached forever

Because `sessionPromise` is never cleared, a rejected `Session.open(config)` will be cached and every future `getSession()` call will keep returning the same rejection until page reload. Wrap `Session.open` so that on rejection you reset `this.sessionPromise` (and optionally retry) before rethrowing:

```ts
if (!this.sessionPromise) {
  const url = ZenohManager.getWebsocketUrl()
  const config = new Config(url)
  this.sessionPromise = Session.open(config)
    .catch(err => {
      this.sessionPromise = null
      throw err
    })
}
```

This preserves de-duplication while avoiding a permanent failure state.
</issue_to_address>

### Comment 2
<location path="core/frontend/src/libs/zenoh/index.ts" line_range="22-28" />
<code_context>
+    return `${protocol}://${window.location.host}/zenoh-api/`
+  }
+
+  public getSession(): Promise<Session> {
+    if (!this.sessionPromise) {
+      const url = ZenohManager.getWebsocketUrl()
+      const config = new Config(url)
+      this.sessionPromise = Session.open(config)
+    }
+    return this.sessionPromise
+  }
+}
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider exposing a close/reset mechanism for the shared Zenoh session

Call sites that used to close their own `Session` (`CloudTrayMenu`, `ZenohNetwork`, `ZenohInspector`, `ConsoleLogger`) now just drop references, so the shared session may stay open for the lifetime of the app. This removes the ability to explicitly close the websocket on teardown or to recreate a fresh session after long-lived connections degrade or the backend restarts.

Consider adding a `ZenohManager.close()` (or similar) that closes the underlying `Session` (if it exists) and resets `sessionPromise` to `null`, so components can explicitly trigger shutdown or reconnection without reintroducing per-component setup logic.

Suggested implementation:

```typescript
  public getSession(): Promise<Session> {
    if (!this.sessionPromise) {
      const url = ZenohManager.getWebsocketUrl()
      const config = new Config(url)
      this.sessionPromise = Session.open(config)
    }
    return this.sessionPromise
  }

  public async close(): Promise<void> {
    if (this.sessionPromise) {
      try {
        const session = await this.sessionPromise
        await session.close()
      } finally {
        this.sessionPromise = null
      }
    }
  }

  public static async close(): Promise<void> {
    if (!ZenohManager.instance) {
      return
    }
    await ZenohManager.instance.close()
  }

  public getSession(): Promise<Session> {
    if (!this.sessionPromise) {

```

I’ve assumed that:
1. `this.sessionPromise` is declared as `Promise<Session> | null`. If it is currently `Promise<Session>` only, update its type to be nullable.
2. `Session.close()` returns a `Promise<void>`. If it is synchronous, you can remove `await` and the `async` keywords where appropriate.
3. Call sites that need to explicitly reset the shared session should now call `await ZenohManager.close()` when tearing down or when forcing a reconnect.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@nicoschmdt nicoschmdt force-pushed the zenoh-singleton-frontend branch from 261b354 to c7d3aa9 Compare March 5, 2026 17:55
@patrickelectric patrickelectric merged commit 5dc9c93 into bluerobotics:master Mar 10, 2026
7 checks passed
@nicoschmdt nicoschmdt deleted the zenoh-singleton-frontend branch March 11, 2026 15:35
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.

2 participants