Skip to content

chore(@rocket.chat/ddp-client): introduce experimental Meteor independent DDP client (re-add)#40430

Merged
ggazzo merged 2 commits into
developfrom
fix/sdk-v2-totp-and-status-regressions
May 6, 2026
Merged

chore(@rocket.chat/ddp-client): introduce experimental Meteor independent DDP client (re-add)#40430
ggazzo merged 2 commits into
developfrom
fix/sdk-v2-totp-and-status-regressions

Conversation

@ggazzo

@ggazzo ggazzo commented May 6, 2026

Copy link
Copy Markdown
Member

Summary

Re-adds the experimental SDK-over-DDP transport originally landed in #40301, with two regression fixes found while validating the legacy (flag-off) path.

  • Re-applies the Use_RC_SDK admin setting and the @rocket.chat/ddp-client-backed transport from chore(@rocket.chat/ddp-client): introduce experimental Meteor independent DDP client #40301. Default remains off — the migration ships dormant.
  • Forwards the original DDP-encoded result frame from method.call / method.callAnon REST responses back into Meteor's _streamHandlers instead of synthesising an error: 'unknown' envelope. Without this, withAsyncTOTP no longer recognised totp-required and the 2FA modal never opened for any Meteor method protected by twoFactorRequired.
  • Restores the { ...Meteor.status() } spread in ServerProvider.getStatus on the legacy path. Meteor.status() reuses the same internal object (mutated in place), so dropping the spread made useSyncExternalStore see an unchanged snapshot and ConnectionStatusBar froze on connect/drop.

Test plan

  • With Use_RC_SDK off (default), trigger a Meteor method protected by twoFactorRequired (e.g. saveUserProfile for a user with TOTP enabled) and confirm the TOTP modal opens and the call resolves once a valid code is submitted.
  • With Use_RC_SDK off, drop the network and confirm ConnectionStatusBar flips to "waiting"/"connecting" and back to "connected" without a hard reload.
  • With Use_RC_SDK on (URL ?sdk_transport=on or admin setting), smoke test login, message send, presence, and force-logout.
    Task: ARCH-2127

Summary by CodeRabbit

  • New Features

    • Added experimental "Use Rocket.Chat SDK" admin setting (General → General Settings) to enable alternative DDP transport for real-time communications.
    • Transport selection can be controlled per-tab via URL parameter or browser storage for testing purposes.
  • Chores

    • Added SDK transport layer dependencies.

ggazzo added 2 commits May 6, 2026 18:06
…on status snapshot

Two regressions reintroduced by reapplying #40301:

- `ddpOverREST` rebuilt every REST-side error with `error: 'unknown'`,
  dropping the original DDP error code. The Rocket.Chat REST middleware
  encodes the original Meteor error inside `body.message` as a DDP `result`
  frame for `method.call` / `method.callAnon`, so forward that frame
  untouched when present. Only fall back to a synthetic frame for
  auth-middleware errors (e.g. 401) where `body.message` is a plain string.
  Without this, `withAsyncTOTP` no longer recognised `totp-required` and
  the 2FA modal never opened for any Meteor method protected by
  `twoFactorRequired`.

- `ServerProvider.getStatus` dropped the `{ ...Meteor.status() }` spread on
  the legacy path. `Meteor.status()` returns the same internal object on
  every call (mutated in place), so `useReactiveValue` /
  `useSyncExternalStore` compared snapshots by identity and stopped
  re-rendering. `ConnectionStatusBar` froze on connect/drop until a hard
  reload. Restore the spread.
@ggazzo ggazzo requested review from a team as code owners May 6, 2026 21:46
@dionisio-bot

dionisio-bot Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

Looks like this PR is not ready to merge, because of the following issues:

  • This PR is missing the 'stat: QA assured' label

Please fix the issues and try again

If you have any trouble, please check the PR guidelines

@changeset-bot

changeset-bot Bot commented May 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 57e1e2d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 42 packages
Name Type
@rocket.chat/meteor Minor
@rocket.chat/core-typings Minor
@rocket.chat/rest-typings Minor
@rocket.chat/uikit-playground Patch
@rocket.chat/api-client Patch
@rocket.chat/apps Patch
@rocket.chat/core-services Patch
@rocket.chat/cron Patch
@rocket.chat/ddp-client Patch
@rocket.chat/fuselage-ui-kit Major
@rocket.chat/gazzodown Major
@rocket.chat/http-router Patch
@rocket.chat/livechat Patch
@rocket.chat/model-typings Patch
@rocket.chat/models Patch
@rocket.chat/ui-avatar Major
@rocket.chat/ui-client Major
@rocket.chat/ui-contexts Major
@rocket.chat/ui-voip Major
@rocket.chat/web-ui-registration Major
@rocket.chat/account-service Patch
@rocket.chat/authorization-service Patch
@rocket.chat/ddp-streamer Patch
@rocket.chat/omnichannel-transcript Patch
@rocket.chat/presence-service Patch
@rocket.chat/queue-worker Patch
@rocket.chat/abac Patch
@rocket.chat/federation-matrix Patch
@rocket.chat/license Patch
@rocket.chat/media-calls Patch
@rocket.chat/omnichannel-services Patch
@rocket.chat/pdf-worker Patch
@rocket.chat/presence Patch
rocketchat-services Patch
@rocket.chat/network-broker Patch
@rocket.chat/omni-core-ee Patch
@rocket.chat/mock-providers Patch
@rocket.chat/instance-status Patch
@rocket.chat/omni-core Patch
@rocket.chat/server-fetch Patch
@rocket.chat/ui-video-conf Major
@rocket.chat/ui-composer Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The head commit changed during the review from ed8edd9 to 57e1e2d.

Walkthrough

This PR introduces an experimental SDK-over-DDP transport that routes Meteor DDP traffic through the @rocket.chat/ddp-client library via a single WebSocket, controlled by a new admin setting. The client-side implementation includes SDK wrapper with authentication, Meteor fallback, interceptors for DDP operations, and integration into existing features like presence and login flow.

Changes

SDK-over-DDP Transport

Layer / File(s) Summary
Admin Setting & Metadata
apps/meteor/server/settings/general.ts, apps/meteor/app/ui-master/server/index.ts, packages/i18n/src/locales/en.i18n.json
New Use_RC_SDK boolean admin setting in General section; server injects rc-sdk-transport-enabled meta tag into HTML head based on setting; i18n keys added for UI labels and descriptions.
Transport Detection
apps/meteor/client/lib/sdk/sdkTransportEnabled.ts
New isSdkTransportEnabled() function checks URL parameter sdk_transport, localStorage key rc-config-sdk_transport, and meta tag in that precedence order; defaults to false in non-browser environments.
SDK Core & Authentication
apps/meteor/client/lib/sdk/ddpSdk.ts, apps/meteor/client/lib/sdk/meteorBackedSdk.ts
DDPSDK singleton wrapper with token sync, login coordination, and auth error handling; Meteor-backed fallback implementation that delegates to Meteor.connection when SDK transport is disabled; exports getDdpSdk(), ensureConnectedAndAuthenticated(), adoptAccountFromMeteorLoginResult(), isAuthError(), teardownAuthenticatedConnection().
Streamer & Presence Adapters
apps/meteor/client/lib/sdk/streamerAdapter.ts, apps/meteor/app/notifications/client/lib/Presence.ts
New createDdpSdkStreamerAdapter() wires SDK DDP messages into StreamerCentral interface; Presence integration conditionally registers SDK-backed streamer when enabled.
Meteor DDP Stream Interception
apps/meteor/client/meteor/overrides/stubMeteorStream.ts, apps/meteor/client/meteor/overrides/killMeteorStream.ts, apps/meteor/client/meteor/overrides/subscribeViaSDK.ts
Stub replaces Meteor.connection._stream with shim forwarding frames to SDK; kill module resets revival/quiescence state for synthetic frames; subscription override routes Meteor.subscribe calls through SDK when enabled.
DDP & Collection Bridging
apps/meteor/client/meteor/overrides/ddpSdkCollectionBridge.ts, apps/meteor/client/meteor/overrides/ddpOverREST.ts
Collection bridge forwards eligible SDK frames into Meteor.connection handlers; DDP-over-REST adds resume-login detection and guards token-based login on resumes to prevent races.
Client SDK & Streamer
apps/meteor/app/utils/client/lib/SDKClient.ts
Stream creation conditional on sdkTransportEnabled; new createNewDdpSdkStream() subscribes via SDK and bridges events; publish routes through SDK or Meteor based on flag.
Login & Auth State
apps/meteor/client/lib/loggedIn.ts, apps/meteor/client/views/root/hooks/loggedIn/useForceLogout.ts
Login helpers branch on sdkTransportEnabled: use Accounts.onLogin for Meteor path or new subscribeToLogin() bridge for SDK path; force logout clears token and userId when SDK enabled.
Presence Subscription
apps/meteor/client/lib/presence.ts
New subscribeUserPresence() helper routes through SDK when connected and authenticated, otherwise falls back to Meteor.
Provider & Startup Wiring
apps/meteor/client/providers/ServerProvider.tsx, apps/meteor/client/startup/startup.ts
ServerProvider fuses SDK and Meteor status; disconnect/reconnect logic branches on transport; startup sync uses ensureConnectedAndAuthenticated() with deduplication for SDK path.
Bootstrap & Debugging
apps/meteor/client/main.ts
Top-level SDK module import ensures eager initialization of transport layer.
Test & Cache Updates
apps/meteor/tests/e2e/page-objects/fragments/home-content.ts, apps/meteor/client/lib/cachedStores/CachedStore.ts, ee/apps/ddp-streamer/src/DDPStreamer.ts, apps/meteor/package.json
E2E message send now uses transport-agnostic polling; cache version bumped from 18 to 19 for EJSON encoding; DDPStreamer logout uses graceful socket close; @rocket.chat/ddp-client workspace dependency added.
Changeset
.changeset/use-rc-sdk-transport-setting.md
Changeset documents new setting and minor version bump for @rocket.chat/meteor.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

The PR spans 20+ files with heterogeneous logic across multiple interconnected layers: SDK singleton management, Meteor interception/stubbing, authentication coordination, and conditional branching throughout. While individual files follow consistent patterns, the cumulative complexity of ensuring correct transport routing across presence, login, streaming, and collection updates demands careful verification of state synchronization and error handling paths.

Possibly related PRs

  • RocketChat/Rocket.Chat#40301: Directly implements the same SDK-over-DDP transport feature with overlapping files and exports (getDdpSdk, isSdkTransportEnabled, streamerAdapter, Meteor overrides).

Suggested labels

type: feature, scope: client, scope: transport, scope: experimental

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main change: re-introduction of an experimental Meteor-independent DDP client with the Use_RC_SDK feature.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ggazzo ggazzo added this to the 8.5.0 milestone May 6, 2026
@coderabbitai coderabbitai Bot added the type: feature Pull requests that introduces new feature label May 6, 2026

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 15

🧹 Nitpick comments (9)
apps/meteor/client/meteor/overrides/ddpOverREST.ts (2)

9-37: 💤 Low value

Minor: shouldBypass could reuse the new isResumeLogin helper.

Line 24 inlines the same method === 'login' && params?.[0]?.resume check that line 9 just extracted. Consolidating keeps both call sites in sync if the resume-detection rule changes.

♻️ Proposed fix
-const shouldBypass = ({ msg, method, params }: Meteor.IDDPMessage): boolean => {
-	if (msg !== 'method') {
+const shouldBypass = (message: Meteor.IDDPMessage): boolean => {
+	const { msg, method } = message;
+	if (msg !== 'method') {
 		return true;
 	}
 
-	// In microservices CI, ddp-streamer-service registers `login`, `logout`,
+	// In microservices CI, ddp-streamer-service registers `login`, `logout`,
 	// `setUserStatus`, and `UserPresence:*` as native methods (configureServer.ts
 	// in ee/apps/ddp-streamer); every other method delegates to the Meteor
 	// service via callMethodWithToken (extra hop). Bypassing these to Meteor's
 	// own WS routes them straight to ddp-streamer for the fast path; routing
 	// them through REST would wedge them on the slow rocketchat-main path
 	// instead, blowing past the 5s `expect(...).toBeVisible()` timeouts the
 	// post-relogin tests rely on.
-	if (method === 'login' && params?.[0]?.resume) {
+	if (isResumeLogin(message)) {
 		return true;
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/meteor/overrides/ddpOverREST.ts` around lines 9 - 37, The
inline resume-login check in shouldBypass duplicates the helper isResumeLogin;
replace the condition if (method === 'login' && params?.[0]?.resume) with a call
to isResumeLogin({ method, params }) so shouldBypass reuses the centralized
logic (remove the duplicated expression and use the isResumeLogin helper
instead).

45-57: 💤 Low value

Consider containing _streamHandlers.onMessage async rejections.

_streamHandlers.onMessage returns a Promise (the message handler is implemented as an async generator, as called out in ddpSdkCollectionBridge.ts lines 73-81). If a stale frame ever lands here without an active invoker, an unhandled rejection would bubble out of this scope. Mirroring the bridge's wrap pattern keeps behavior consistent across both call sites.

♻️ Proposed fix
-			this._streamHandlers.onMessage(resultMessage);
+			const result = this._streamHandlers.onMessage(resultMessage) as unknown;
+			if (result && typeof (result as Promise<unknown>).then === 'function') {
+				(result as Promise<unknown>).catch((err) => {
+					console.warn('[ddpOverREST] processResult drop (async)', err);
+				});
+			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/meteor/overrides/ddpOverREST.ts` around lines 45 - 57, The
processResult function should guard against unhandled promise rejections from
this._streamHandlers.onMessage: instead of calling it directly, invoke it
through the same async-wrap pattern used by the bridge (i.e., wrap the returned
Promise and attach a .catch handler so rejections are handled and routed to the
stream handler's error/cleanup path), keeping the early-return check on
this._methodInvokers[message.id] and otherwise forwarding the message with
this._livedata_data and then calling this._streamHandlers.onMessage via the
wrapped/catch-handled invocation to prevent unhandled rejections.
apps/meteor/client/lib/sdk/meteorBackedSdk.ts (2)

60-70: 💤 Low value

Avoid mutating Meteor's SubscriptionHandle via Object.assign.

Object.assign(sub, …) overwrites Meteor's reactive ready() (boolean reactive) with one that returns Promise.resolve() on the same object. While the cast hides this from typing, anything that still treats the returned handle as a Meteor handle (e.g. shared subscription pools, Tracker code re-reading ready()) sees broken semantics. Prefer returning a new wrapper object that holds sub as the underlying handle.

♻️ Proposed fix
-		const sub = (Meteor.connection.subscribe as (name: string, ...args: unknown[]) => Meteor.SubscriptionHandle)(name, ...args);
-		// Approximate DDPSDK's Subscription shape with Meteor's handle. The
-		// codebase only reads `stop`/`ready`/`isReady`/`id` from it.
-		return Object.assign(sub, {
-			id: '',
-			isReady: false,
-			ready: () => Promise.resolve(),
-			onChange: () => undefined,
-		}) as unknown as ReturnType<DDPSDK['client']['subscribe']>;
+		const sub = (Meteor.connection.subscribe as (name: string, ...args: unknown[]) => Meteor.SubscriptionHandle)(name, ...args);
+		return {
+			id: '',
+			isReady: false,
+			ready: () => Promise.resolve(),
+			onChange: () => undefined,
+			stop: () => sub.stop(),
+		} as unknown as ReturnType<DDPSDK['client']['subscribe']>;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/lib/sdk/meteorBackedSdk.ts` around lines 60 - 70, In the
subscribe function, avoid mutating Meteor.connection.subscribe's
SubscriptionHandle (do not use Object.assign(sub, ...)); instead return a new
wrapper object that holds the original sub and delegates stop, ready (returning
sub.ready()), isReady (reflecting sub.ready or sub.isReady), and id to the
underlying handle while adding the onChange method and matching the
DDPSDK['client']['subscribe'] return shape; update the wrapper to call
sub.stop(), forward sub.ready()/sub.isReady access, and expose id from sub
without overwriting the original SubscriptionHandle.

81-98: ⚡ Quick win

Fix onCollection listener accumulation on Meteor.connection._stream.

Each onCollection(id, callback) invocation permanently binds a handler to the underlying stream's eventCallbacks.message array with no removal mechanism. Repeated calls (during navigation, room switches) cause accumulating handlers that match every DDP frame N times. While Meteor's _stream indeed lacks an off method in this implementation, the handler leak is still preventable: store a reference to the unsubscribe callback returned by stream.on() (if available) or maintain a cleanup registry to filter handlers instead of binding direct callbacks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/lib/sdk/meteorBackedSdk.ts` around lines 81 - 98, The
onCollection function currently attaches a permanent handler to
Meteor.connection._stream via stream.on('message', handler) causing listener
accumulation; fix it by making each handler removable: when calling stream.on in
onCollection, capture and return the unsubscribe function if stream.on provides
one (use that to remove the handler), and if stream.on does not return an
unsubscribe, wrap handler with an active flag registered in a local cleanup
registry (or closure) so the returned function flips the flag to ignore future
invocations; update references to onCollection, handler, stream.on and
noopUnsubscribe so callers get a real unsubscribe that prevents the leak.
apps/meteor/client/lib/sdk/ddpSdk.ts (2)

152-162: 💤 Low value

isAuthError referenced before its declaration.

It's a const arrow function (not a hoisted function declaration) declared at L152 but referenced inside ensureConnectedAndAuthenticated at L129. Runtime is fine — the reference resolves only when the exported function is later invoked, by which point the module has finished evaluating — but if anything ever invokes ensureConnectedAndAuthenticated synchronously during module init (e.g., a top-level await import side effect), this would TDZ. Move isAuthError above its first use to keep the file readable and TDZ-safe.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/lib/sdk/ddpSdk.ts` around lines 152 - 162, Move the
isAuthError declaration so it appears before its first use in
ensureConnectedAndAuthenticated (or convert it to a hoisted function
declaration) to avoid a temporal-dead-zone risk; locate the const isAuthError =
(...) arrow function and place it above the ensureConnectedAndAuthenticated
function (or replace it with function isAuthError(...) { ... }) so the
identifier is available at module-evaluation time.

217-217: ⚡ Quick win

Inconsistent flag check — re-evaluates isSdkTransportEnabled() instead of using the module-cached value.

L10 already captures const sdkTransportEnabled = isSdkTransportEnabled();, and every other site in this file (and in SDKClient.ts/ServerProvider.tsx) reads the cached constant. Calling isSdkTransportEnabled() again here means a different result is theoretically possible (e.g., if the implementation reads from a mutable source), and the dead code at L52 vs L217 is jarring on a quick read.

🔧
-if (typeof window !== 'undefined' && isSdkTransportEnabled()) {
+if (typeof window !== 'undefined' && sdkTransportEnabled) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/lib/sdk/ddpSdk.ts` at line 217, The condition re-calls
isSdkTransportEnabled() instead of using the previously cached constant
sdkTransportEnabled; change the if-check to use sdkTransportEnabled so the
module-level flag is used consistently (e.g., replace isSdkTransportEnabled()
with sdkTransportEnabled in the window-check branch around the code using
window), ensuring all references in this module match the module-cached value
and avoid re-evaluation.
apps/meteor/app/utils/client/lib/SDKClient.ts (3)

149-250: ⚡ Quick win

Significant duplication with createNewMeteorStream.

The onChange, ready, onError, onStop, isReady, and unsubList blocks in createNewDdpSdkStream (lines 206–248) are nearly identical to the equivalents in createNewMeteorStream (lines 93–146). Only the subscribe/onCollection plumbing is transport-specific. Consider extracting a shared makeStreamMapValue(meta, ee, { stop }) factory that produces the common surface, leaving each transport responsible only for triggering ee.emit('ready' | 'error' | 'stop'). That removes ~40 lines and prevents the two paths from drifting (e.g., the legacy onChange still emits an extra blank line that the SDK variant dropped — see line 97 vs line 208).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/app/utils/client/lib/SDKClient.ts` around lines 149 - 250, The
two stream creators duplicate the subscriber surface (onChange, ready, onError,
onStop, isReady, unsubList) between createNewDdpSdkStream and
createNewMeteorStream; extract that common behavior into a small factory (e.g.,
makeStreamMapValue(meta, ee, handlers)) that returns the StreamMapValue shape
and accepts a transport-specific stop callback, then have createNewDdpSdkStream
and createNewMeteorStream only perform their transport-specific
subscribe/onCollection wiring and call ee.emit('ready'|'error'|'stop') as
needed; update both functions to use makeStreamMapValue so the shared logic
lives in one place and remove the duplicated blocks.

198-203: 💤 Low value

Per-subscription onCollection listeners scale O(N²) with active subscriptions.

Every createNewDdpSdkStream call registers its own listener on stream-${streamName}, then filters by eventName !== key. With N active subscriptions on the same collection (e.g., notify-user, notify-room), every incoming frame walks all N listeners just to dispatch one. The streamProxy already keys by stream-${streamName}/${key} — you can register a single onCollection per streamName (memoized in a Map<string, () => void>) and let the proxy's keyed emit do the routing, mirroring how the legacy Meteor.connection._stream bridge at lines 270–276 works.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/app/utils/client/lib/SDKClient.ts` around lines 198 - 203, The
current createNewDdpSdkStream registers a new sdk.client.onCollection listener
for each subscription which causes O(N²) work; fix it by memoizing a single
onCollection listener per streamName (use a Map<string, () => void> e.g.,
streamListeners) so the first createNewDdpSdkStream call for a given streamName
registers sdk.client.onCollection(`stream-${streamName}`, ...) and stores its
off/unsubscribe function, and subsequent calls reuse that listener instead of
adding more; inside that single listener remove the per-listener filtering and
simply forward events via
streamProxy.emit(`stream-${streamName}/${data.fields?.eventName}`,
data.fields?.args) so the proxy handles keyed routing, and ensure you call the
stored off function when the last subscription for that streamName is removed.

359-367: 💤 Low value

publish errors are silently swallowed on the SDK path.

void getDdpSdk().client.callAsync(...) discards both the resolution and rejection. If the server returns an error for a stream-${name} write (auth, validation, rate-limit), the caller has no way to react and there's no telemetry — it just silently drops on the floor. The legacy Meteor.call(...) without a callback has the same ergonomics, so this matches existing behavior, but on the SDK path it's worth at least logging:

🔧 Proposed fix
 const publish = sdkTransportEnabled
 	? (name: string, args: unknown[]) => {
-			void getDdpSdk().client.callAsync(`stream-${name}`, ...args);
+			void getDdpSdk()
+				.client.callAsync(`stream-${name}`, ...args)
+				.catch((err) => console.warn('[sdk.publish] failed', name, err));
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/app/utils/client/lib/SDKClient.ts` around lines 359 - 367, The
SDK publish branch is dropping promise rejections from
getDdpSdk().client.callAsync; update the sdkTransportEnabled branch in the
publish definition so callAsync is not called with a leading void but instead
attach a .catch handler that logs the error (including the stream name and
relevant args/context) so failures aren’t silently swallowed; if there’s a
project logging utility available in this file prefer that (e.g., processLogger
or similar), otherwise use console.error, and keep the Meteor.call path
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/meteor/app/utils/client/lib/SDKClient.ts`:
- Around line 178-204: The current flow calls
ensureConnectedAndAuthenticated().catch(() => undefined).then(...) which can
hang if ensureConnectedAndAuthenticated() never settles; update the logic in the
subscribe block to race ensureConnectedAndAuthenticated() with a timeout and
handle rejection so ready() always resolves or rejects: wrap the call to
ensureConnectedAndAuthenticated() (used before calling getDdpSdk(), creating
subscription and setting up offCollection) in a Promise.race with a short
timeout, and on timeout or rejection emit ee.emit('ready', [err]) and
ee.emit('error', err) (and return early so no subscription is created);
alternatively, if getDdpSdk().connection.status === 'connected' &&
getDdpSdk().account.uid is true, bypass the wait and subscribe immediately —
adjust the code around ensureConnectedAndAuthenticated(), getDdpSdk(),
subscription, and the ready() handlers accordingly.

In `@apps/meteor/client/lib/cachedStores/CachedStore.ts`:
- Around line 53-56: Delete the multi-line implementation comment in
CachedStore.ts (the block explaining the bump from 18 → 19 and the EJSON/JSON
rationale) so the file contains no inline implementation commentary; instead,
move that explanation into the PR description or the changeset/changelog for
this change (reference: the CachedStore.ts comment block near the version bump
and any nearby comment tokens mentioning "18 → 19" and "EJSON/JSON" so you
remove the correct text).

In `@apps/meteor/client/lib/loggedIn.ts`:
- Around line 74-81: Both Accounts.onLogin(handler) and
subscribeToLogin(handler) can register the same handler when sdkTransportEnabled
is true, causing onLoggedIn to fire twice; change the subscription logic so only
one source is registered: create accountsSubscription =
Accounts.onLogin(handler) only when sdkTransportEnabled is false, and otherwise
use stopUserIdSubscription = subscribeToLogin(handler) (i.e., make the
Accounts.onLogin registration conditional on !sdkTransportEnabled). Keep the
isLoggedIn() immediate check and ensure variable names accountsSubscription and
stopUserIdSubscription remain unchanged.

In `@apps/meteor/client/lib/sdk/ddpSdk.ts`:
- Around line 73-124: The race occurs because inflightLogin is checked before
the 500ms busy-wait, allowing two callers to both pass the check and each start
loginWithToken; modify ensureConnectedAndAuthenticated to re-check inflightLogin
immediately after the busy-wait (and return/await it if set) before assigning a
new inflightLogin, or alternatively assign inflightLogin to a promise
placeholder before the busy-wait so only the first caller creates the actual
login promise; reference ensureConnectedAndAuthenticated, inflightLogin,
sdk.account.loginWithToken, waitForConnected and startConnect when applying the
fix.
- Around line 40-48: waitForConnected currently only listens for the 'connected'
event and can hang if the DDP handshake fails; update waitForConnected (function
waitForConnected) to also check sdk.connection.status for 'failed' up-front and
add a 'connection' event listener that rejects the Promise when status ===
'failed' (or any terminal failure), ensuring you clean up both listeners (the
'connected' and 'connection' handlers) when resolving or rejecting so no
handlers leak.

In `@apps/meteor/client/lib/sdk/meteorBackedSdk.ts`:
- Around line 190-200: The getter user in meteorBackedSdk currently casts
Meteor.user() (which returns {_id, ...}) to {id, username?, token?,
tokenExpires?}, causing account.user?.id to be undefined; fix by mapping the
shape explicitly in the user getter: read Meteor.user() into a local u and
return an object that sets id: u._id (and copies username/token/tokenExpires if
present), or alternatively update the advertised type to include _id and adjust
callers; locate the getter named "user" and change the return to construct the
correct id field instead of an unsafe cast.

In `@apps/meteor/client/lib/sdk/sdkTransportEnabled.ts`:
- Around line 9-11: Update the JSDoc in sdkTransportEnabled.ts to reference the
actual admin setting name used by the server: replace the incorrect
`SDK_DDP_Transport_Enabled` with `Use_RC_SDK` (the setting watched in the server
index). Ensure the doc line describing the `<meta
name="rc-sdk-transport-enabled" ...>` injection mentions `Use_RC_SDK` so future
maintainers can locate the correct admin setting.

In `@apps/meteor/client/meteor/overrides/stubMeteorStream.ts`:
- Around line 38-44: The status() getter currently calls the dependency's
changed() which incorrectly invalidates dependents on every read; change it to
call the dependency's depend() (e.g., this.statusListeners?.depend()) so reads
register a dependency, and leave changed() only in statusChanged() to notify
updates; apply the same swap for the other occurrences mentioned (around the
blocks that reference statusListeners.changed()/depend() at the other
locations).
- Around line 29-47: The stub type MeteorIDDPStream and its implementation are
missing the internal boolean flag _connected, causing Meteor's runtime checks
(e.g., MethodInvoker) to treat the stream as disconnected; add a _connected:
boolean property to the MeteorIDDPStream type and ensure the stub instance sets
and updates _connected = true on successful connect/reconnect and _connected =
false on disconnect/_lostConnection so runtime checks see the correct connection
state (update functions/methods send, reconnect, disconnect, _lostConnection to
toggle _connected accordingly).

In `@apps/meteor/client/meteor/overrides/subscribeViaSDK.ts`:
- Around line 17-21: The subscription failure handling in subscribeViaSDK (and
its SubscribeCallbacks type) currently only invokes onError(err), which breaks
Meteor's expected onStop(err) contract; update the failure path so it invokes
both callbacks: call onStop(err) first (to preserve Meteor semantics) and then
onError(err) if provided, ensuring you reference the SubscribeCallbacks.onStop
and SubscribeCallbacks.onError handlers used in subscribeViaSDK to locate and
modify the error branch.

In `@apps/meteor/client/providers/ServerProvider.tsx`:
- Around line 117-132: sdkStatusToMeteor currently maps only sdkStatus and
ignores Meteor's own status, which violates the "worst-case of both transports"
intent; update sdkStatusToMeteor to consider both sdkStatus and the Meteor
`status` (and still include `retryCount`/`retryTime`) by computing a severity
ranking for statuses from both sources and returning the more severe result
(e.g., map statuses to numeric severity, compare sdkStatus vs. meteor.status,
then return the status/connected value corresponding to the worse severity),
keeping the existing shape returned (status, connected, ...retry) and
referencing sdkStatusToMeteor, sdkStatus, and the meteor variable in the
implementation.

In `@apps/meteor/client/startup/startup.ts`:
- Around line 43-65: runUserDataSync can apply stale session data after
awaiting; after any await points (notably after
ensureConnectedAndAuthenticated() and synchronizeUserData(uid)) re-check that
userIdStore.getState() === uid (and that user is still present) and return early
if it changed; only then call sdk.call('userSetUtcOffset', ...) and
emitStatusChange(user.status) to avoid leaking updates into a different or
logged-out session.

In `@apps/meteor/client/views/root/hooks/loggedIn/useForceLogout.ts`:
- Around line 20-25: The early return in useForceLogout (the branch checking
sdkTransportEnabled) skips local credential cleanup and leaves
Meteor.loginToken/userId set; remove the early return and ensure the legacy
branch still performs the same local cleanup steps (clear
Meteor._localStorage/Meteor.loginToken and Meteor.userId, and call the same
local logout/cleanup functions used in the SDK path) when handling the
force_logout path in useForceLogout so credentials are cleared immediately even
if sdkTransportEnabled is false.

In `@ee/apps/ddp-streamer/src/DDPStreamer.ts`:
- Around line 63-72: The large inline rationale in DDPStreamer (around the
socket shutdown logic that uses ws.close() with a fallback to ws.terminate())
should be removed from implementation code; replace the block comment with a
concise one-line note like "Graceful close with terminate fallback" or delete
the comment entirely, and move the detailed explanation into the PR/changelog.
Locate the socket shutdown code in the DDPStreamer class (the section
referencing ws.close() and ws.terminate()) and update the comment accordingly
while leaving the existing behavior (close then terminate fallback) unchanged.

---

Nitpick comments:
In `@apps/meteor/app/utils/client/lib/SDKClient.ts`:
- Around line 149-250: The two stream creators duplicate the subscriber surface
(onChange, ready, onError, onStop, isReady, unsubList) between
createNewDdpSdkStream and createNewMeteorStream; extract that common behavior
into a small factory (e.g., makeStreamMapValue(meta, ee, handlers)) that returns
the StreamMapValue shape and accepts a transport-specific stop callback, then
have createNewDdpSdkStream and createNewMeteorStream only perform their
transport-specific subscribe/onCollection wiring and call
ee.emit('ready'|'error'|'stop') as needed; update both functions to use
makeStreamMapValue so the shared logic lives in one place and remove the
duplicated blocks.
- Around line 198-203: The current createNewDdpSdkStream registers a new
sdk.client.onCollection listener for each subscription which causes O(N²) work;
fix it by memoizing a single onCollection listener per streamName (use a
Map<string, () => void> e.g., streamListeners) so the first
createNewDdpSdkStream call for a given streamName registers
sdk.client.onCollection(`stream-${streamName}`, ...) and stores its
off/unsubscribe function, and subsequent calls reuse that listener instead of
adding more; inside that single listener remove the per-listener filtering and
simply forward events via
streamProxy.emit(`stream-${streamName}/${data.fields?.eventName}`,
data.fields?.args) so the proxy handles keyed routing, and ensure you call the
stored off function when the last subscription for that streamName is removed.
- Around line 359-367: The SDK publish branch is dropping promise rejections
from getDdpSdk().client.callAsync; update the sdkTransportEnabled branch in the
publish definition so callAsync is not called with a leading void but instead
attach a .catch handler that logs the error (including the stream name and
relevant args/context) so failures aren’t silently swallowed; if there’s a
project logging utility available in this file prefer that (e.g., processLogger
or similar), otherwise use console.error, and keep the Meteor.call path
unchanged.

In `@apps/meteor/client/lib/sdk/ddpSdk.ts`:
- Around line 152-162: Move the isAuthError declaration so it appears before its
first use in ensureConnectedAndAuthenticated (or convert it to a hoisted
function declaration) to avoid a temporal-dead-zone risk; locate the const
isAuthError = (...) arrow function and place it above the
ensureConnectedAndAuthenticated function (or replace it with function
isAuthError(...) { ... }) so the identifier is available at module-evaluation
time.
- Line 217: The condition re-calls isSdkTransportEnabled() instead of using the
previously cached constant sdkTransportEnabled; change the if-check to use
sdkTransportEnabled so the module-level flag is used consistently (e.g., replace
isSdkTransportEnabled() with sdkTransportEnabled in the window-check branch
around the code using window), ensuring all references in this module match the
module-cached value and avoid re-evaluation.

In `@apps/meteor/client/lib/sdk/meteorBackedSdk.ts`:
- Around line 60-70: In the subscribe function, avoid mutating
Meteor.connection.subscribe's SubscriptionHandle (do not use Object.assign(sub,
...)); instead return a new wrapper object that holds the original sub and
delegates stop, ready (returning sub.ready()), isReady (reflecting sub.ready or
sub.isReady), and id to the underlying handle while adding the onChange method
and matching the DDPSDK['client']['subscribe'] return shape; update the wrapper
to call sub.stop(), forward sub.ready()/sub.isReady access, and expose id from
sub without overwriting the original SubscriptionHandle.
- Around line 81-98: The onCollection function currently attaches a permanent
handler to Meteor.connection._stream via stream.on('message', handler) causing
listener accumulation; fix it by making each handler removable: when calling
stream.on in onCollection, capture and return the unsubscribe function if
stream.on provides one (use that to remove the handler), and if stream.on does
not return an unsubscribe, wrap handler with an active flag registered in a
local cleanup registry (or closure) so the returned function flips the flag to
ignore future invocations; update references to onCollection, handler, stream.on
and noopUnsubscribe so callers get a real unsubscribe that prevents the leak.

In `@apps/meteor/client/meteor/overrides/ddpOverREST.ts`:
- Around line 9-37: The inline resume-login check in shouldBypass duplicates the
helper isResumeLogin; replace the condition if (method === 'login' &&
params?.[0]?.resume) with a call to isResumeLogin({ method, params }) so
shouldBypass reuses the centralized logic (remove the duplicated expression and
use the isResumeLogin helper instead).
- Around line 45-57: The processResult function should guard against unhandled
promise rejections from this._streamHandlers.onMessage: instead of calling it
directly, invoke it through the same async-wrap pattern used by the bridge
(i.e., wrap the returned Promise and attach a .catch handler so rejections are
handled and routed to the stream handler's error/cleanup path), keeping the
early-return check on this._methodInvokers[message.id] and otherwise forwarding
the message with this._livedata_data and then calling
this._streamHandlers.onMessage via the wrapped/catch-handled invocation to
prevent unhandled rejections.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b877ad5c-ecad-492a-a3fc-5e1c32c79d0f

📥 Commits

Reviewing files that changed from the base of the PR and between 281c22a and 57e1e2d.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (26)
  • .changeset/use-rc-sdk-transport-setting.md
  • apps/meteor/app/notifications/client/lib/Presence.ts
  • apps/meteor/app/ui-master/server/index.ts
  • apps/meteor/app/utils/client/lib/SDKClient.ts
  • apps/meteor/client/lib/cachedStores/CachedStore.ts
  • apps/meteor/client/lib/loggedIn.ts
  • apps/meteor/client/lib/presence.ts
  • apps/meteor/client/lib/sdk/ddpSdk.ts
  • apps/meteor/client/lib/sdk/meteorBackedSdk.ts
  • apps/meteor/client/lib/sdk/sdkTransportEnabled.ts
  • apps/meteor/client/lib/sdk/streamerAdapter.ts
  • apps/meteor/client/main.ts
  • apps/meteor/client/meteor/overrides/ddpOverREST.ts
  • apps/meteor/client/meteor/overrides/ddpSdkCollectionBridge.ts
  • apps/meteor/client/meteor/overrides/index.ts
  • apps/meteor/client/meteor/overrides/killMeteorStream.ts
  • apps/meteor/client/meteor/overrides/stubMeteorStream.ts
  • apps/meteor/client/meteor/overrides/subscribeViaSDK.ts
  • apps/meteor/client/providers/ServerProvider.tsx
  • apps/meteor/client/startup/startup.ts
  • apps/meteor/client/views/root/hooks/loggedIn/useForceLogout.ts
  • apps/meteor/package.json
  • apps/meteor/server/settings/general.ts
  • apps/meteor/tests/e2e/page-objects/fragments/home-content.ts
  • ee/apps/ddp-streamer/src/DDPStreamer.ts
  • packages/i18n/src/locales/en.i18n.json
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: 📦 Build Packages
  • GitHub Check: CodeQL-Build
  • GitHub Check: CodeQL-Build
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/playwright.mdc)

**/*.{ts,tsx,js}: Write concise, technical TypeScript/JavaScript with accurate typing in Playwright tests
Avoid code comments in the implementation

Files:

  • apps/meteor/client/main.ts
  • apps/meteor/client/lib/sdk/sdkTransportEnabled.ts
  • apps/meteor/client/lib/cachedStores/CachedStore.ts
  • apps/meteor/server/settings/general.ts
  • apps/meteor/client/meteor/overrides/index.ts
  • apps/meteor/app/ui-master/server/index.ts
  • apps/meteor/client/meteor/overrides/ddpSdkCollectionBridge.ts
  • apps/meteor/client/lib/sdk/ddpSdk.ts
  • ee/apps/ddp-streamer/src/DDPStreamer.ts
  • apps/meteor/client/lib/sdk/streamerAdapter.ts
  • apps/meteor/client/meteor/overrides/subscribeViaSDK.ts
  • apps/meteor/app/utils/client/lib/SDKClient.ts
  • apps/meteor/client/meteor/overrides/killMeteorStream.ts
  • apps/meteor/tests/e2e/page-objects/fragments/home-content.ts
  • apps/meteor/client/lib/presence.ts
  • apps/meteor/client/lib/sdk/meteorBackedSdk.ts
  • apps/meteor/client/meteor/overrides/ddpOverREST.ts
  • apps/meteor/app/notifications/client/lib/Presence.ts
  • apps/meteor/client/providers/ServerProvider.tsx
  • apps/meteor/client/startup/startup.ts
  • apps/meteor/client/lib/loggedIn.ts
  • apps/meteor/client/meteor/overrides/stubMeteorStream.ts
  • apps/meteor/client/views/root/hooks/loggedIn/useForceLogout.ts
apps/meteor/tests/e2e/page-objects/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/playwright.mdc)

Utilize existing page objects pattern from apps/meteor/tests/e2e/page-objects/

Files:

  • apps/meteor/tests/e2e/page-objects/fragments/home-content.ts
apps/meteor/tests/e2e/**/*.{ts,spec.ts}

📄 CodeRabbit inference engine (.cursor/rules/playwright.mdc)

apps/meteor/tests/e2e/**/*.{ts,spec.ts}: Store commonly used locators in variables/constants for reuse
Follow Page Object Model pattern consistently in Playwright tests

Files:

  • apps/meteor/tests/e2e/page-objects/fragments/home-content.ts
🧠 Learnings (8)
📚 Learning: 2026-02-10T16:32:42.586Z
Learnt from: tassoevan
Repo: RocketChat/Rocket.Chat PR: 38528
File: apps/meteor/client/startup/roles.ts:14-14
Timestamp: 2026-02-10T16:32:42.586Z
Learning: In Rocket.Chat's Meteor client code, DDP streams use EJSON and Date fields arrive as Date objects; do not manually construct new Date() in stream handlers (for example, in sdk.stream()). Only REST API responses return plain JSON where dates are strings, so implement explicit conversion there if needed. Apply this guidance to all TypeScript files under apps/meteor/client to ensure consistent date handling in DDP streams and REST responses.

Applied to files:

  • apps/meteor/client/main.ts
  • apps/meteor/client/lib/sdk/sdkTransportEnabled.ts
  • apps/meteor/client/lib/cachedStores/CachedStore.ts
  • apps/meteor/client/meteor/overrides/index.ts
  • apps/meteor/client/meteor/overrides/ddpSdkCollectionBridge.ts
  • apps/meteor/client/lib/sdk/ddpSdk.ts
  • apps/meteor/client/lib/sdk/streamerAdapter.ts
  • apps/meteor/client/meteor/overrides/subscribeViaSDK.ts
  • apps/meteor/client/meteor/overrides/killMeteorStream.ts
  • apps/meteor/client/lib/presence.ts
  • apps/meteor/client/lib/sdk/meteorBackedSdk.ts
  • apps/meteor/client/meteor/overrides/ddpOverREST.ts
  • apps/meteor/client/startup/startup.ts
  • apps/meteor/client/lib/loggedIn.ts
  • apps/meteor/client/meteor/overrides/stubMeteorStream.ts
  • apps/meteor/client/views/root/hooks/loggedIn/useForceLogout.ts
📚 Learning: 2026-02-26T19:25:44.063Z
Learnt from: gabriellsh
Repo: RocketChat/Rocket.Chat PR: 38778
File: packages/ui-voip/src/providers/useMediaSession.ts:192-192
Timestamp: 2026-02-26T19:25:44.063Z
Learning: In the Rocket.Chat repository, do not reference Biome lint rules in code review feedback. Biome is not used even if biome.json exists; only reference Biome rules if there is explicit, project-wide usage documented. For TypeScript files, review lint implications without Biome guidance unless the project enables Biome rules.

Applied to files:

  • apps/meteor/client/main.ts
  • apps/meteor/client/lib/sdk/sdkTransportEnabled.ts
  • apps/meteor/client/lib/cachedStores/CachedStore.ts
  • apps/meteor/server/settings/general.ts
  • apps/meteor/client/meteor/overrides/index.ts
  • apps/meteor/app/ui-master/server/index.ts
  • apps/meteor/client/meteor/overrides/ddpSdkCollectionBridge.ts
  • apps/meteor/client/lib/sdk/ddpSdk.ts
  • ee/apps/ddp-streamer/src/DDPStreamer.ts
  • apps/meteor/client/lib/sdk/streamerAdapter.ts
  • apps/meteor/client/meteor/overrides/subscribeViaSDK.ts
  • apps/meteor/app/utils/client/lib/SDKClient.ts
  • apps/meteor/client/meteor/overrides/killMeteorStream.ts
  • apps/meteor/tests/e2e/page-objects/fragments/home-content.ts
  • apps/meteor/client/lib/presence.ts
  • apps/meteor/client/lib/sdk/meteorBackedSdk.ts
  • apps/meteor/client/meteor/overrides/ddpOverREST.ts
  • apps/meteor/app/notifications/client/lib/Presence.ts
  • apps/meteor/client/startup/startup.ts
  • apps/meteor/client/lib/loggedIn.ts
  • apps/meteor/client/meteor/overrides/stubMeteorStream.ts
  • apps/meteor/client/views/root/hooks/loggedIn/useForceLogout.ts
📚 Learning: 2026-02-26T19:25:44.063Z
Learnt from: gabriellsh
Repo: RocketChat/Rocket.Chat PR: 38778
File: packages/ui-voip/src/providers/useMediaSession.ts:192-192
Timestamp: 2026-02-26T19:25:44.063Z
Learning: In this repository (RocketChat/Rocket.Chat), Biome lint rules are not used even if a biome.json exists. When reviewing TypeScript files (e.g., packages/ui-voip/src/providers/useMediaSession.ts), ensure lint suggestions do not reference Biome-specific rules. Rely on general ESLint/TypeScript lint rules and project conventions instead.

Applied to files:

  • apps/meteor/client/main.ts
  • apps/meteor/client/lib/sdk/sdkTransportEnabled.ts
  • apps/meteor/client/lib/cachedStores/CachedStore.ts
  • apps/meteor/server/settings/general.ts
  • apps/meteor/client/meteor/overrides/index.ts
  • apps/meteor/app/ui-master/server/index.ts
  • apps/meteor/client/meteor/overrides/ddpSdkCollectionBridge.ts
  • apps/meteor/client/lib/sdk/ddpSdk.ts
  • ee/apps/ddp-streamer/src/DDPStreamer.ts
  • apps/meteor/client/lib/sdk/streamerAdapter.ts
  • apps/meteor/client/meteor/overrides/subscribeViaSDK.ts
  • apps/meteor/app/utils/client/lib/SDKClient.ts
  • apps/meteor/client/meteor/overrides/killMeteorStream.ts
  • apps/meteor/tests/e2e/page-objects/fragments/home-content.ts
  • apps/meteor/client/lib/presence.ts
  • apps/meteor/client/lib/sdk/meteorBackedSdk.ts
  • apps/meteor/client/meteor/overrides/ddpOverREST.ts
  • apps/meteor/app/notifications/client/lib/Presence.ts
  • apps/meteor/client/startup/startup.ts
  • apps/meteor/client/lib/loggedIn.ts
  • apps/meteor/client/meteor/overrides/stubMeteorStream.ts
  • apps/meteor/client/views/root/hooks/loggedIn/useForceLogout.ts
📚 Learning: 2026-05-06T12:21:44.083Z
Learnt from: juliajforesti
Repo: RocketChat/Rocket.Chat PR: 40256
File: apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx:121-149
Timestamp: 2026-05-06T12:21:44.083Z
Learning: Field wrappers in rocket.chat/fuselage-forms (Field, FieldLabel, FieldRow, FieldError, FieldHint) auto-create htmlFor/id associations, aria-describedby, and role="alert" for errors. Do not manually set htmlFor, id, aria-describedby, or role attributes when using these wrappers. This automatic wiring does not apply to plain rocket.chat/fuselage components, which require explicit ID wiring per the accessibility docs. In code reviews, prefer using fuselage-forms wrappers for form fields and verify there is no unnecessary manual ID/aria wiring in files that use these wrappers. If a component uses plain fuselage components, ensure proper id wiring as per docs.

Applied to files:

  • apps/meteor/client/main.ts
  • apps/meteor/client/lib/sdk/sdkTransportEnabled.ts
  • apps/meteor/client/lib/cachedStores/CachedStore.ts
  • apps/meteor/server/settings/general.ts
  • apps/meteor/client/meteor/overrides/index.ts
  • apps/meteor/app/ui-master/server/index.ts
  • apps/meteor/client/meteor/overrides/ddpSdkCollectionBridge.ts
  • apps/meteor/client/lib/sdk/ddpSdk.ts
  • ee/apps/ddp-streamer/src/DDPStreamer.ts
  • apps/meteor/client/lib/sdk/streamerAdapter.ts
  • apps/meteor/client/meteor/overrides/subscribeViaSDK.ts
  • apps/meteor/app/utils/client/lib/SDKClient.ts
  • apps/meteor/client/meteor/overrides/killMeteorStream.ts
  • apps/meteor/tests/e2e/page-objects/fragments/home-content.ts
  • apps/meteor/client/lib/presence.ts
  • apps/meteor/client/lib/sdk/meteorBackedSdk.ts
  • apps/meteor/client/meteor/overrides/ddpOverREST.ts
  • apps/meteor/app/notifications/client/lib/Presence.ts
  • apps/meteor/client/providers/ServerProvider.tsx
  • apps/meteor/client/startup/startup.ts
  • apps/meteor/client/lib/loggedIn.ts
  • apps/meteor/client/meteor/overrides/stubMeteorStream.ts
  • apps/meteor/client/views/root/hooks/loggedIn/useForceLogout.ts
📚 Learning: 2026-03-16T21:50:37.589Z
Learnt from: amitb0ra
Repo: RocketChat/Rocket.Chat PR: 39676
File: .changeset/migrate-users-register-openapi.md:3-3
Timestamp: 2026-03-16T21:50:37.589Z
Learning: For changes related to OpenAPI migrations in Rocket.Chat/OpenAPI, when removing endpoint types and validators from rocket.chat/rest-typings (e.g., UserRegisterParamsPOST, /v1/users.register) document this as a minor changeset (not breaking) per RocketChat/Rocket.Chat-Open-API#150 Rule 7. Note that the endpoint type is re-exposed via a module augmentation .d.ts in the consuming package (e.g., packages/web-ui-registration/src/users-register.d.ts). In reviews, ensure the changeset clearly states: this is a non-breaking change, the major version should not be bumped, and the changeset reflects a minor version bump. Do not treat this as a breaking change during OpenAPI migrations.

Applied to files:

  • .changeset/use-rc-sdk-transport-setting.md
📚 Learning: 2025-12-16T17:29:40.430Z
Learnt from: gabriellsh
Repo: RocketChat/Rocket.Chat PR: 37834
File: apps/meteor/tests/e2e/page-objects/fragments/admin-flextab-emoji.ts:12-22
Timestamp: 2025-12-16T17:29:40.430Z
Learning: In all page-object files under apps/meteor/tests/e2e/page-objects/, import expect from ../../utils/test (Playwright's async expect) instead of from Jest. Jest's expect is synchronous and incompatible with web-first assertions like toBeVisible, which can cause TypeScript errors.

Applied to files:

  • apps/meteor/tests/e2e/page-objects/fragments/home-content.ts
📚 Learning: 2026-02-24T19:39:42.247Z
Learnt from: juliajforesti
Repo: RocketChat/Rocket.Chat PR: 38493
File: apps/meteor/tests/e2e/page-objects/fragments/message.ts:7-7
Timestamp: 2026-02-24T19:39:42.247Z
Learning: In RocketChat e2e tests, avoid using data-qa attributes to locate elements. Prefer semantic locators such as getByRole, getByLabel, getByText, getByTitle and ARIA-based selectors. Apply this rule to all TypeScript files under apps/meteor/tests/e2e to improve test reliability, accessibility, and maintainability.

Applied to files:

  • apps/meteor/tests/e2e/page-objects/fragments/home-content.ts
📚 Learning: 2026-03-27T14:52:56.865Z
Learnt from: dougfabris
Repo: RocketChat/Rocket.Chat PR: 39892
File: apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx:150-155
Timestamp: 2026-03-27T14:52:56.865Z
Learning: In Rocket.Chat, there are two different `ModalBackdrop` components with different prop APIs. During review, confirm the import source: (1) `rocket.chat/fuselage` `ModalBackdrop` uses `ModalBackdropProps` based on `BoxProps` (so it supports `onClick` and other Box/DOM props) and does not have an `onDismiss` prop; (2) `rocket.chat/ui-client` `ModalBackdrop` uses a narrower props interface like `{ children?: ReactNode; onDismiss?: () => void }` and handles Escape keypress and outside mouse-up, and it does not forward arbitrary DOM props such as `onClick`. Flag mismatched props (e.g., `onDismiss` passed to the fuselage component or `onClick` passed to the ui-client component) and ensure the usage matches the correct component being imported.

Applied to files:

  • apps/meteor/client/providers/ServerProvider.tsx
🔇 Additional comments (18)
apps/meteor/package.json (1)

105-105: Dependency addition is correctly scoped.

Adding @rocket.chat/ddp-client as a runtime workspace dependency here matches the new transport path usage.

apps/meteor/server/settings/general.ts (1)

99-104: Use_RC_SDK setting registration looks good.

Defaulting to false with explicit i18n description/alert keeps the feature safely gated while exposing clear admin controls.

apps/meteor/app/ui-master/server/index.ts (1)

31-33: Meta-tag toggle wiring is correct.

This watcher cleanly maps Use_RC_SDK to rc-sdk-transport-enabled with deterministic on/off values for client detection.

packages/i18n/src/locales/en.i18n.json (1)

5622-5624: Clear and actionable experimental feature copy.

The new label, description, and alert text are consistent and set correct expectations for admins enabling the SDK transport path.

.changeset/use-rc-sdk-transport-setting.md (1)

1-5: LGTM!

Changeset accurately enumerates the new admin setting, meta-tag, URL parameter, and localStorage precedence and matches the bump scope.

apps/meteor/tests/e2e/page-objects/fragments/home-content.ts (1)

123-149: LGTM!

Transport-agnostic flow looks correct: snapshotting before, polling for count >= before + 1, and asserting lastUserMessage no longer has rcx-message--pending is a reasonable DOM-only completion signal that doesn't depend on REST vs DDP transport. The >= (instead of ==) is well-justified for encrypted-channel decryption-status items.

apps/meteor/client/lib/sdk/sdkTransportEnabled.ts (1)

16-28: LGTM!

SSR guard, defensive try/catch, and the URL > localStorage > meta resolution order match the documented contract. Defaulting to false keeps the migration dormant.

apps/meteor/client/meteor/overrides/ddpSdkCollectionBridge.ts (1)

65-99: LGTM!

Bridge surface and frame filtering look correct: SDK-internal id detection prevents No callback invoker for method ... throws on Meteor's _process_updated/_process_result, and the dual sync/async error containment around _streamHandlers.onMessage keeps the queue draining when an individual frame hits a dead invoker. Module-load-only installation matches the rest of the boot pipeline.

apps/meteor/client/meteor/overrides/ddpOverREST.ts (1)

102-142: LGTM — preserves Meteor error codes for withAsyncTOTP.

Forwarding the original DDP-encoded result frame from body.message (when msg === 'result' and id matches) is the correct fix for the TOTP regression — codes like totp-required survive untouched. The id match prevents accidental cross-talk, and the synthetic-frame fallback still handles auth-middleware 401s. Good defensive sequencing.

apps/meteor/client/main.ts (1)

1-4: LGTM!

The side-effect import is placed after ./meteor/overrides (which installs _send interception, the SDK collection bridge, etc.) and ./meteor/startup, which is the right ordering — the DDP SDK singleton is materialised once the override surface is wired.

apps/meteor/client/meteor/overrides/index.ts (1)

2-5: LGTM!

Side-effect import order is reasonable: REST routing wired first, then the SDK collection bridge, subscription routing, stream stub, and revival reset. Each new module self-gates on isSdkTransportEnabled(), so the legacy path remains untouched.

apps/meteor/app/utils/client/lib/SDKClient.ts (1)

264-277: LGTM — legacy bridge correctly gated.

Skipping the Meteor.connection._stream.on('message', ...) bridge when SDK transport is enabled is the right call: with the flag on, frames arrive on the DDPSDK socket via onCollection (line 198) and bridging the Meteor stream too would double-fire streamProxy. The flag-off path preserves the legacy behavior exactly.

apps/meteor/client/providers/ServerProvider.tsx (3)

140-144: LGTM — restoring the spread fixes the useSyncExternalStore identity check.

Meteor.status() mutates and returns the same internal object on every call, so without the spread useReactiveValue's useSyncExternalStore snapshot identity never changes and ConnectionStatusBar stops re-rendering on connect/drop — exactly as the inline comment describes. The fix matches the PR's stated regression.


81-100: 💤 Low value

reconnect doesn't wait for Meteor's reconnect before re-authing the SDK — verified as safe.

Meteor.reconnect() is fire-and-forget; the call to ensureConnectedAndAuthenticated() runs immediately after. This creates a race where the SDK's login can finish before Meteor's session re-establishes, breaking ordering guarantees if downstream code relies on Meteor state being ready first.

Verification via codebase search confirms this is safe: onLoggedIn consumers (incomingMessages.ts, roles.ts, startup.ts) use SDK methods (sdk.stream, sdk.rest.get) and SDK-sourced state (userIdStore) rather than reading Meteor state directly. The codebase already acknowledges the ordering risk in comments (e.g., ddpSdk.ts lines 267–273 explicitly explain why separate ensureConnectedAndAuthenticated calls are avoided in certain paths to prevent duplicate Accounts.onLogin fires).


107-110: 'connection' event covers every status transition.

DDPSDK's Connection.emitStatus() emits a 'connection' event of type ConnectionStatus for every state change ('idle' | 'connecting' | 'connected' | 'failed' | 'closed' | 'disconnected' | 'reconnecting'), so a single listener here is enough to invalidate ddpSdkStatusDep for all transitions handled by sdkStatusToMeteor.

apps/meteor/client/lib/sdk/ddpSdk.ts (3)

252-264: LGTM — auth-error swallowing prevents unhandled-rejection storms on auto-relogin.

DDPSDK auto-fires loginWithToken on every connected event with void, so without this wrap, an auth-shaped rejection would bubble to window.onunhandledrejection. Returning undefined only on isAuthError keeps non-auth failures (e.g., network) flowing to the caller. The recovery path through Meteor's DDP.onReconnectstubMeteorStream is the right place for actual recovery, as the comment explains.


198-208: 💤 Low value

teardownAuthenticatedConnection closes the connection without resetting instance, and inflightLogin is not cleared during teardown.

After logout, the DDP socket is closed but instance still points to the same SDK object. The next call to getDdpSdk() returns that closed instance (L51 short-circuits on instance set), and ensureConnectedAndAuthenticated relies on the status check at L82 calling startConnect to revive it. However:

  • sdk.connection.connect() is called on a previously-close()d instance without creating a new SDK instance. The public @rocket.chat/ddp-client documentation recommends creating a new SDK instance via DDPSDK.create() or DDPSDK.createAndConnect() after closing, not reconnecting the same instance — whether the connection is idempotent after close is an undocumented implementation detail.
  • inflightLogin is not cleared during teardown (only in the finally block of L118–124). If a login request is in flight when logout occurs, it continues executing and can re-populate account.uid on the post-teardown instance even though account.user was cleared.

14-18: ⚡ Quick win

The URL transformation in computeDdpUrl correctly handles relative ROOT_URLs with subpaths.

DDPSDK.create() expects ws:// or wss:// URLs with host and optional path (as shown in DDPSDK tests calling DDPSDK.create('ws://localhost:1234') and the documented example 'wss://open.rocket.chat/websocket'). ConnectionImpl strips the protocol and appends /websocket internally, so passing wss://example.com/chat correctly resolves to wss://example.com/chat/websocket for subpath-mounted installs. The current regex transformation is appropriate for the documented DDPSDK API.

Comment on lines +178 to +204
void ensureConnectedAndAuthenticated()
.catch(() => undefined)
.then(() => {
if (stopped) return;
const sdk = getDdpSdk();
subscription = sdk.client.subscribe(`stream-${streamName}`, key, { useCollection: false, args });

subscription
.ready()
.then(() => {
if (stopped) return;
meta.ready = true;
ee.emit('ready', [undefined, { msg: 'ready', subs: [subscription!.id] }]);
})
.catch((err) => {
if (stopped) return;
ee.emit('ready', [err]);
ee.emit('error', err);
});

offCollection = sdk.client.onCollection(`stream-${streamName}`, (data: any) => {
if (data?.msg !== 'changed') return;
if (data.collection !== `stream-${streamName}`) return;
if (data.fields?.eventName !== key) return;
streamProxy.emit(`stream-${streamName}/${key}` as keyof EventMap, data.fields.args);
});
});

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

ready() can hang indefinitely if ensureConnectedAndAuthenticated() never settles.

The auth gate is wrapped in .catch(() => undefined).then(...), so a rejected auth still subscribes. But a pending auth (e.g., DDP socket never reaches connected because there's no timeout in waitForConnected) means the .then callback never runs, no subscription is created, and any caller awaiting stream.ready() waits forever — including useReactiveValue consumers that gate UI on subscription readiness.

Consider either:

  • propagating a connect/auth timeout into ee.emit('ready', [err]) so ready() rejects, or
  • not deferring the subscribe at all when sdk.connection.status === 'connected' && sdk.account.uid (most post-login subscription calls), and only deferring on the explicitly-anonymous path.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/app/utils/client/lib/SDKClient.ts` around lines 178 - 204, The
current flow calls ensureConnectedAndAuthenticated().catch(() =>
undefined).then(...) which can hang if ensureConnectedAndAuthenticated() never
settles; update the logic in the subscribe block to race
ensureConnectedAndAuthenticated() with a timeout and handle rejection so ready()
always resolves or rejects: wrap the call to ensureConnectedAndAuthenticated()
(used before calling getDdpSdk(), creating subscription and setting up
offCollection) in a Promise.race with a short timeout, and on timeout or
rejection emit ee.emit('ready', [err]) and ee.emit('error', err) (and return
early so no subscription is created); alternatively, if
getDdpSdk().connection.status === 'connected' && getDdpSdk().account.uid is
true, bypass the wait and subscribe immediately — adjust the code around
ensureConnectedAndAuthenticated(), getDdpSdk(), subscription, and the ready()
handlers accordingly.

Comment on lines +53 to +56
// Bumped from 18 → 19 to invalidate caches populated before the DDPSDK
// wire encoding was switched from JSON to EJSON. Entries written by the
// JSON window stored dates as ISO strings instead of Date instances, so
// fields like subscription.ls would fail `.getTime()` when read back.

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove inline implementation commentary in this block.

The new multi-line rationale comment should be moved to PR/changeset docs, keeping implementation comment-free per repo rule.

Proposed change
-	// Bumped from 18 → 19 to invalidate caches populated before the DDPSDK
-	// wire encoding was switched from JSON to EJSON. Entries written by the
-	// JSON window stored dates as ISO strings instead of Date instances, so
-	// fields like subscription.ls would fail `.getTime()` when read back.
 	private readonly version = 19;

As per coding guidelines, **/*.{ts,tsx,js}: Avoid code comments in implementation.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Bumped from 18 → 19 to invalidate caches populated before the DDPSDK
// wire encoding was switched from JSON to EJSON. Entries written by the
// JSON window stored dates as ISO strings instead of Date instances, so
// fields like subscription.ls would fail `.getTime()` when read back.
private readonly version = 19;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/lib/cachedStores/CachedStore.ts` around lines 53 - 56,
Delete the multi-line implementation comment in CachedStore.ts (the block
explaining the bump from 18 → 19 and the EJSON/JSON rationale) so the file
contains no inline implementation commentary; instead, move that explanation
into the PR description or the changeset/changelog for this change (reference:
the CachedStore.ts comment block near the version bump and any nearby comment
tokens mentioning "18 → 19" and "EJSON/JSON" so you remove the correct text).

Comment on lines +74 to 81
// With the SDK transport on, login can land via REST (ddpOverREST) without
// filling Meteor.users — Accounts.onLogin's autorun would never fire.
// Bridge off userIdStore as belt-and-braces. With the flag off, the legacy
// DDP path populates Meteor.users and Accounts.onLogin fires reliably; the
// extra userIdStore subscription would just double-fire callbacks.
const accountsSubscription = Accounts.onLogin(handler);
const stopUserIdSubscription = sdkTransportEnabled ? subscribeToLogin(handler) : undefined;
if (isLoggedIn()) handler();

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

onLoggedIn now fires twice for one successful login.

With SDK transport enabled, Line 79 and Line 80 subscribe to two different sources for the same login transition. This PR already had to add syncOnce in apps/meteor/client/startup/startup.ts to mask that duplication there; other onLoggedIn callers will still double-run their side effects.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/lib/loggedIn.ts` around lines 74 - 81, Both
Accounts.onLogin(handler) and subscribeToLogin(handler) can register the same
handler when sdkTransportEnabled is true, causing onLoggedIn to fire twice;
change the subscription logic so only one source is registered: create
accountsSubscription = Accounts.onLogin(handler) only when sdkTransportEnabled
is false, and otherwise use stopUserIdSubscription = subscribeToLogin(handler)
(i.e., make the Accounts.onLogin registration conditional on
!sdkTransportEnabled). Keep the isLoggedIn() immediate check and ensure variable
names accountsSubscription and stopUserIdSubscription remain unchanged.

Comment on lines +40 to +48
const waitForConnected = (sdk: DDPSDK): Promise<void> => {
if (sdk.connection.status === 'connected') return Promise.resolve();
return new Promise<void>((resolve) => {
const stop = sdk.connection.on('connected', () => {
stop();
resolve();
});
});
};

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, let's read the specific file and lines mentioned
head -60 apps/meteor/client/lib/sdk/ddpSdk.ts | tail -25

Repository: RocketChat/Rocket.Chat

Length of output: 852


🏁 Script executed:

# Let's see the full context around those lines
sed -n '30,60p' apps/meteor/client/lib/sdk/ddpSdk.ts

Repository: RocketChat/Rocket.Chat

Length of output: 1129


🏁 Script executed:

# Search for the Connection class definition and its event API
rg "class Connection|\.on\(" apps/meteor/client/lib/sdk/ddpSdk.ts -A 3

Repository: RocketChat/Rocket.Chat

Length of output: 151


🏁 Script executed:

# Check what status values are possible and if 'failed' is a real status
rg "failed|connected" apps/meteor/client/lib/sdk/ddpSdk.ts -B 2 -A 2

Repository: RocketChat/Rocket.Chat

Length of output: 1795


🏁 Script executed:

# Look for all uses of waitForConnected to understand impact
rg "waitForConnected" apps/meteor/client/lib/sdk/ddpSdk.ts

Repository: RocketChat/Rocket.Chat

Length of output: 155


🏁 Script executed:

# Search for Connection implementation or types in the codebase
fd -e ts -e tsx | xargs rg "connection.*status|emitStatus" | head -20

Repository: RocketChat/Rocket.Chat

Length of output: 1894


🏁 Script executed:

# Read the Connection class to understand the event API
sed -n '1,100p' packages/ddp-client/src/Connection.ts | head -80

Repository: RocketChat/Rocket.Chat

Length of output: 1543


🏁 Script executed:

# Find emitStatus and surrounding context to understand how events work
rg -B 5 -A 5 "private emitStatus" packages/ddp-client/src/Connection.ts

Repository: RocketChat/Rocket.Chat

Length of output: 280


🏁 Script executed:

# Check the event listener signature - what does .on() return
rg -B 3 -A 3 "\.on\(" packages/ddp-client/src/Connection.ts | head -30

Repository: RocketChat/Rocket.Chat

Length of output: 204


🏁 Script executed:

# Find all places waitForConnected is called
rg "waitForConnected" apps/meteor/client/lib/sdk/ddpSdk.ts -B 5 -A 10

Repository: RocketChat/Rocket.Chat

Length of output: 730


🏁 Script executed:

# Check if there's a distinction between 'connected' and 'connection' events
rg "'connected'|'connection'" packages/ddp-client/src/Connection.ts | head -20

Repository: RocketChat/Rocket.Chat

Length of output: 696


🏁 Script executed:

# Check the complete context where waitForConnected is called
rg -B 10 -A 15 "await waitForConnected" apps/meteor/client/lib/sdk/ddpSdk.ts

Repository: RocketChat/Rocket.Chat

Length of output: 864


🏁 Script executed:

# Verify the Emitter types to understand event signatures
rg "extends Emitter" packages/ddp-client/src/Connection.ts -A 10

Repository: RocketChat/Rocket.Chat

Length of output: 451


🏁 Script executed:

# Check if there's error handling around the waitForConnected call
rg "ensureConnectedAndAuthenticated|waitForConnected" apps/meteor/client/lib/sdk/ddpSdk.ts -B 2 -A 8

Repository: RocketChat/Rocket.Chat

Length of output: 3149


🏁 Script executed:

# Look for any tests or documentation about Connection status transitions
rg "failed|'connection'" packages/ddp-client/src/Connection.ts -B 2 -A 2 | head -50

Repository: RocketChat/Rocket.Chat

Length of output: 1250


🏁 Script executed:

# Check what happens after 'failed' status - is there auto-recovery
rg -A 20 "this.status = 'failed'" packages/ddp-client/src/Connection.ts

Repository: RocketChat/Rocket.Chat

Length of output: 819


🏁 Script executed:

# Verify the flow after 'failed' and 'disconnected' events
rg "failed|disconnect" packages/ddp-client/src/Connection.ts | grep -E "(emit|status)" | head -30

Repository: RocketChat/Rocket.Chat

Length of output: 257


🏁 Script executed:

# Check if startConnect is called in any retry or recovery logic
rg "startConnect" apps/meteor/client/lib/sdk/ddpSdk.ts -B 5 -A 10 | head -60

Repository: RocketChat/Rocket.Chat

Length of output: 1522


waitForConnected hangs indefinitely when DDP connection fails — add rejection handler for terminal failure status.

The function only listens for the 'connected' event. If the WebSocket handshake fails (server sends 'failed' message or connection drops during initialization), the Connection emits a 'connection' event with status 'failed' and then 'disconnected', but waitForConnected never resolves or rejects. This leaves ensureConnectedAndAuthenticated() and all its callers (userIdStore.subscribe at L282, login flows) indefinitely suspended.

The suggested fix correctly listens to the 'connection' event and rejects when status becomes 'failed':

🔧 Suggested fix — handle terminal failure
 const waitForConnected = (sdk: DDPSDK): Promise<void> => {
 	if (sdk.connection.status === 'connected') return Promise.resolve();
-	return new Promise<void>((resolve) => {
-		const stop = sdk.connection.on('connected', () => {
-			stop();
-			resolve();
-		});
-	});
+	return new Promise<void>((resolve, reject) => {
+		const offConnected = sdk.connection.on('connected', () => {
+			offConnected();
+			offFailed();
+			resolve();
+		});
+		const offFailed = sdk.connection.on('connection', (status) => {
+			if (status === 'failed') {
+				offConnected();
+				offFailed();
+				reject(new Error('DDPSDK connection failed'));
+			}
+		});
+	});
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/lib/sdk/ddpSdk.ts` around lines 40 - 48, waitForConnected
currently only listens for the 'connected' event and can hang if the DDP
handshake fails; update waitForConnected (function waitForConnected) to also
check sdk.connection.status for 'failed' up-front and add a 'connection' event
listener that rejects the Promise when status === 'failed' (or any terminal
failure), ensuring you clean up both listeners (the 'connected' and 'connection'
handlers) when resolving or rejecting so no handlers leak.

Comment on lines +73 to +124
export const ensureConnectedAndAuthenticated = async (): Promise<void> => {
const sdk = getDdpSdk();

// IMPORTANT: must wait for the DDP `connected` handshake before issuing
// any wait-method (login uses wait:true). DDPDispatcher serializes wait
// blocks at the queue head, so a login dispatched while connecting
// queues ahead of the connect frame ws.onopen later emits — the connect
// frame ends up wedged in a non-wait block behind the wait block and
// never flushes, leaving the socket open but DDP-unhandshaked.
if (
sdk.connection.status === 'idle' ||
sdk.connection.status === 'closed' ||
sdk.connection.status === 'disconnected' ||
sdk.connection.status === 'failed'
) {
void startConnect(sdk);
}
await waitForConnected(sdk);

const token = readStoredLoginToken();
if (!token || sdk.account.uid) {
return;
}

if (inflightLogin) {
await inflightLogin;
return;
}

// Give Meteor's own login flow (resume routed through stubMeteorStream
// + adoptAccountFromMeteorLoginResult) time to populate sdk.account
// before we issue our own loginWithToken. If adopt fires first, we can
// short-circuit and avoid sending a second login frame on the SDK
// socket — which would otherwise create a duplicate Presence
// connection (processConnectionStatus prefers ONLINE over AWAY in the
// aggregate, breaking the auto-away flow). 500ms covers a single
// server roundtrip in CI; if the stub-routed login hasn't completed by
// then, fall back to issuing our own loginWithToken below.
for (let i = 0; i < 20 && !sdk.account.uid; i++) {
await new Promise<void>((resolve) => setTimeout(resolve, 25));
}
if (sdk.account.uid) {
return;
}

inflightLogin = (async () => {
try {
await sdk.account.loginWithToken(token);
} finally {
inflightLogin = undefined;
}
})();

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Race in inflightLogin gating — two concurrent callers can each fire a loginWithToken.

The intent (per the comments at L102–110) is that only one login goes out at a time, but the gate at L97 is before the 500 ms busy-wait at L111–113. Two concurrent callers interleave like this:

  1. Call A awaits waitForConnected (L90) → yields.
  2. Call B awaits waitForConnected → yields.
  3. A resumes; inflightLogin is undefined; falls through L97.
  4. A enters the busy-wait loop and awaits setTimeout (L112) → yields.
  5. B resumes; inflightLogin still undefined (A hasn't reached L118); falls through L97.
  6. B also enters the busy-wait, eventually reaches L118 — both A and B set their own inflightLogin and dispatch a login frame.

That's exactly the duplicate-Presence-session scenario the surrounding comments warn about (processConnectionStatus ends up with two entries, breaking auto-away). Re-check inflightLogin after the busy-wait, or move the inflightLogin = ... assignment up before the first await.

🔧 Suggested fix
 	for (let i = 0; i < 20 && !sdk.account.uid; i++) {
 		await new Promise<void>((resolve) => setTimeout(resolve, 25));
 	}
 	if (sdk.account.uid) {
 		return;
 	}
+	if (inflightLogin) {
+		await inflightLogin;
+		return;
+	}

 	inflightLogin = (async () => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/lib/sdk/ddpSdk.ts` around lines 73 - 124, The race occurs
because inflightLogin is checked before the 500ms busy-wait, allowing two
callers to both pass the check and each start loginWithToken; modify
ensureConnectedAndAuthenticated to re-check inflightLogin immediately after the
busy-wait (and return/await it if set) before assigning a new inflightLogin, or
alternatively assign inflightLogin to a promise placeholder before the busy-wait
so only the first caller creates the actual login promise; reference
ensureConnectedAndAuthenticated, inflightLogin, sdk.account.loginWithToken,
waitForConnected and startConnect when applying the fix.

Comment on lines +17 to +21
type SubscribeCallbacks = {
onReady?: () => void;
onError?: (err: Error) => void;
onStop?: (err?: Error) => void;
};

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

❓ Verification inconclusive

Meteor.subscribe callbacks onStop error nosub official docs


Meteor.subscribe callbacks: use onStop (not onStop “error” vs “nosub” separately). 1) What onStop does (officially) - Meteor.subscribe accepts an optional callbacks object that may include onStop and onReady.[1][2] - If the subscription fails or is terminated by the server, onStop is called with a Meteor.Error argument.[1][2] - If the subscription is stopped by calling stop on the subscription handle or inside the publication, onStop is called with no arguments.[1][2] 2) About “nosub” and the onStop error - The internal DDP “nosub” message corresponds to “subscription not found” / subscription termination conditions.[3] - In Meteor’s implementation and tests for subscribe errors, when a “nosub” comes back and the subscription is terminated, the client-side onStop receives the error (a Meteor.Error) as the optional first argument.[3] 3) Common “onStop error: nosub / callback error” gotchas - Don’t expect onStop to fire from an onError handler; onError was replaced by the more general onStop callback.[3] - If you stop a subscription in response to cleanup code (or if your publication calls this.stop / this.error), expect onStop to be called either with an error (server error/termination) or with no argument (client stop / in-publication stop).[1][2] If you paste the exact runtime error and the code around your Meteor.subscribe (callbacks object and where you call stop), I can pinpoint whether you’re hitting the expected “nosub -> onStop(error)” path versus a separate exception thrown inside your onStop callback.

Citations:


Script executed:

fd -t f "subscribeViaSDK.ts" --exec wc -l {}

Repository: RocketChat/Rocket.Chat


Repository: RocketChat/Rocket.Chat
Exit code: 0

stdout:

65 ./apps/meteor/client/meteor/overrides/subscribeViaSDK.ts


Script executed:

cat -n ./apps/meteor/client/meteor/overrides/subscribeViaSDK.ts

Repository: RocketChat/Rocket.Chat


Repository: RocketChat/Rocket.Chat
Exit code: 0

stdout:

     1	import { Meteor } from 'meteor/meteor';
     2	
     3	import { getDdpSdk } from '../../lib/sdk/ddpSdk';
     4	import { isSdkTransportEnabled } from '../../lib/sdk/sdkTransportEnabled';
     5	
     6	/**
     7	 * Route Meteor.connection.subscribe through DDPSDK so the few direct
     8	 * publications that Accounts and Meteor core still open
     9	 * (loginServiceConfiguration, meteor_autoupdate_clientVersions, ...)
    10	 * ride our single WebSocket instead of Meteor's.
    11	 *
    12	 * The collection frames produced by those subscriptions are re-fed into
    13	 * Meteor.connection._streamHandlers by ddpSdkCollectionBridge, so
    14	 * Meteor's collection dispatch keeps working unchanged.
    15	 */
    16	
    17	type SubscribeCallbacks = {
    18		onReady?: () => void;
    19		onError?: (err: Error) => void;
    20		onStop?: (err?: Error) => void;
    21	};
    22	
    23	const extractCallbacks = (args: unknown[]): { params: unknown[]; callbacks: SubscribeCallbacks } => {
    24		if (args.length === 0) return { params: [], callbacks: {} };
    25	
    26		const last = args[args.length - 1];
    27	
    28		if (typeof last === 'function') {
    29			return { params: args.slice(0, -1), callbacks: { onReady: last as () => void } };
    30		}
    31	
    32		if (
    33			last !== null &&
    34			typeof last === 'object' &&
    35			(typeof (last as any).onReady === 'function' ||
    36				typeof (last as any).onError === 'function' ||
    37				typeof (last as any).onStop === 'function')
    38		) {
    39			return { params: args.slice(0, -1), callbacks: last as SubscribeCallbacks };
    40		}
    41	
    42		return { params: args, callbacks: {} };
    43	};
    44	
    45	type MeteorSubscriptionHandle = Meteor.SubscriptionHandle;
    46	
    47	if (isSdkTransportEnabled()) {
    48		(Meteor.connection as any).subscribe = ((name: string, ...rest: unknown[]): MeteorSubscriptionHandle => {
    49			const { params, callbacks } = extractCallbacks(rest);
    50			const subscription = getDdpSdk().client.subscribe(name, ...params);
    51	
    52			subscription
    53				.ready()
    54				.then(() => callbacks.onReady?.())
    55				.catch((err: Error) => callbacks.onError?.(err));
    56	
    57			return {
    58				stop: () => {
    59					subscription.stop();
    60					callbacks.onStop?.();
    61				},
    62				ready: () => subscription.isReady,
    63			} as MeteorSubscriptionHandle;
    64		}) as Meteor.IMeteorConnection['subscribe'];
    65	}


Correct error handling to match Meteor's onStop(error) contract for failed subscriptions.

This override sends subscription failures only to onError (line 55), but Meteor callers typically observe subscription failures through onStop(error). Existing code implementing only onStop will silently miss errors when the SDK transport is enabled.

On subscription failure, call both onStop(err) and onError(err) to maintain backward compatibility with standard Meteor behavior while supporting SDK-aware callbacks.

Required fix
 		subscription
 			.ready()
 			.then(() => callbacks.onReady?.())
-			.catch((err: Error) => callbacks.onError?.(err));
+			.catch((err: Error) => {
+				callbacks.onStop?.(err);
+				callbacks.onError?.(err);
+			});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/meteor/overrides/subscribeViaSDK.ts` around lines 17 - 21,
The subscription failure handling in subscribeViaSDK (and its SubscribeCallbacks
type) currently only invokes onError(err), which breaks Meteor's expected
onStop(err) contract; update the failure path so it invokes both callbacks: call
onStop(err) first (to preserve Meteor semantics) and then onError(err) if
provided, ensuring you reference the SubscribeCallbacks.onStop and
SubscribeCallbacks.onError handlers used in subscribeViaSDK to locate and modify
the error branch.

Comment on lines +117 to +132
switch (sdkStatus) {
case 'connected':
return { status: 'connected', connected: true, ...retry };
case 'connecting':
return { status: 'connecting', connected: false, ...retry };
case 'reconnecting':
return { status: 'connecting', connected: false, ...retry };
case 'failed':
return { status: 'failed', connected: false, ...retry };
case 'closed':
case 'disconnected':
return { status: 'waiting', connected: false, ...retry };
case 'idle':
default:
return { status: 'offline', connected: false, ...retry };
}

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

sdkStatusToMeteor discards Meteor's own status when it's worse than the SDK's.

The PR comment at lines 102–104 says the goal is "the worst-case of both transports", but the implementation only ever maps sdkStatus and uses meteor solely for retryCount/retryTime. If the SDK is 'connected' but Meteor is 'waiting'/'failed' (e.g., the SDK socket recovered first), the bar will show 'connected' and hide the underlying Meteor outage — the opposite of worst-case.

If "worst-case" is the intent, fold Meteor's status in:

🔧 Suggested combine
 const getStatus = sdkTransportEnabled
 	? () => {
 			ddpSdkStatusDep!.depend();
-			return sdkStatusToMeteor(getDdpSdk().connection.status, Meteor.status());
+			const meteor = Meteor.status();
+			const sdk = sdkStatusToMeteor(getDdpSdk().connection.status, meteor);
+			return sdk.connected && meteor.connected ? sdk : !sdk.connected ? sdk : meteor;
 		}

If the intent is actually "SDK-first, Meteor only for retry metadata", clarify the comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/providers/ServerProvider.tsx` around lines 117 - 132,
sdkStatusToMeteor currently maps only sdkStatus and ignores Meteor's own status,
which violates the "worst-case of both transports" intent; update
sdkStatusToMeteor to consider both sdkStatus and the Meteor `status` (and still
include `retryCount`/`retryTime`) by computing a severity ranking for statuses
from both sources and returning the more severe result (e.g., map statuses to
numeric severity, compare sdkStatus vs. meteor.status, then return the
status/connected value corresponding to the worse severity), keeping the
existing shape returned (status, connected, ...retry) and referencing
sdkStatusToMeteor, sdkStatus, and the meteor variable in the implementation.

Comment on lines +43 to +65
const runUserDataSync = async (uid: string) => {
// synchronizeUserData opens a `stream-notify-user/${uid}/userData` sub
// over DDPSDK. The server rejects that sub with "not-allowed" until
// DDPSDK has completed loginWithToken on its own socket. Both
// runUserDataSync and ensureConnectedAndAuthenticated are subscribers
// of userIdStore, so without sequencing the sub races the auth and
// hits the rejection on every re-login. Await the SDK auth here so
// the sub fires authenticated.
try {
await ensureConnectedAndAuthenticated();
} catch {
// non-fatal: sdk.stream queues until DDPSDK eventually auths
}
const user = await synchronizeUserData(uid);
if (!user) return;

const utcOffset = -new Date().getTimezoneOffset() / 60;
if (user.utcOffset !== utcOffset) {
sdk.call('userSetUtcOffset', utcOffset);
}

emitStatusChange(user.status);
};

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard runUserDataSync against logout and user switches.

runUserDataSync(uid) awaits twice and then still calls userSetUtcOffset / emitStatusChange without re-checking that userIdStore.getState() still matches uid. A logout or fast re-login in the middle of those awaits can apply stale session data to the next session.

Suggested stale-request guard
 	const runUserDataSync = async (uid: string) => {
@@
 		try {
 			await ensureConnectedAndAuthenticated();
 		} catch {
 			// non-fatal: sdk.stream queues until DDPSDK eventually auths
 		}
+		if (userIdStore.getState() !== uid) {
+			return;
+		}
 		const user = await synchronizeUserData(uid);
-		if (!user) return;
+		if (!user || userIdStore.getState() !== uid) return;
 
 		const utcOffset = -new Date().getTimezoneOffset() / 60;
 		if (user.utcOffset !== utcOffset) {
 			sdk.call('userSetUtcOffset', utcOffset);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const runUserDataSync = async (uid: string) => {
// synchronizeUserData opens a `stream-notify-user/${uid}/userData` sub
// over DDPSDK. The server rejects that sub with "not-allowed" until
// DDPSDK has completed loginWithToken on its own socket. Both
// runUserDataSync and ensureConnectedAndAuthenticated are subscribers
// of userIdStore, so without sequencing the sub races the auth and
// hits the rejection on every re-login. Await the SDK auth here so
// the sub fires authenticated.
try {
await ensureConnectedAndAuthenticated();
} catch {
// non-fatal: sdk.stream queues until DDPSDK eventually auths
}
const user = await synchronizeUserData(uid);
if (!user) return;
const utcOffset = -new Date().getTimezoneOffset() / 60;
if (user.utcOffset !== utcOffset) {
sdk.call('userSetUtcOffset', utcOffset);
}
emitStatusChange(user.status);
};
const runUserDataSync = async (uid: string) => {
// synchronizeUserData opens a `stream-notify-user/${uid}/userData` sub
// over DDPSDK. The server rejects that sub with "not-allowed" until
// DDPSDK has completed loginWithToken on its own socket. Both
// runUserDataSync and ensureConnectedAndAuthenticated are subscribers
// of userIdStore, so without sequencing the sub races the auth and
// hits the rejection on every re-login. Await the SDK auth here so
// the sub fires authenticated.
try {
await ensureConnectedAndAuthenticated();
} catch {
// non-fatal: sdk.stream queues until DDPSDK eventually auths
}
if (userIdStore.getState() !== uid) {
return;
}
const user = await synchronizeUserData(uid);
if (!user || userIdStore.getState() !== uid) return;
const utcOffset = -new Date().getTimezoneOffset() / 60;
if (user.utcOffset !== utcOffset) {
sdk.call('userSetUtcOffset', utcOffset);
}
emitStatusChange(user.status);
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/startup/startup.ts` around lines 43 - 65, runUserDataSync
can apply stale session data after awaiting; after any await points (notably
after ensureConnectedAndAuthenticated() and synchronizeUserData(uid)) re-check
that userIdStore.getState() === uid (and that user is still present) and return
early if it changed; only then call sdk.call('userSetUtcOffset', ...) and
emitStatusChange(user.status) to avoid leaking updates into a different or
logged-out session.

Comment on lines +20 to +25
if (!sdkTransportEnabled) {
// Flag off: develop's exact behaviour — only set the session flag.
// The legacy WS-reconnect → loginWithToken-fails → accounts-base
// chain handles the actual local cleanup.
return;
}

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not skip local credential cleanup on the legacy branch

Returning early on Line 20 means Meteor.loginToken/userId can remain populated after force_logout until a later reconnect path runs. That creates stale-auth races (notably around startup sync/reset logic) and can leave the client in an inconsistent logged-in state.

Proposed fix
-			if (!sdkTransportEnabled) {
-				// Flag off: develop's exact behaviour — only set the session flag.
-				// The legacy WS-reconnect → loginWithToken-fails → accounts-base
-				// chain handles the actual local cleanup.
-				return;
-			}
+			// Always clear local auth artifacts immediately on force logout.
+			// This avoids stale session races while reconnect/logout chains settle.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/views/root/hooks/loggedIn/useForceLogout.ts` around lines
20 - 25, The early return in useForceLogout (the branch checking
sdkTransportEnabled) skips local credential cleanup and leaves
Meteor.loginToken/userId set; remove the early return and ensure the legacy
branch still performs the same local cleanup steps (clear
Meteor._localStorage/Meteor.loginToken and Meteor.userId, and call the same
local logout/cleanup functions used in the SDK path) when handling the
force_logout path in useForceLogout so credentials are cleared immediately even
if sdkTransportEnabled is false.

Comment on lines +63 to +72
// Graceful close: lets the WS lib flush queued frames (including
// the `notify-user/<uid>/force_logout` stream message that the
// monolith listener at apps/meteor/server/modules/listeners/listeners.module.ts:49
// just enqueued) before the socket goes down. Previously this was
// `ws.terminate()`, which sends a TCP RST immediately and drops
// the queued frames — clients depending on the stream message
// (useForceLogout hook → Accounts._unstoreLoginToken + setUserId(null))
// then never see the cleanup, leaving stale credentials in
// localStorage. Falls back to terminate() after a short grace
// period for unresponsive sockets.

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Move this rationale out of implementation code.

Please remove the long inline explanation and keep the behavior self-evident in code; rationale can live in PR/changeset docs.

Proposed change
-					// Graceful close: lets the WS lib flush queued frames (including
-					// the `notify-user/<uid>/force_logout` stream message that the
-					// monolith listener at apps/meteor/server/modules/listeners/listeners.module.ts:49
-					// just enqueued) before the socket goes down. Previously this was
-					// `ws.terminate()`, which sends a TCP RST immediately and drops
-					// the queued frames — clients depending on the stream message
-					// (useForceLogout hook → Accounts._unstoreLoginToken + setUserId(null))
-					// then never see the cleanup, leaving stale credentials in
-					// localStorage. Falls back to terminate() after a short grace
-					// period for unresponsive sockets.
 					ws.close();

As per coding guidelines, **/*.{ts,tsx,js}: Avoid code comments in implementation.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Graceful close: lets the WS lib flush queued frames (including
// the `notify-user/<uid>/force_logout` stream message that the
// monolith listener at apps/meteor/server/modules/listeners/listeners.module.ts:49
// just enqueued) before the socket goes down. Previously this was
// `ws.terminate()`, which sends a TCP RST immediately and drops
// the queued frames — clients depending on the stream message
// (useForceLogout hook → Accounts._unstoreLoginToken + setUserId(null))
// then never see the cleanup, leaving stale credentials in
// localStorage. Falls back to terminate() after a short grace
// period for unresponsive sockets.
ws.close();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ee/apps/ddp-streamer/src/DDPStreamer.ts` around lines 63 - 72, The large
inline rationale in DDPStreamer (around the socket shutdown logic that uses
ws.close() with a fallback to ws.terminate()) should be removed from
implementation code; replace the block comment with a concise one-line note like
"Graceful close with terminate fallback" or delete the comment entirely, and
move the detailed explanation into the PR/changelog. Locate the socket shutdown
code in the DDPStreamer class (the section referencing ws.close() and
ws.terminate()) and update the comment accordingly while leaving the existing
behavior (close then terminate fallback) unchanged.

@codecov

codecov Bot commented May 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 20.86570% with 713 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.57%. Comparing base (281c22a) to head (57e1e2d).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop   #40430      +/-   ##
===========================================
- Coverage    69.99%   69.57%   -0.42%     
===========================================
  Files         3309     3317       +8     
  Lines       120956   121867     +911     
  Branches     21683    21855     +172     
===========================================
+ Hits         84666    84794     +128     
- Misses       32990    33744     +754     
- Partials      3300     3329      +29     
Flag Coverage Δ
e2e 58.88% <14.28%> (-0.78%) ⬇️
e2e-api 46.29% <100.00%> (-0.94%) ⬇️
unit 70.42% <22.99%> (-0.33%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ggazzo ggazzo force-pushed the fix/sdk-v2-totp-and-status-regressions branch from ed8edd9 to 57e1e2d Compare May 6, 2026 22:05
@ggazzo

ggazzo commented May 6, 2026

Copy link
Copy Markdown
Member Author

I'm will this one, even without approve, since it was approved before here: #40301

I also created a test for this issue: #40431

@ggazzo ggazzo merged commit dbce434 into develop May 6, 2026
67 of 86 checks passed
@ggazzo ggazzo deleted the fix/sdk-v2-totp-and-status-regressions branch May 6, 2026 23:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: feature Pull requests that introduces new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant