Skip to content

Feat/mulitple data channel(llm) connections#4661

Merged
JudyZhuHz merged 72 commits intowebex:nextfrom
mickelr:feat/mulitLLMConnectionsWithShutdown
Mar 13, 2026
Merged

Feat/mulitple data channel(llm) connections#4661
JudyZhuHz merged 72 commits intowebex:nextfrom
mickelr:feat/mulitLLMConnectionsWithShutdown

Conversation

@mickelr
Copy link
Copy Markdown
Contributor

@mickelr mickelr commented Jan 20, 2026

COMPLETES #< INSERT LINK TO ISSUE >

https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-697743

This pull request addresses

To support the 5k webinar, client needs to keep the main LLM connection after entring practice session with practice session LLM connection. So client needs to maintain more than one LLM connections in a single meeting. Currently the llm and mercury lib only support one connection at same time. We need a way to implement multiple data channel (LLM) connections at same time.

by making the following changes

Consider the backward compatibility, I use a map to manage mulitple socket connections with different session ids. If no session id passed when connect/disconnect, we will use default session id by which the lib can be compatibility with previous caller.

Change Type

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Tooling change
  • Internal code refactor

The following scenarios were tested

Join meeting, check CC, reaction, whiteboard feature can work normally

The GAI Coding Policy And Copyright Annotation Best Practices

  • GAI was not used (or, no additional notation is required)
  • Code was generated entirely by GAI
  • GAI was used to create a draft that was subsequently customized or modified
  • Coder created a draft manually that was non-substantively modified by GAI (e.g., refactoring was performed by GAI on manually written code)
  • Tool used for AI assistance (GitHub Copilot / Other - specify)
    • Github Copilot
    • Other - Please Specify
  • This PR is related to
    • Feature
    • Defect fix
    • Tech Debt
    • Automation

I certified that

  • I have read and followed contributing guidelines
  • I discussed changes with code owners prior to submitting this pull request
  • I have not skipped any automated checks
  • All existing and new tests passed
  • I have updated the documentation accordingly

Make sure to have followed the contributing guidelines before submitting.

mickelr and others added 30 commits September 10, 2025 16:52
…onsWithShutdown

# Conflicts:
#	packages/@webex/internal-plugin-mercury/src/mercury.js
…down' into feat/mulitLLMConnectionsWithShutdown
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0045311f81

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +367 to +369
for (const sessionId of this.sockets.keys()) {
disconnectPromises.push(this.disconnect(options, sessionId));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Abort in-flight connects when disconnecting all sessions

disconnectAll() only iterates this.sockets.keys(), but connect() now tracks in-flight attempts in _connectPromises before any socket exists (during device.register() and early backoff setup). If logout()/disconnectAll() is called in that window, the session is skipped here and the underlying connect attempt continues, so a socket can still open after the caller believes all sessions were disconnected.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@rarajes2 rarajes2 left a comment

Choose a reason for hiding this comment

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

  • Have reviewed the Mercury plugin. will do another round of review for llm plugin.
  • Let's update the README.md for both the llm and mercury plugins with old and new ways to consume the connections.
  • If possible please add a vidcast showcasing the working of existing features such as meeting/calling as this is affecting a lot of packages

* @returns {boolean} True if at least one socket is connected
*/
hasConnectedSockets() {
const socket = this.sockets.get(this.defaultSessionId);
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.

  • Can we also add support for the provided sessionId and default/fallback could be defaultSessionId ?
  • We can simply return the socket?.connected

Comment thread packages/@webex/internal-plugin-mercury/src/mercury.js Outdated
},

// @oneFlight
connect(webSocketUrl, sessionId = this.defaultSessionId) {
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.

  • Please add js-doc for this method
  • Does this sessionId need to be generated and maintained by Consumer application? See if we can generate the sessionId internally and return the connection object to send and listen for the messages to/from backend where the webSocketUrl will be passed manually from the Application. The default listening on the webex.internal.mercury.on() should work as expected for the usual connection

Copy link
Copy Markdown
Contributor Author

@mickelr mickelr Mar 8, 2026

Choose a reason for hiding this comment

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

Yes we can generate the sessionId internally if Consumer application tell us which data channel it intend to connect. We can improve this in later PR

return this._connectWithBackoff(webSocketUrl, sessionId);
})
.finally(() => {
this._connectPromises.delete(sessionId);
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.

Q. Why are we deleting the created promise here ? Shouldn't this be deleted on disconnect ?

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.

_connectPromises is an internal map (keyed by sessionId) that caches the in-flight “connect” Promise for each Mercury session.
Functionally it is used to:
De-duplicate concurrent connect calls: if connect() (or the internal connect path) is called multiple times for the same sessionId while a connection attempt is already running, the code can return the existing promise from _connectPromises instead of starting another socket open/backoff cycle.
Provide a single completion signal per session: all callers awaiting a connect for that sessionId resolve/reject from the same promise, so state (connected, connecting, socket refs) stays consistent.
Allow cleanup/reset: once the connect attempt finishes (success, permanent failure, or abort/disconnect), the entry should be removed/cleared so a future connect starts a fresh attempt rather than reusing a settled promise.
In short: _connectPromises is the per-session “only one connect attempt at a time” mechanism.

Comment thread packages/@webex/internal-plugin-mercury/src/mercury.js Outdated
Comment on lines 489 to 504
@@ -361,8 +498,8 @@ const Mercury = WebexPlugin.extend({
return Promise.reject(err);
}

if (!isShutdownSwitchover && !this.backoffCall) {
const msg = `${this.namespace}: prevent socket open when backoffCall no longer defined`;
if (!isShutdownSwitchover && !backoffCall) {
const msg = `${this.namespace}: prevent socket open when backoffCall no longer defined for ${sessionId}`;
const err = new Error(msg);

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.

Image

Same condition is repeated inside single function

Copy link
Copy Markdown
Contributor Author

@mickelr mickelr Mar 8, 2026

Choose a reason for hiding this comment

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

Not same, but can simplify it

Comment thread packages/@webex/internal-plugin-mercury/src/socket/socket-base.js Outdated
@mickelr mickelr requested a review from rarajes2 March 8, 2026 13:08
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eef7a6d14c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

this.onmessage = this.onmessage.bind(this);
this.onclose = this.onclose.bind(this);
// Increase max listeners to avoid memory leak warning in tests
this.setMaxListeners(5);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Increase Socket max listeners above connection baseline

Setting this.setMaxListeners(5) is lower than the number of listeners a normal connection adds: Mercury attaches five listeners in _attachSocketEventListeners() (close, message, pong, sequence-mismatch, ping-pong-latency), and Socket._authorize() adds another message listener with once(). That means each connection now crosses the limit and emits MaxListenersExceededWarning during normal operation, creating noisy diagnostics and masking real leak signals.

Useful? React with 👍 / 👎.

@mickelr
Copy link
Copy Markdown
Contributor Author

mickelr commented Mar 9, 2026

Copy link
Copy Markdown
Contributor

@rarajes2 rarajes2 left a comment

Choose a reason for hiding this comment

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

For calling we should test with the web.webex.com/calling?calling url. The one shown in the vidcast uses the meeting itself

Comment on lines +36 to +50
```js
const mercury = webex.internal.mercury;

// Default session
await mercury.connect();

// Additional session
await mercury.connect(undefined, 'secondary-session');

// Disconnect only one session
await mercury.disconnect(undefined, 'secondary-session');

// Disconnect everything
await mercury.disconnectAll();
```
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.

Would be nice to have sample code for how to listen to multiple connections

await mercury.connect();

// Additional session
await mercury.connect(undefined, 'secondary-session');
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.

Q. Instead of undefined we can pass a custom wss url which will be able to connect to any wss server, right?

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.

Right

await webex.internal.llm.disconnectLLM({code: 1000, reason: 'done'}, 'session-a');

// Disconnect all sessions
await webex.internal.llm.disconnectAllLLM({code: 1000, reason: 'shutdown'});
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.

Same here, it will be nice to have listener logic documented as well

Comment on lines +23 to +28
const SOCKET_READY_STATE = Object.freeze({
CONNECTING: 0,
OPEN: 1,
CLOSING: 2,
CLOSED: 3,
});
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.

Should we create a constants.ts file for consistency ?

Comment on lines +43 to +44
export default // @ts-ignore
// @ts-ignore
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.

@ts-ignore is repeated and seems bit off

* @returns {Promise<void>}
*/
public disconnectAllLLM = (options?: {code: number; reason: string}): Promise<void> =>
this.disconnectAll(options).then(() => {
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.

Q. Is this coming from Mercury plugin ?

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.

meeting-plugin will call the interface

@mickelr
Copy link
Copy Markdown
Contributor Author

mickelr commented Mar 12, 2026

https://app.vidcast.io/share/9052d53e-0a6c-4970-a21d-8ddf71fa8ff3
vidcast for calling

@mickelr mickelr requested a review from rarajes2 March 12, 2026 05:52
Copy link
Copy Markdown
Contributor

@rarajes2 rarajes2 left a comment

Choose a reason for hiding this comment

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

Approving the PR. Did a sanity test on the calling sdk. It seems to be working fine with the updated changes in the socket plugins.

Please check one last comment regarding connected state and take a call on how do we want to handle it.

resolve();

// Update overall connected status
this.connected = this.hasConnectedSockets();
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.

Suppose we have multiple connections alive and if the default connection is closed but other connections are still intact, in that case we are marking here that overall connection is lost. This feels bit misleading.

Should we maintain a map of session IDs including the default one with the connected property and based on that every connection can be decided to be connected or disconnected ?

This change will need to be updated with for the derived ampersand property as well. Shown below:

  derived: {
    listening: {
      deps: ['connected'],
      fn() {
        return this.connected;
      },
    },
  },

Please discuss this with your team and take a call on this.

@JudyZhuHz JudyZhuHz merged commit 12d4fd5 into webex:next Mar 13, 2026
12 checks passed
@github-actions
Copy link
Copy Markdown

🎉 Your changes are now available!

Released in: v3.11.0-next.52
Packages updated: @webex/internal-plugin-llm, @webex/internal-plugin-mercury

📖 View full changelog →

Thank you for your contribution!


🤖 This is an automated message. For questions, please refer to the documentation.

fnowakow pushed a commit to fnowakow/webex-js-sdk that referenced this pull request Mar 20, 2026
Co-authored-by: Shreyas Sharma <72344404+Shreyas281299@users.noreply.github.com>
vamshigovardhana pushed a commit to vamshigovardhana/webex-js-sdk that referenced this pull request Apr 1, 2026
Co-authored-by: Shreyas Sharma <72344404+Shreyas281299@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

validated If the pull request is validated for automation.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants