Skip to content

feat(multiplexed-ws): implement multiplexed ws for compass-web COMPASS-10330#7948

Open
mabaasit wants to merge 34 commits intomainfrom
add-feature-flag-for-new-ccs
Open

feat(multiplexed-ws): implement multiplexed ws for compass-web COMPASS-10330#7948
mabaasit wants to merge 34 commits intomainfrom
add-feature-flag-for-new-ccs

Conversation

@mabaasit
Copy link
Copy Markdown
Collaborator

@mabaasit mabaasit commented Apr 6, 2026

Description

Checklist

  • New tests and/or benchmarks are included
  • Documentation is changed or added
  • If this change updates the UI, screenshots/videos are added and a design review is requested
  • If this change could impact the load on the MongoDB cluster, please describe the expected and worst case impact
  • I have signed the MongoDB Contributor License Agreement (https://www.mongodb.com/legal/contributor-agreement)

Motivation and Context

  • Bugfix
  • New feature
  • Dependency update
  • Misc

Open Questions

Dependents

Types of changes

  • Backport Needed
  • Patch (non-breaking change which fixes an issue)
  • Minor (non-breaking change which adds functionality)
  • Major (fix or feature that would cause existing functionality to change)

Copy link
Copy Markdown
Collaborator

@nbbeeken nbbeeken left a comment

Choose a reason for hiding this comment

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

Great work! I think my comments are mostly in the direction of simplifying things a bit. Appreciate your approach here 🎉

Comment thread packages/atlas-service/src/atlas-service.ts
Comment thread packages/atlas-service/src/util.ts Outdated
Comment thread packages/compass-web/src/multiplex-ws-transport.ts Outdated
Comment thread packages/compass-web/src/multiplex-ws-transport.ts Outdated
Comment thread packages/compass-web/src/multiplex-ws-transport.ts Outdated
Comment thread packages/compass-web/src/multiplex-ws-transport.ts Outdated
Comment thread packages/compass-web/src/multiplex-ws-transport.ts Outdated
Comment thread packages/compass-web/src/multiplex-ws-transport.ts Outdated
Comment thread packages/compass-web/src/multiplex-ws-transport.ts
Comment thread packages/compass-web/src/multiplex-ws-transport.ts Outdated
Copilot AI review requested due to automatic review settings April 9, 2026 19:40
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Introduces a shared, multiplexed WebSocket transport for Compass Web so multiple MongoDB driver “TCP” streams can be tunneled through a single browser WebSocket, gated behind a new feature flag and Atlas-configured endpoint.

Changes:

  • Add MultiplexWebSocketTransport (BSON-framed) plus unit tests.
  • Wire up a global singleton transport in compass-web and route net/tls polyfills through it when enabled.
  • Add preferences feature flag + Atlas service configuration and endpoint builder for the multiplexed WS URL.

Reviewed changes

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

Show a summary per file
File Description
packages/compass-web/webpack.config.js Adds tls alias polyfill and removes tls from externals.
packages/compass-web/src/multiplex-ws-transport.ts Implements shared WS transport + framing + reconnect logic + singleton getter/setter.
packages/compass-web/src/multiplex-ws-transport.spec.ts Adds unit tests for framing, routing, reconnect, singleton behavior, URL building.
packages/compass-web/src/index.tsx Exposes multiplex transport API from package entry.
packages/compass-web/src/entrypoint.tsx Installs transport provider gated by preference flag and Atlas endpoint.
packages/compass-web/sandbox/index.tsx Enables the new flag in sandbox for local testing.
packages/compass-web/polyfills/tls/index.ts Updates tls polyfill to delegate to net polyfill with TLS semantics.
packages/compass-web/polyfills/net/index.ts Adds multiplex-aware path in net.Socket polyfill.
packages/compass-preferences-model/src/feature-flags.ts Defines enableMultiplexWebSocketOnWeb feature flag.
packages/atlas-service/src/util.ts Adds multiplexedWsBaseUrl to Atlas service config presets.
packages/atlas-service/src/atlas-service.ts Adds multiplexWebsocketEndpoint(projectId) URL helper.
Comments suppressed due to low confidence (2)

packages/compass-web/polyfills/net/index.ts:80

  • The fallback URL http://localhost:1337 is not a valid WebSocket URL for the browser WebSocket constructor (it expects ws:// or wss://). This will throw if lookup() doesn't provide wsURL. Consider changing the default to ws://localhost:1337 (or deriving it from window.location).
    const { wsURL, ...atlasOptions } =
      lookup?.() ?? ({} as { wsURL?: string; clusterName?: string });
    this._ws = new WebSocket(wsURL ?? 'http://localhost:1337');
    this._ws.binaryType = 'arraybuffer';
    this._ws.addEventListener(

packages/compass-web/polyfills/net/index.ts:17

  • The module-level comment says this net.Socket polyfill is "only used when running compass-web in a local sandbox", but this file now contains the production multiplex transport path as well. Please update the comment to reflect the current usage to avoid misleading future changes.
/**
 * net.Socket interface that works over the WebSocket connection. For now, only
 * used when running compass-web in a local sandbox, mms has their own
 * implementation
 */

Comment on lines +201 to +208
this.connectPromise = null;
setTimeout(() => {
if (this.closed) return;
this.connectPromise = new Promise<void>((resolve, reject) => {
this.connectResolve = resolve;
this.connectReject = reject;
this.openWebSocket();
});
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

In the retryable-close branch, connectPromise is set to null and a new promise is created later, but the original connect() promise (if someone is currently awaiting it) is never resolved/rejected. If the first connection attempt closes before it reaches the open event, callers can hang forever. Consider keeping a single pending promise across retries (reusing the same resolve/reject), or explicitly rejecting the in-flight promise before scheduling a retry and returning a new promise from connect().

Suggested change
this.connectPromise = null;
setTimeout(() => {
if (this.closed) return;
this.connectPromise = new Promise<void>((resolve, reject) => {
this.connectResolve = resolve;
this.connectReject = reject;
this.openWebSocket();
});
if (this.connectResolve === null && this.connectReject === null) {
this.connectPromise = new Promise<void>((resolve, reject) => {
this.connectResolve = resolve;
this.connectReject = reject;
});
}
setTimeout(() => {
if (this.closed) return;
this.openWebSocket();

Copilot uses AI. Check for mistakes.
Comment thread packages/compass-web/src/entrypoint.tsx
Comment on lines +62 to +66
// The websocket is connected already and we will emit the connect
// event so that driver sends the first message to the server.
setTimeout(() => {
this.emit(options.tls ? 'secureConnect' : 'connect');
});
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

This code emits the socket 'connect'/'secureConnect' event immediately after registering with the multiplex transport, but it never waits for transport.connect() to complete. If the shared WebSocket is still connecting, early driver writes will be dropped by sendRaw(). Consider awaiting transport.connect() before emitting 'connect' (and/or buffering writes until the shared WebSocket is OPEN).

Suggested change
// The websocket is connected already and we will emit the connect
// event so that driver sends the first message to the server.
setTimeout(() => {
this.emit(options.tls ? 'secureConnect' : 'connect');
});
void transport
.connect()
.then(() => {
setTimeout(() => {
this.emit(options.tls ? 'secureConnect' : 'connect');
});
})
.catch((err: Error) => {
this._teardown();
setTimeout(() => this.emit('error', err));
});

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yeah its a good point. However, we are implementing multiplexing in such a way that we have established the link first and the users will be able to interact (to trigger connections)

Comment thread packages/compass-web/sandbox/index.tsx Outdated
Comment on lines +69 to +70
/** Configured in webpack.config.js when start the multiplex ws proxy */
multiplexedWsBaseUrl="ws://localhost:1338"
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.

Any way we can avoid having this an interface on compass-web at all?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, I've been thinking about this as well. Webpack we can not use as we use it for serving atlas-${env}s. I also want to avoid using lookup we have for current ccs. Its something that I'd need to handle in this file directly. I'll look into it

setMultiplexTransport(null);
};
}, [enableMultiplexWebSocketOnWeb, ccsUrl, logger]);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Should we show a loading view here till we establish the connection? We can also force users to either refresh here (if it fails to connect), and show a proper message (instead of toast). Thoughts.

Comment thread packages/compass-web/src/multiplex-ws-transport.ts Outdated
Comment thread packages/compass-web/src/multiplex-ws-transport.ts Outdated
Comment thread packages/compass-web/src/multiplex-ws-transport.ts Outdated
Comment on lines +62 to +66
// The websocket is connected already and we will emit the connect
// event so that driver sends the first message to the server.
setTimeout(() => {
this.emit(options.tls ? 'secureConnect' : 'connect');
});
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yeah its a good point. However, we are implementing multiplexing in such a way that we have established the link first and the users will be able to interact (to trigger connections)

Comment thread packages/compass-web/src/multiplex-ws-transport.ts Outdated
Comment thread packages/atlas-service/src/util.ts Outdated
@mabaasit mabaasit changed the title multiplexed ws for ccs feat(multiplexed-ws): implement multiplexed ws for compass-web COMPASS-10330 Apr 14, 2026
@github-actions github-actions bot added the feat label Apr 14, 2026
@mabaasit mabaasit added the no release notes Fix or feature not for release notes label Apr 14, 2026
@mabaasit mabaasit marked this pull request as ready for review April 14, 2026 17:41
@mabaasit mabaasit requested a review from a team as a code owner April 14, 2026 17:41
@mabaasit mabaasit requested a review from paula-stacho April 14, 2026 17:41
Comment thread packages/atlas-service/src/atlas-service.ts Outdated
Comment thread packages/compass-web/scripts/multiplex-ws-proxy.js Outdated
Comment thread packages/compass-web/src/entrypoint.tsx Outdated
Comment thread packages/compass-web/src/entrypoint.tsx Outdated
Comment thread packages/compass-web/src/entrypoint.tsx Outdated
Comment on lines +173 to +180
signal?.addEventListener(
'abort',
(event) => {
const reason = (event.target as AbortSignal).reason;
this.connectReject?.(reason ?? new Error('Connection aborted.'));
},
{ once: true }
);
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.

if the promise succeeds or fails for a reason other than the signal then nothing appears to unsubscribe this listener currently. If the signal is reused a bunch its going to hang on to references to each "this" that the listener is supposed to call this.connectReject on. Maybe another usage of var abortListener = addEventListener(signal, ...) and connectPromise.finally(() => dispose())

disposableStack.use(
addEventListener(ws, 'message', this.onMessage.bind(this))
);
disposableStack.defer(() => this.close.bind(this)('Transport Disposed'));
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.

Suggested change
disposableStack.defer(() => this.close.bind(this)('Transport Disposed'));
disposableStack.defer(() => this.close('Transport Disposed'));

Because its an arrow function the bind is redundant

If you want to use bind:

disposableStack.defer(this.close.bind(this, 'Transport Disposed'));

Comment on lines +167 to +171
this.connectPromise = new Promise<void>((resolve, reject) => {
this.connectResolve = resolve;
this.connectReject = reject;
this.openWebSocket();
});
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.

Suggested change
this.connectPromise = new Promise<void>((resolve, reject) => {
this.connectResolve = resolve;
this.connectReject = reject;
this.openWebSocket();
});
const { resolve, reject, promise } = this.connectPromise = Promise.withResolvers<void>();
this.connectPromise = promise;
this.connectResolve = resolve;
this.connectReject = reject;
this.openWebSocket();

This is a total opinion, feel free to ignore the total suggestion but if we could at least move this.openWebSocket(); out of the scope of the promise (now that this function is proper async that's a safe change) it makes it clearer the order of operations to the next reader who may not be as familiar with what the promise ctor is doing here.

feel free to ignore even that second part, I'm nit picking. Flatter code (indentation) just reads easier

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I agree it looks better. For now I am leaving it as is (only because we are not using it yet in compass), but as I am doing a follow up on this PR, I'll try to address it there.

sourceAddress,
}: MultiplexWebSocketTransportOptions) {
this.baseUrl = _wsUrlOverride ?? baseUrl ?? 'ws://localhost:1338';
this.sourceAddress = sourceAddress ?? 'localhost';
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.

Just noticed this as I was playing around in the hex dump of the protocol, one of the things I did over on the sample client was to generate a host per "transport" that would make it increment if the websocket needed rebuilding, which can be helpful for debugging. I have the code there: https://github.com/10gen/mms/blob/dcd38ad75940797ad2b367fda194d7dad177cf83/systems/data-explorer/cluster-connection/test/utils/mongodb/tls.mts#L14-L28 optional idea, we can always revisit. This value for now is unused for any functional purpose.

* Manages a single shared WebSocket connection for a browser tab and multiplexes
* all MongoDB driver TCP connections over it using BSON 5-tuple framing.
*/
export class MultiplexWebSocketTransport implements Disposable {
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.

This is minor but carries some intention, in the tech design and sample client I use the terminology "link" intentionally. It's just alluding to the idea that this websocket is our simulation of the link layer of the driver's networking. That's why we can establish it externally from driver connections now, previously the WebSockets served as pretend TCP connections or transport layer. I really don't feel strongly about this, so feel free to leave it, just wanted to share that thinking.

Comment thread packages/atlas-service/src/atlas-service.ts Outdated
Comment thread packages/compass-web/scripts/multiplex-ws-proxy.js Outdated
Comment thread packages/compass-web/src/types.d.ts Outdated
Comment thread packages/compass-web/package.json Outdated
"buffer": "^6.0.3",
"chai": "^4.3.6",
"compass-preferences-model": "^2.75.0",
"core-js": "^3.49.0",
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.

Can you explain this? core-js is a dependency of shared webpack config and it's important we stay on the same version in the repo

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Can you explain this?

Explain why a different version or this dep?
Version - I did not know that we are using it in webpack config. I'll align the versions.
Dependency - DisposableStack we are using to clean up, is not supported in Safari.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

(we discussed this and we are already doing this stuff in webpack config). I'll drop this from here, add support for types and also bump core-js in webpack-config.

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.

Does safari work with this change?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

With c1b4b03, it does now.

Comment thread packages/compass-web/webpack.config.js Outdated
@semgrep-code-mongodb
Copy link
Copy Markdown

Semgrep found 1 run-shell-injection finding:

  • .github/workflows/update-dependencies.yaml

Using variable interpolation ${{...}} with github context data in a run: step could allow an attacker to inject their own code into the runner. This would allow them to steal secrets and code. github context data can have arbitrary user input and should be treated as untrusted. Instead, use an intermediate environment variable with env: to store the data and use the environment variable in the run: script. Be sure to use double-quotes the environment variable, like this: "$ENVVAR".

🛟 Help? Slack #semgrep-help or go/semgrep-help.

Resolution Options:

  • Fix the code
  • Reply /fp $reason (if security gap doesn’t exist)
  • Reply /ar $reason (if gap is valid but intentional; add mitigations/monitoring)
  • Reply /other $reason (e.g., test-only)

@mabaasit
Copy link
Copy Markdown
Collaborator Author

mabaasit commented Apr 16, 2026

Things to follow up on:

  1. Clean net/index.ts to drop support for non-multiplexed websockets
  2. MultiplexWebSocketTransport -> Link
  3. Different sourceAddress (new everything a new transport is created and from localhost to a single character. set this everything we create a new WS instance)
  4. Use Promise.withResolvers

@mabaasit mabaasit requested review from gribnoysup and nbbeeken April 16, 2026 09:50
@mabaasit
Copy link
Copy Markdown
Collaborator Author

Semgrep found 1 run-shell-injection finding:

Addressed in 97b8b21

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

Labels

feat no release notes Fix or feature not for release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants