Skip to content

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

Merged
ggazzo merged 14 commits into
developfrom
worktree-sdk-over-ddp-client
May 6, 2026
Merged

chore(@rocket.chat/ddp-client): introduce experimental Meteor independent DDP client#40301
ggazzo merged 14 commits into
developfrom
worktree-sdk-over-ddp-client

Conversation

@ggazzo

@ggazzo ggazzo commented Apr 24, 2026

Copy link
Copy Markdown
Member

Note

This is an intermediate step towards completely removing the use of the meteor client (auth, oauth, ddp, tracker, etc.), all this code must be removed at once after we have migrated everything to the SDK, so even if not everything is perfect or makes complete sense, it should only last a short time. That's why it's under a flag, if the original behavior is affected we must revert everything and adjust before reinserting

Summary

Migrates the frontend DDP transport from Meteor's WebSocket to our own
@rocket.chat/ddp-client SDK, gated behind a runtime flag for staged
rollout. The migration ships dormant by default — legacy Meteor DDP
stays the active transport — and can be opted into per-tab, per-user,
or per-workspace. Meteor Accounts stays as the auth anchor.

Runtime flag — Use_RC_SDK

Three sources resolve the flag in order; default off if none match:

  1. URL parameter ?sdk_transport=on|off — per-tab override.
  2. rc-config-sdk_transport in localStorage — persisted per-user.
  3. <meta name="rc-sdk-transport-enabled"> injected from the new
    Use_RC_SDK admin setting (General → Use Rocket.Chat SDK) — global
    opt-in / kill-switch.

apps/meteor/client/lib/sdk/sdkTransportEnabled.ts is the single
source of truth; the rest of the codebase reads it once at module init
and dispatches accordingly.

What the flag gates

When the flag is off, getDdpSdk() returns a Meteor-backed
pass-through proxy (apps/meteor/client/lib/sdk/meteorBackedSdk.ts)
with the same DDPSDK shape — client.subscribe
Meteor.connection.subscribe, client.callAsync
Meteor.callAsync, account.uidMeteor.userId(),
connection.status derived from Meteor.status() via Tracker autorun.
Consumers (ServerProvider, presence.ts, SDKClient, the override
modules) keep calling getDdpSdk() unconditionally and behave like
develop without any per-call-site flag checks. No second WebSocket is
opened, no auth lifecycle runs, no Presence session is duplicated
server-side.

When the flag is on, getDdpSdk() returns the real DDPSDK
instance and the five transport overrides install:

  • ddpSdkCollectionBridge — re-feeds DDPSDK collection frames into
    Meteor's _streamHandlers.onMessage.
  • subscribeViaSDK — routes Meteor.connection.subscribe through
    sdk.client.subscribe.
  • stubMeteorStream — replaces Meteor.connection._stream with a
    stub that forwards outbound frames through the SDK socket.
  • killMeteorStream — drains revival/quiescence bookkeeping and
    outstanding-method blocks on logout.
  • ddpOverREST — wraps Meteor.connection._send so non-bypassed
    methods route via REST. Always on (matches develop's behavior),
    not gated by the flag — the resume-only login handler in
    ddp-streamer-service 403s every other login shape, and routing via
    REST is the only way the form login flow works in microservices.

Other always-on changes

These are server-side or package-internal fixes that apply regardless
of the flag:

  • ee/apps/ddp-streamer/src/DDPStreamer.tsws.close() (graceful)
    on user.forceLogout instead of ws.terminate(), with a 5s fallback
    to terminate. Lets the notify-user/<uid>/force_logout stream
    message reach the client before the socket goes down.
  • packages/ddp-client/src/Connection.ts — idempotent
    connect()/reconnect(), retryCount reset on successful connection,
    stale ws.onclose ignored when a newer ws has taken over, retry
    timer re-checks status before firing.
  • packages/ddp-client/src/DDPDispatcher.ts — non-method frames
    bypass the wait queue so connect/sub/ping aren't queued behind
    a wait login block.

Boot-time flag-gated bits

  • ddpSdk.ts side-effect block (loginWithToken auth-error wrap,
    userIdStore subscriber, ensureConnectedAndAuthenticated boot path).
  • app/notifications/client/lib/Presence.ts — only registers the
    user-presence streamer on the SDK socket when the flag is on. With
    flag off, only Meteor.connection's streamer runs.
  • loggedIn.tswhenLoggedIn / onLoggedIn use both
    Accounts.onLogin and a userIdStore bridge when flag on (REST-
    routed login can't fill Meteor.users); with flag off they match
    develop's Accounts.onLogin-only behavior.
  • useForceLogout.ts — only clears local credentials manually when
    flag on; with flag off the legacy WS-reconnect-then-relogin chain
    handles cleanup.
  • startup.tsrunUserDataSync awaits SDK auth before
    synchronizeUserData only when flag on.

Test plan

  • TypeScript passes across the meteor workspace.
  • Local: flag on (?sdk_transport=on) — login, logout,
    account-manage-devices, admin-room, account-profile all pass on
    monolith.
  • Local: flag off (default) — same suites pass on monolith
    (bit-for-bit develop behavior).
  • CI: full E2E + API + Unit green.
  • Manual (flag on): inspect DevTools → Network → WS. Two sockets
    should be open after login — Meteor's stays present as the
    Accounts anchor, DDPSDK carries every method/subscription.
  • Manual (flag on): login with password, logout, SAML login, LDAP
    login still work end-to-end. Session resume on reload goes
    straight into the app.
  • Manual (flag on): user-presence updates from another tab appear
    in near-realtime.
  • Manual (flag on): ConnectionStatusBar reflects DDPSDK outages —
    disable the DDPSDK socket via DevTools and verify the offline
    banner appears.
  • Manual (flag off): the workspace behaves exactly like develop —
    no SDK socket opens, no proxy WebSocket activity.

Task: ARCH-2127

Summary by CodeRabbit

  • New Features

    • Added experimental "Use Rocket.Chat SDK" setting in workspace administration. Browser restart required after toggling this feature.
  • Bug Fixes

    • Improved graceful shutdown handling during forced user logout.
  • Tests

    • Enhanced transport-agnostic message delivery validation in E2E tests.

@dionisio-bot

dionisio-bot Bot commented Apr 24, 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 Apr 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 95ecdba

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 Apr 24, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This PR introduces an experimental SDK-over-DDP transport feature that routes Meteor DDP traffic through @rocket.chat/ddp-client over a single WebSocket connection instead of Meteor's legacy stream. The feature is controlled by a new admin setting Use_RC_SDK, with client-side precedence for URL parameters and localStorage. The implementation spans feature-flag detection, a DDPSDK singleton with Meteor authentication integration, transport layer stubbing, subscription/stream routing, login/presence coordination, and E2E test updates.

Changes

SDK-over-DDP Transport Migration

Layer / File(s) Summary
Feature Flag & Configuration
apps/meteor/client/lib/sdk/sdkTransportEnabled.ts, apps/meteor/server/settings/general.ts, apps/meteor/app/ui-master/server/index.ts, .changeset/..., packages/i18n/src/locales/en.i18n.json
Runtime feature-flag detection with URL/localStorage/meta precedence. New server setting Use_RC_SDK with i18n strings and meta tag injection.
Core SDK Instance & Connection Management
apps/meteor/client/lib/sdk/ddpSdk.ts, apps/meteor/package.json
DDPSDK singleton creation with WS URL computation, EJSON encoding/decoding, connection lifecycle (startConnect, waitForConnected), and Meteor token-based authentication with auto-retry and error handling.
Meteor-Backed Fallback
apps/meteor/client/lib/sdk/meteorBackedSdk.ts
Fallback DDPSDK implementation delegating to Meteor.connection for environments where SDK transport is disabled.
Stream & Publish Layer
apps/meteor/app/utils/client/lib/SDKClient.ts, apps/meteor/client/lib/sdk/streamerAdapter.ts
Conditional stream creation (createNewDdpSdkStream vs. legacy), SDK adapter for streamer integration, and publish routing through SDK or Meteor.
Transport Overrides
apps/meteor/client/meteor/overrides/stubMeteorStream.ts, apps/meteor/client/meteor/overrides/subscribeViaSDK.ts, apps/meteor/client/meteor/overrides/ddpSdkCollectionBridge.ts, apps/meteor/client/meteor/overrides/killMeteorStream.ts
Stub Meteor's stream when SDK enabled, redirect subscriptions through SDK, bridge DDPSDK frames to Meteor handlers, reset Meteor DDP state for revival/quiescence.
DDP Protocol Adjustments
apps/meteor/client/meteor/overrides/ddpOverREST.ts
Refined resume-login detection and DDP-over-REST error normalization to ensure consistent error encoding.
Login & Session Management
apps/meteor/client/lib/loggedIn.ts, apps/meteor/client/lib/presence.ts, apps/meteor/client/views/root/hooks/loggedIn/useForceLogout.ts, apps/meteor/client/lib/sdk/ddpSdk.ts
SDK-aware login detection via userIdStore, presence subscription routing, force-logout cleanup, and adoption of Meteor login results into DDPSDK account state.
Status & Method Integration
apps/meteor/client/providers/ServerProvider.tsx, apps/meteor/client/startup/startup.ts
SDK-aware disconnect/reconnect/status logic, method call routing through SDK, and user data sync coordination with deduplication and retry safety.
Infrastructure & Startup
apps/meteor/app/notifications/client/lib/Presence.ts, apps/meteor/client/lib/cachedStores/CachedStore.ts, apps/meteor/client/main.ts
Server-side presence streamer adapter setup, cache version bump for EJSON encoding change, and DDP SDK module initialization.
Server-side Graceful Shutdown
ee/apps/ddp-streamer/src/DDPStreamer.ts
Graceful WebSocket close-before-terminate flow for force-logout to flush queued frames.
Tests & Documentation
apps/meteor/tests/e2e/page-objects/fragments/home-content.ts
E2E test refactoring to use DOM-based settling instead of HTTP response parsing, making tests transport-agnostic.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

This PR introduces a substantial and intricate architectural migration affecting authentication, subscription, streaming, and DDP protocol layers across client and server. The changes span multiple new SDK modules, numerous client overrides, and complex interactions between Meteor's legacy transport, DDPSDK, and login/presence flows. Each logical section introduces dense logic with specific retry semantics, error handling, and state management that requires careful reasoning to ensure compatibility with both SDK-enabled and fallback Meteor-backed modes.

Suggested labels

type: feature, scope: core, area: transport

🚥 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
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.
Title check ✅ Passed The PR title 'chore(@rocket.chat/ddp-client): introduce experimental Meteor independent DDP client' accurately summarizes the main change: introducing an experimental SDK-based DDP client transport to replace Meteor's native implementation, gated by a feature flag.

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

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • ARCH-2127: Request failed with status code 401

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 changed the title chore: Worktree sdk over ddp client feat(sdk): migrate frontend transport to @rocket.chat/ddp-client (phase 2) Apr 24, 2026
@codecov

codecov Bot commented Apr 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 20.46980% with 711 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.58%. Comparing base (55b837b) to head (95ecdba).

Additional details and impacted files

Impacted file tree graph

@@                           Coverage Diff                            @@
##           fix/ddp-dispatcher-non-method-frames   #40301      +/-   ##
========================================================================
- Coverage                                 69.96%   69.58%   -0.39%     
========================================================================
  Files                                      3309     3309              
  Lines                                    120994   121346     +352     
  Branches                                  21805    21730      -75     
========================================================================
- Hits                                      84659    84439     -220     
- Misses                                    33026    33582     +556     
- Partials                                   3309     3325      +16     
Flag Coverage Δ
e2e 58.81% <13.53%> (-0.83%) ⬇️
e2e-api 46.22% <100.00%> (-0.14%) ⬇️
unit 70.51% <22.99%> (-0.24%) ⬇️

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 worktree-sdk-over-ddp-client branch 3 times, most recently from e0d347f to de8a406 Compare April 24, 2026 19:38
@ggazzo ggazzo changed the title feat(sdk): migrate frontend transport to @rocket.chat/ddp-client (phase 2) feat(sdk): replace Meteor DDP transport with @rocket.chat/ddp-client Apr 24, 2026
@ggazzo ggazzo force-pushed the worktree-sdk-over-ddp-client branch 6 times, most recently from 4dfa46d to 56c5e1e Compare April 29, 2026 19:33
@ggazzo

ggazzo commented Apr 29, 2026

Copy link
Copy Markdown
Member Author

@copilot resolve the merge conflicts in this pull request

Copilot AI commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Merge conflicts resolved in d51c070. The only conflict was in apps/meteor/package.json — kept @rocket.chat/ddp-client: "workspace:^" from this branch and took the updated @rocket.chat/css-in-js: "^0.32.0" from develop.

@ggazzo ggazzo force-pushed the worktree-sdk-over-ddp-client branch 3 times, most recently from d24843c to c2d763d Compare May 4, 2026 17:54
@CLAassistant

CLAassistant commented May 4, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@ggazzo ggazzo changed the base branch from develop to fix/ddp-dispatcher-non-method-frames May 4, 2026 17:54
ggazzo and others added 5 commits May 4, 2026 14:59
DDPDispatcher's pushItem path runs for every payload that goes through
dispatch — including connect, sub, unsub, and ping/pong. When the head
of the queue is a wait block (e.g. a login), pushItem deliberately
returns without sending so the next method waits. Applied to a non-
method payload, that means the connect frame ws.onopen emits gets
dropped on the floor: the socket sits open, the DDP handshake never
happens, and any caller waiting on `connected` hangs forever.

Visible failure mode: token-resume on page reload dispatched the wait
login while the socket was still connecting; when ws.onopen later
fired the connect frame, it was queued behind that login and never
sent. Status stuck at "connecting".

Fix: short-circuit dispatch for any payload whose msg is not 'method'
— emit it straight away. Wait/queue serialization is a property of
method calls, not protocol frames.
Previously both rejected with `Error('Connection in progress')` when
called against an already-connecting/connected state. That's hostile to
the only callers we have:

- The retry timer scheduled by `ws.onclose` fires `void this.reconnect()`
  with no `.catch`. If an external `connect()` won the race, the timer
  rejected and the unhandled rejection surfaced as a pageError on every
  reconnect cycle.
- Bootstrap paths in SDK consumers (e.g. apps/meteor/client/lib/sdk/ddpSdk.ts)
  already wrap in `.catch(() => {})` purely to silence the same noise.

Make both methods resolve with the current status when the connection is
already in flight or established, and have them clear the pending retry
timer on entry. The retry timer's callback also re-checks the status
before calling `reconnect()` so a connection re-established in the
meantime doesn't trigger a redundant reset cycle.

Also short-circuit `ws.onclose` when the closed socket isn't `this.ws`
anymore — a stale close from a replaced socket otherwise flipped
`status` back to `disconnected`, bumped `retryCount`, and scheduled a
fresh timer while a newer socket was healthy.

Tests cover all four shapes:
  - reconnect/connect when already connected → resolves, doesn't throw
  - retry timer firing after an external connect won the race → no
    unhandled rejection, status stays connected
  - stale ws.onclose after socket replacement → status and retryCount
    untouched, no extra timer scheduled

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion to the 'idempotent connect/reconnect' fix in 7742877.
retryCount was incremented on every disconnect but never zeroed on
successful (re)connect. With the default budget of 1, a single
force-logout cycle that chains back-to-back ws.close events (server
broadcasts force_logout → SDK reconnects → app calls Meteor.logout()
on top → server's logout handler closes the WS again) drained the
budget. The next disconnect's onclose handler bailed at
`retryCount >= retryOptions.retryCount`, leaving the SDK permanently
disconnected. Method frames already in the dispatcher queue stayed
queued forever — observed in EE microservices CI as
e2ee-passphrase-management.spec.ts:87 (and :76) where the final
loginByUserState's login frame never reached the server because the
SDK socket never reconnected.

Stack trace pointing at the offending second logout:
   Accounts.logout → standardLogout (saml.ts) → logout (UserProvider)
     → useResetE2EPasswordMutation.onSuccess

The reset E2E password mutation calls `Meteor.logout()` on success by
design (to force re-login with the new keys). In monolith CE that
went via Meteor's own connection (separate retry policy); in EE
microservices it now hits the SDK socket via stubMeteorStream and
exposes the budget exhaustion.

Verified locally with BASE_URL=http://localhost:4000 IS_EE=true:
  - e2ee-passphrase-management.spec.ts:76 ✓
  - e2ee-passphrase-management.spec.ts:87 ✓
  - e2ee-key-reset.spec.ts ✓

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ggazzo ggazzo force-pushed the fix/ddp-dispatcher-non-method-frames branch from 6084971 to 881312e Compare May 4, 2026 18:00
@ggazzo ggazzo force-pushed the worktree-sdk-over-ddp-client branch 9 times, most recently from c8b8f18 to a2f7db0 Compare May 5, 2026 13:54
Adds the `Use_RC_SDK` admin setting (General → Use Rocket.Chat SDK) and
matching client-side opt-in helper. The migration ships dormant — legacy
Meteor DDP transport remains the default. Three sources resolve the flag,
URL > localStorage > meta tag:

  - `?sdk_transport=on|off` URL parameter (per-tab)
  - `rc-config-sdk_transport` localStorage entry (per-user)
  - `<meta name="rc-sdk-transport-enabled">` injected from the
    `Use_RC_SDK` setting (workspace-wide opt-in / kill-switch)

The flag is read once at module init and used to dispatch the SDK code
paths back to their Meteor equivalents when off:

  - The 5 SDK-transport overrides (`ddpOverREST`, `ddpSdkCollectionBridge`,
    `subscribeViaSDK`, `stubMeteorStream`, `killMeteorStream`) wrap their
    bodies in `if (isSdkTransportEnabled())` — `Meteor.connection._send`,
    `_subscribe`, and `_stream` stay un-wrapped when off.
  - `apps/meteor/app/utils/client/lib/SDKClient.ts` keeps both
    `createNewMeteorStream` and `createNewDdpSdkStream`. With the flag off
    streams use Meteor's subscribe + the original
    `Meteor.connection._stream.on('message')` bridge; with the flag on the
    SDK stream is used. `publish` similarly dispatches between
    `Meteor.call` and `getDdpSdk().client.callAsync`.
  - `apps/meteor/client/providers/ServerProvider.tsx` only subscribes to
    DDPSDK connection events, combines DDPSDK status into `getStatus`,
    and calls `ensureConnectedAndAuthenticated` from `reconnect()` /
    `getDdpSdk().connection.close()` from `disconnect()` when the flag is
    on. With the flag off, `getStatus` returns `Meteor.status()` directly.
  - The boot-time side-effect block in `ddpSdk.ts` (loginWithToken wrap,
    userIdStore subscriber, reset-on-reconnect listener) is gated as well
    — keeping the SDK socket "anonymous" when off so it doesn't create
    duplicate Presence sessions.

E2e default stays off, matching CI's dormant baseline; the SDK-transport
path is reachable via URL/localStorage for manual verification or a
dedicated future job.
@ggazzo ggazzo force-pushed the worktree-sdk-over-ddp-client branch from a2f7db0 to 6e52a48 Compare May 5, 2026 15:18
@ggazzo ggazzo marked this pull request as ready for review May 5, 2026 18:42
@ggazzo ggazzo requested review from a team as code owners May 5, 2026 18:42
@ggazzo ggazzo changed the title feat(sdk): replace Meteor DDP transport with @rocket.chat/ddp-client feat(@rocket.chat/ddp-client): replace Meteor DDP transport May 5, 2026
@coderabbitai coderabbitai Bot added the type: feature Pull requests that introduces new feature label May 5, 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: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
ee/apps/ddp-streamer/src/DDPStreamer.ts (1)

53-79: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Apply the terminate fallback to the sessionId force-logout path too.

Line [58] uses ws.close() without the 5s guard, while the uid path (Line [74]-Line [79]) has a hard-stop fallback. This leaves one forced-logout path weaker and potentially hanging.

Suggested patch
 this.onEvent('user.forceLogout', (uid: string, sessionId?: string) => {
+	const closeWithGuard = (ws: WebSocket): void => {
+		ws.close();
+		const guard = setTimeout(() => {
+			if (ws.readyState !== WebSocket.CLOSED) {
+				ws.terminate();
+			}
+		}, 5000);
+		ws.once('close', () => clearTimeout(guard));
+	};
+
 	this.wss?.clients.forEach((ws) => {
 		const client = clientMap.get(ws);
 		if (sessionId) {
 			if (client?.connection.id === sessionId) {
-				ws.close();
+				closeWithGuard(ws);
 			}
 			return;
 		}
 		if (client?.userId === uid) {
-			ws.close();
-			const guard = setTimeout(() => {
-				if (ws.readyState !== ws.CLOSED) {
-					ws.terminate();
-				}
-			}, 5000);
-			ws.once('close', () => clearTimeout(guard));
+			closeWithGuard(ws);
 		}
 	});
 });
🤖 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 53 - 79, The sessionId
branch inside the onEvent('user.forceLogout') handler currently calls ws.close()
but lacks the 5s terminate fallback used in the uid branch; update the sessionId
path (the block that checks if client?.connection.id === sessionId) to perform
ws.close(), create the same guard timer that calls ws.terminate() after 5000ms
if ws.readyState !== ws.CLOSED, and clear that timer on ws.once('close', ...) so
the session-specific forced-logout uses the same
graceful-close-with-terminate-fallback logic as the uid path.
apps/meteor/app/utils/client/lib/SDKClient.ts (1)

26-47: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard null before dereferencing the DDP payload.

typeof null === 'object', so these checks still let null reach msg.msg / msg.fields.eventName. A malformed frame would throw inside the stream bridge instead of being ignored.

Suggested fix
 const isChangedCollectionPayload = (
 	msg: any,
 ): msg is { msg: 'changed'; collection: string; fields: { eventName: string; args: unknown[] } } => {
-	if (typeof msg !== 'object' && (msg !== null || msg !== undefined)) {
+	if (msg == null || typeof msg !== 'object') {
 		return false;
 	}
 	if (msg.msg !== 'changed') {
 		return false;
 	}
 	if (typeof msg.collection !== 'string') {
 		return false;
 	}
-	if (typeof msg.fields !== 'object' && (msg.fields !== null || msg.fields !== undefined)) {
+	if (msg.fields == null || typeof msg.fields !== 'object') {
 		return false;
 	}
 	if (typeof msg.fields.eventName !== 'string') {
 		return false;
 	}
🤖 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 26 - 47, The
type-guard is letting null through because typeof null === 'object'; update
isChangedCollectionPayload so it first explicitly rejects null/undefined before
dereferencing: in the function (identify by name isChangedCollectionPayload and
the parameter msg) replace the incorrect checks with explicit null/undefined
guards such as if (msg === null || msg === undefined || typeof msg !== 'object')
return false; and similarly for fields use if (msg.fields === null || msg.fields
=== undefined || typeof msg.fields !== 'object') return false; ensuring those
checks occur before accessing msg.msg or msg.fields.eventName.
🧹 Nitpick comments (1)
ee/apps/ddp-streamer/src/DDPStreamer.ts (1)

63-72: ⚡ Quick win

Remove the large inline implementation comment.

Please avoid embedding this rationale block directly in runtime code; keep intent in naming or external docs instead.

As per coding guidelines: "Avoid code comments in the implementation".

🤖 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, Remove the
large multi-line rationale block embedded in DDPStreamer.ts and replace it with
a concise one-line comment or a self-documenting identifier; specifically, in
the method handling socket shutdown (the code touching the ws object and the
graceful close + terminate fallback logic), delete the long inline explanation
and either rename the function/variable to convey intent (e.g.,
gracefulCloseWithFallback) or add a short comment like "Graceful close with
terminate fallback for unresponsive sockets." Move the full rationale and
behavior details to external docs (README or design doc) and reference that doc
in a brief in-code note if needed.
🤖 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 code currently calls
ensureConnectedAndAuthenticated().catch(() => undefined).then(...), so the .then
branch runs even after a reject and recreates the unwanted anonymous
subscription; fix by only executing the subscription logic when
ensureConnectedAndAuthenticated resolves successfully — move the .catch to the
end (call ensureConnectedAndAuthenticated().then(() => { /* subscribe:
getDdpSdk(), sdk.client.subscribe(...), subscription.ready(),
sdk.client.onCollection(...) */ }).catch(() => undefined)) or explicitly gate
the .then body on a successful result; update references to getDdpSdk(),
subscription, offCollection, meta.ready and the ready/error event emissions
accordingly so they run only on successful authentication/connection.

In `@apps/meteor/client/lib/sdk/ddpSdk.ts`:
- Around line 118-149: The auth-wrapper around sdk.account.loginWithToken is
swallowing authentication errors (returning undefined) so inflightLogin never
rejects and ensureConnectedAndAuthenticated's cleanup is skipped; update that
wrapper (the function wrapping sdk.account.loginWithToken) to let auth errors
propagate by rethrowing the error after any local cleanup (e.g. after calling
Accounts._unstoreLoginToken() and setting Meteor.connection.setUserId(null))
instead of returning undefined, so inflightLogin rejects and
ensureConnectedAndAuthenticated can run its stale-token recovery.
- Around line 30-47: startConnect currently swallows connect() failures and
waitForConnected only waits for the 'connected' event, which can deadlock
ensureConnectedAndAuthenticated; update startConnect (the function calling
sdk.connection.connect()) to re-throw the caught error (do not swallow in the
catch) so the promise rejects on failure, and change the call site in
ensureConnectedAndAuthenticated from using void startConnect(sdk) to await
startConnect(sdk) so connection errors surface immediately instead of waiting
forever in waitForConnected.

In `@apps/meteor/client/lib/sdk/meteorBackedSdk.ts`:
- Around line 169-173: The close() implementation currently calls
Meteor.disconnect() while connect() is a no-op, which breaks shared Meteor for
callers that expect to reconnect; either make both proxy methods no-ops or
implement connect() to restore the Meteor connection and restart the status
bridge. Update the proxy's connect() and close() (referencing connect, close,
stopBridge, and Meteor.disconnect) so they are symmetric: either remove
Meteor.disconnect() and stopBridge?() from close() so both methods are inert, or
have connect() call Meteor.reconnect() (or the correct Meteor connection
restore) and reinitialize the status bridge (reset stopBridge) so a subsequent
connect() after close() returns the client to an operational state.

In `@apps/meteor/client/lib/sdk/sdkTransportEnabled.ts`:
- Around line 19-24: The current logic only short-circuits on URL 'on' and
treats a stored localStorage='off' as if absent, so change the precedence to:
first read URL param (if 'on' return true; if 'off' return false), then read
localStorage for KEY and return true if 'on' or false if 'off' (i.e. honor
explicit 'off' before falling back), and only after those checks consult the
meta tag (META_NAME). Update the code that uses fromUrl,
window.localStorage.getItem(`rc-config-${KEY}`), and
meta?.getAttribute('content') to implement that exact URL > localStorage > meta
precedence.

In `@apps/meteor/client/meteor/overrides/ddpOverREST.ts`:
- Around line 73-80: The endpoint selection currently uses getUserId() and
wasResumeLogin to choose between 'method.call' and 'method.callAnon'; change it
so that any non-resume login goes through 'method.callAnon' regardless of
getUserId(): update the logic that sets endpoint (the variable named endpoint)
to check if methodName === 'login' && !wasResumeLogin and force
'method.callAnon', otherwise keep the existing branch between 'method.call' and
'method.callAnon' based on getUserId() and wasResumeLogin.

In `@apps/meteor/client/meteor/overrides/stubMeteorStream.ts`:
- Around line 89-90: The Tracker.Dependency type and usage are inverted: extend
the inline type for TrackerDependency to include depend(): void (not just
changed()), update status() to call statusListeners.depend() so callers register
a reactive dependency, and ensure wherever you signal updates (the code that
mutates status) calls statusListeners.changed() to invalidate dependents; apply
the same fix to the other occurrence around the 124–130 block (use the same
TrackerDependency, call depend() in readers like status(), and call changed()
when the status value is updated).

In `@apps/meteor/client/meteor/overrides/subscribeViaSDK.ts`:
- Around line 17-21: The rejection path currently only calls onError but must
also propagate SDK subscription failures via onStop(err) to match Meteor
semantics; update the subscribeViaSDK promise rejection handler (the code using
SubscribeCallbacks and its onError/onStop callbacks) to call onStop(err) with
the error when a subscription fails (in addition to or instead of onError as
appropriate), ensuring the error is passed through to callers expecting
server-initiated failure via onStop.

In `@apps/meteor/client/startup/startup.ts`:
- Around line 80-98: syncOnce currently sets lastSyncedUid before
runUserDataSync completes which causes in-flight attempts to be deduped and lost
on failure; change syncOnce to track in-flight vs completed separately (e.g.
introduce an inFlightUid or a Set inFlightUids) and only set lastSyncedUid after
runUserDataSync resolves successfully, or alternatively record a pendingRetry
flag and in the runUserDataSync finally block check if userIdStore.getState()
=== uid and replay the sync if needed; update references in syncOnce,
lastSyncedUid, runUserDataSync error/finally handling and any callers
(onLoggedIn, userIdStore.subscribe, boot-time call) so retries are not
swallowed.

In `@apps/meteor/tests/e2e/page-objects/fragments/home-content.ts`:
- Around line 139-148: The current wait uses messageListItems.count() and
lastUserMessage which can track the wrong tail item if other messages arrive;
change the final wait to target the specific message you sent by capturing the
message text (or the exact item index created: messageListItems.nth(before)) and
poll/assert that that specific element has left the pending state and contains
the sent text. Concretely, after btnSend.click() store the sent text (or compute
the newItem = this.messageListItems.nth(before)), then replace the count-based
poll and lastUserMessage assertion with a poll/expect against newItem (or the
element located by the sent text) toBeVisible/
not.toHaveClass(/rcx-message--pending/) and toContainText(sentText).

---

Outside diff comments:
In `@apps/meteor/app/utils/client/lib/SDKClient.ts`:
- Around line 26-47: The type-guard is letting null through because typeof null
=== 'object'; update isChangedCollectionPayload so it first explicitly rejects
null/undefined before dereferencing: in the function (identify by name
isChangedCollectionPayload and the parameter msg) replace the incorrect checks
with explicit null/undefined guards such as if (msg === null || msg ===
undefined || typeof msg !== 'object') return false; and similarly for fields use
if (msg.fields === null || msg.fields === undefined || typeof msg.fields !==
'object') return false; ensuring those checks occur before accessing msg.msg or
msg.fields.eventName.

In `@ee/apps/ddp-streamer/src/DDPStreamer.ts`:
- Around line 53-79: The sessionId branch inside the onEvent('user.forceLogout')
handler currently calls ws.close() but lacks the 5s terminate fallback used in
the uid branch; update the sessionId path (the block that checks if
client?.connection.id === sessionId) to perform ws.close(), create the same
guard timer that calls ws.terminate() after 5000ms if ws.readyState !==
ws.CLOSED, and clear that timer on ws.once('close', ...) so the session-specific
forced-logout uses the same graceful-close-with-terminate-fallback logic as the
uid path.

---

Nitpick comments:
In `@ee/apps/ddp-streamer/src/DDPStreamer.ts`:
- Around line 63-72: Remove the large multi-line rationale block embedded in
DDPStreamer.ts and replace it with a concise one-line comment or a
self-documenting identifier; specifically, in the method handling socket
shutdown (the code touching the ws object and the graceful close + terminate
fallback logic), delete the long inline explanation and either rename the
function/variable to convey intent (e.g., gracefulCloseWithFallback) or add a
short comment like "Graceful close with terminate fallback for unresponsive
sockets." Move the full rationale and behavior details to external docs (README
or design doc) and reference that doc in a brief in-code note if needed.
🪄 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: 3177b8d5-b387-4f39-9f40-7f05a41971cb

📥 Commits

Reviewing files that changed from the base of the PR and between 881312e and 6e52a48.

⛔ 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
🧰 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/views/root/hooks/loggedIn/useForceLogout.ts
  • apps/meteor/client/meteor/overrides/ddpSdkCollectionBridge.ts
  • apps/meteor/client/meteor/overrides/subscribeViaSDK.ts
  • apps/meteor/app/ui-master/server/index.ts
  • apps/meteor/client/lib/sdk/streamerAdapter.ts
  • apps/meteor/client/lib/sdk/sdkTransportEnabled.ts
  • apps/meteor/app/notifications/client/lib/Presence.ts
  • apps/meteor/server/settings/general.ts
  • apps/meteor/client/meteor/overrides/index.ts
  • apps/meteor/client/meteor/overrides/stubMeteorStream.ts
  • apps/meteor/client/lib/presence.ts
  • apps/meteor/client/providers/ServerProvider.tsx
  • ee/apps/ddp-streamer/src/DDPStreamer.ts
  • apps/meteor/client/lib/sdk/meteorBackedSdk.ts
  • apps/meteor/client/startup/startup.ts
  • apps/meteor/client/lib/cachedStores/CachedStore.ts
  • apps/meteor/client/lib/sdk/ddpSdk.ts
  • apps/meteor/client/meteor/overrides/ddpOverREST.ts
  • apps/meteor/client/meteor/overrides/killMeteorStream.ts
  • apps/meteor/tests/e2e/page-objects/fragments/home-content.ts
  • apps/meteor/app/utils/client/lib/SDKClient.ts
  • apps/meteor/client/lib/loggedIn.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 (7)
📚 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/views/root/hooks/loggedIn/useForceLogout.ts
  • apps/meteor/client/meteor/overrides/ddpSdkCollectionBridge.ts
  • apps/meteor/client/meteor/overrides/subscribeViaSDK.ts
  • apps/meteor/client/lib/sdk/streamerAdapter.ts
  • apps/meteor/client/lib/sdk/sdkTransportEnabled.ts
  • apps/meteor/client/meteor/overrides/index.ts
  • apps/meteor/client/meteor/overrides/stubMeteorStream.ts
  • apps/meteor/client/lib/presence.ts
  • apps/meteor/client/lib/sdk/meteorBackedSdk.ts
  • apps/meteor/client/startup/startup.ts
  • apps/meteor/client/lib/cachedStores/CachedStore.ts
  • apps/meteor/client/lib/sdk/ddpSdk.ts
  • apps/meteor/client/meteor/overrides/ddpOverREST.ts
  • apps/meteor/client/meteor/overrides/killMeteorStream.ts
  • apps/meteor/client/lib/loggedIn.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/views/root/hooks/loggedIn/useForceLogout.ts
  • apps/meteor/client/meteor/overrides/ddpSdkCollectionBridge.ts
  • apps/meteor/client/meteor/overrides/subscribeViaSDK.ts
  • apps/meteor/app/ui-master/server/index.ts
  • apps/meteor/client/lib/sdk/streamerAdapter.ts
  • apps/meteor/client/lib/sdk/sdkTransportEnabled.ts
  • apps/meteor/app/notifications/client/lib/Presence.ts
  • apps/meteor/server/settings/general.ts
  • apps/meteor/client/meteor/overrides/index.ts
  • apps/meteor/client/meteor/overrides/stubMeteorStream.ts
  • apps/meteor/client/lib/presence.ts
  • ee/apps/ddp-streamer/src/DDPStreamer.ts
  • apps/meteor/client/lib/sdk/meteorBackedSdk.ts
  • apps/meteor/client/startup/startup.ts
  • apps/meteor/client/lib/cachedStores/CachedStore.ts
  • apps/meteor/client/lib/sdk/ddpSdk.ts
  • apps/meteor/client/meteor/overrides/ddpOverREST.ts
  • apps/meteor/client/meteor/overrides/killMeteorStream.ts
  • apps/meteor/tests/e2e/page-objects/fragments/home-content.ts
  • apps/meteor/app/utils/client/lib/SDKClient.ts
  • apps/meteor/client/lib/loggedIn.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/views/root/hooks/loggedIn/useForceLogout.ts
  • apps/meteor/client/meteor/overrides/ddpSdkCollectionBridge.ts
  • apps/meteor/client/meteor/overrides/subscribeViaSDK.ts
  • apps/meteor/app/ui-master/server/index.ts
  • apps/meteor/client/lib/sdk/streamerAdapter.ts
  • apps/meteor/client/lib/sdk/sdkTransportEnabled.ts
  • apps/meteor/app/notifications/client/lib/Presence.ts
  • apps/meteor/server/settings/general.ts
  • apps/meteor/client/meteor/overrides/index.ts
  • apps/meteor/client/meteor/overrides/stubMeteorStream.ts
  • apps/meteor/client/lib/presence.ts
  • ee/apps/ddp-streamer/src/DDPStreamer.ts
  • apps/meteor/client/lib/sdk/meteorBackedSdk.ts
  • apps/meteor/client/startup/startup.ts
  • apps/meteor/client/lib/cachedStores/CachedStore.ts
  • apps/meteor/client/lib/sdk/ddpSdk.ts
  • apps/meteor/client/meteor/overrides/ddpOverREST.ts
  • apps/meteor/client/meteor/overrides/killMeteorStream.ts
  • apps/meteor/tests/e2e/page-objects/fragments/home-content.ts
  • apps/meteor/app/utils/client/lib/SDKClient.ts
  • apps/meteor/client/lib/loggedIn.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: 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
📚 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
🔇 Additional comments (3)
apps/meteor/package.json (1)

105-105: Dependency addition looks correct for the flagged transport rollout.

The workspace dependency placement under dependencies matches the runtime use case described in this PR.

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

5619-5621: Looks good — clear and consistent localization keys for the new setting.

The label, description, and alert copy are coherent and match the existing i18n key style.

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

107-110: Please verify connection.on('connection') is a real DDPSDK event.

The rest of this PR subscribes to connected / disconnected. If connection is not emitted here, ddpSdkStatusDep.changed() never runs and useReactiveValue(getStatus) stays stale after transport transitions.

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 | 🟠 Major | ⚡ Quick win

Don't create the SDK subscription after auth/connect failure.

This chain still runs the .then(...) branch after ensureConnectedAndAuthenticated() rejects, which recreates the anonymous nosub path this helper is trying to avoid.

Suggested fix
-	void ensureConnectedAndAuthenticated()
-		.catch(() => undefined)
-		.then(() => {
+	void ensureConnectedAndAuthenticated()
+		.then(() => {
 			if (stopped) return;
 			const sdk = getDdpSdk();
 			subscription = sdk.client.subscribe(`stream-${streamName}`, key, { useCollection: false, args });
 …
 			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);
 			});
-		});
+		})
+		.catch((err) => {
+			if (stopped) return;
+			ee.emit('ready', [err]);
+			ee.emit('error', 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 178 - 204, The
code currently calls ensureConnectedAndAuthenticated().catch(() =>
undefined).then(...), so the .then branch runs even after a reject and recreates
the unwanted anonymous subscription; fix by only executing the subscription
logic when ensureConnectedAndAuthenticated resolves successfully — move the
.catch to the end (call ensureConnectedAndAuthenticated().then(() => { /*
subscribe: getDdpSdk(), sdk.client.subscribe(...), subscription.ready(),
sdk.client.onCollection(...) */ }).catch(() => undefined)) or explicitly gate
the .then body on a successful result; update references to getDdpSdk(),
subscription, offCollection, meta.ready and the ready/error event emissions
accordingly so they run only on successful authentication/connection.

Comment on lines +30 to +47
const startConnect = (sdk: DDPSDK): Promise<unknown> => {
if (connectPromise) return connectPromise;
connectPromise = sdk.connection.connect().catch((err) => {
console.warn('[ddpSdk] connect failed', err);
// Allow a retry on the next call.
connectPromise = undefined;
});
return connectPromise;
};

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 | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

wc -l apps/meteor/client/lib/sdk/ddpSdk.ts

Repository: RocketChat/Rocket.Chat

Length of output: 107


🏁 Script executed:

cat -n apps/meteor/client/lib/sdk/ddpSdk.ts

Repository: RocketChat/Rocket.Chat

Length of output: 13826


A failed connect() can deadlock every auth caller.

startConnect() swallows the rejection in its catch handler without re-throwing, and waitForConnected() only resolves when the 'connected' event fires. If sdk.connection.connect() fails, the promise settles successfully but the connection never emits 'connected', leaving ensureConnectedAndAuthenticated() hanging indefinitely at line 90. Any caller awaiting this function (stream bootstrap, auth flows) stalls until timeout.

The fix requires both: re-throw the error in startConnect() and change void startConnect(sdk) to await startConnect(sdk) at line 88 so the rejection surfaces and prevents waiting on an event that will never fire.

Suggested fix
 const startConnect = (sdk: DDPSDK): Promise<unknown> => {
 	if (connectPromise) return connectPromise;
 	connectPromise = sdk.connection.connect().catch((err) => {
 		console.warn('[ddpSdk] connect failed', err);
 		// Allow a retry on the next call.
 		connectPromise = undefined;
+		throw err;
 	});
 	return connectPromise;
 };
…
 	if (
 		sdk.connection.status === 'idle' ||
 		sdk.connection.status === 'closed' ||
 		sdk.connection.status === 'disconnected' ||
 		sdk.connection.status === 'failed'
 	) {
-		void startConnect(sdk);
+		await startConnect(sdk);
 	}
 	await waitForConnected(sdk);
🤖 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 30 - 47, startConnect
currently swallows connect() failures and waitForConnected only waits for the
'connected' event, which can deadlock ensureConnectedAndAuthenticated; update
startConnect (the function calling sdk.connection.connect()) to re-throw the
caught error (do not swallow in the catch) so the promise rejects on failure,
and change the call site in ensureConnectedAndAuthenticated from using void
startConnect(sdk) to await startConnect(sdk) so connection errors surface
immediately instead of waiting forever in waitForConnected.

Comment on lines +118 to +149
inflightLogin = (async () => {
try {
await sdk.account.loginWithToken(token);
} finally {
inflightLogin = undefined;
}
})();

try {
await inflightLogin;
} catch (error) {
if (isAuthError(error) && readStoredLoginToken() === token) {
// Server rejected the stored token. Without this branch the stored
// token stays in localStorage forever and the router keeps the user
// wedged on /home with no main UI and no login form: ddpOverREST
// routes Meteor's resume login through DDPSDK / REST (not Meteor's
// own WS), and on rejection the resume invoker errors but the
// account state isn't cleared automatically. The token-stable
// guard (readStoredLoginToken() === token) avoids kicking the user
// out when localStorage was updated mid-flight by a parallel flow
// (fresh registration, Meteor's own resume) — the 401 is then on a
// stale token a newer credential already replaced. Drop the local
// credentials manually instead of calling Meteor.logout(): the
// latter dispatches a `logout` method which itself races against
// parallel re-auth flows in CI's parallel-shard environment and
// kicked otherwise-healthy tests out.
Accounts._unstoreLoginToken();
(Meteor.connection as unknown as { setUserId: (uid: string | null) => void }).setUserId(null);
return;
}
console.warn('[ddpSdk] loginWithToken failed', error);
}

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

🧩 Analysis chain

🏁 Script executed:

wc -l apps/meteor/client/lib/sdk/ddpSdk.ts

Repository: RocketChat/Rocket.Chat

Length of output: 107


🏁 Script executed:

cat -n apps/meteor/client/lib/sdk/ddpSdk.ts | sed -n '100,270p'

Repository: RocketChat/Rocket.Chat

Length of output: 8945


The wrapper swallows auth failures, preventing cleanup in ensureConnectedAndAuthenticated().

The wrapper at lines 250–264 intercepts all sdk.account.loginWithToken() calls globally. When an auth error occurs (line 256), it returns undefined (line 260) instead of throwing. This causes the await inflightLogin at line 127 to resolve successfully, bypassing the catch block that cleans up stale credentials at lines 144–146. The stale-token recovery logic is thus skipped for auth failures.

🤖 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 118 - 149, The
auth-wrapper around sdk.account.loginWithToken is swallowing authentication
errors (returning undefined) so inflightLogin never rejects and
ensureConnectedAndAuthenticated's cleanup is skipped; update that wrapper (the
function wrapping sdk.account.loginWithToken) to let auth errors propagate by
rethrowing the error after any local cleanup (e.g. after calling
Accounts._unstoreLoginToken() and setting Meteor.connection.setUserId(null))
instead of returning undefined, so inflightLogin rejects and
ensureConnectedAndAuthenticated can run its stale-token recovery.

Comment on lines +169 to +173
connect: () => Promise.resolve(),
close: () => {
if (typeof Meteor.disconnect === 'function') Meteor.disconnect();
stopBridge?.();
},

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

Don't disconnect Meteor here unless the proxy can reconnect it.

close() tears down the shared Meteor connection, but connect() is a no-op. Any caller that closes and later reconnects this fallback DDPSDK will leave the whole client offline when Use_RC_SDK is disabled. Either keep both lifecycle methods as no-ops in the proxy, or have connect() restore the Meteor connection and reset the status bridge before reuse.

🤖 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 169 - 173, The
close() implementation currently calls Meteor.disconnect() while connect() is a
no-op, which breaks shared Meteor for callers that expect to reconnect; either
make both proxy methods no-ops or implement connect() to restore the Meteor
connection and restart the status bridge. Update the proxy's connect() and
close() (referencing connect, close, stopBridge, and Meteor.disconnect) so they
are symmetric: either remove Meteor.disconnect() and stopBridge?() from close()
so both methods are inert, or have connect() call Meteor.reconnect() (or the
correct Meteor connection restore) and reinitialize the status bridge (reset
stopBridge) so a subsequent connect() after close() returns the client to an
operational state.

Comment on lines +19 to +24
const fromUrl = new URLSearchParams(window.location.search).get(KEY);
if (fromUrl === 'on') return true;
if (fromUrl === 'off') return false;
if (window.localStorage.getItem(`rc-config-${KEY}`) === 'on') return true;
const meta = window.document?.querySelector(`meta[name="${META_NAME}"]`);
return meta?.getAttribute('content') === 'on';

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

Honor persisted localStorage=off before falling back to the meta tag.

Right now only 'on' short-circuits. If the workspace-level meta enables the SDK globally, a user who explicitly stored 'off' still gets forced onto the SDK path, which breaks the documented URL > localStorage > meta precedence.

Suggested fix
 		const fromUrl = new URLSearchParams(window.location.search).get(KEY);
 		if (fromUrl === 'on') return true;
 		if (fromUrl === 'off') return false;
-		if (window.localStorage.getItem(`rc-config-${KEY}`) === 'on') return true;
+		const fromLocalStorage = window.localStorage.getItem(`rc-config-${KEY}`);
+		if (fromLocalStorage === 'on') return true;
+		if (fromLocalStorage === 'off') return false;
 		const meta = window.document?.querySelector(`meta[name="${META_NAME}"]`);
 
 		return meta?.getAttribute('content') === 'on';
📝 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 fromUrl = new URLSearchParams(window.location.search).get(KEY);
if (fromUrl === 'on') return true;
if (fromUrl === 'off') return false;
if (window.localStorage.getItem(`rc-config-${KEY}`) === 'on') return true;
const meta = window.document?.querySelector(`meta[name="${META_NAME}"]`);
return meta?.getAttribute('content') === 'on';
const fromUrl = new URLSearchParams(window.location.search).get(KEY);
if (fromUrl === 'on') return true;
if (fromUrl === 'off') return false;
const fromLocalStorage = window.localStorage.getItem(`rc-config-${KEY}`);
if (fromLocalStorage === 'on') return true;
if (fromLocalStorage === 'off') return false;
const meta = window.document?.querySelector(`meta[name="${META_NAME}"]`);
return meta?.getAttribute('content') === 'on';
🤖 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/sdkTransportEnabled.ts` around lines 19 - 24, The
current logic only short-circuits on URL 'on' and treats a stored
localStorage='off' as if absent, so change the precedence to: first read URL
param (if 'on' return true; if 'off' return false), then read localStorage for
KEY and return true if 'on' or false if 'off' (i.e. honor explicit 'off' before
falling back), and only after those checks consult the meta tag (META_NAME).
Update the code that uses fromUrl,
window.localStorage.getItem(`rc-config-${KEY}`), and
meta?.getAttribute('content') to implement that exact URL > localStorage > meta
precedence.

Comment on lines +73 to 80
// Login itself is the call that establishes auth — running it through
// `method.call` would force the REST middleware to validate the very
// token we're trying to use, and the server would 401 with "You must
// be logged in" before even invoking the login method. The 401 then
// short-circuits the resume callback, leaving the stale token in
// localStorage and the user wedged on /home with no main UI.
const endpoint = !getUserId() || wasResumeLogin ? 'method.callAnon' : 'method.call';

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

Route every non-resume login through method.callAnon.

Line 79 still sends login to method.call whenever getUserId() is truthy. That reintroduces the stale-session/account-switch failure described above: REST auth validates the old token before the login handler runs, so password/SAML/OAuth login can 401 again instead of clearing the session. login should be anonymous regardless of the current client state.

Suggested fix
-		const endpoint = !getUserId() || wasResumeLogin ? 'method.callAnon' : 'method.call';
+		const endpoint = message.method === 'login' || !getUserId() || wasResumeLogin ? 'method.callAnon' : 'method.call';
🤖 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 73 - 80, The
endpoint selection currently uses getUserId() and wasResumeLogin to choose
between 'method.call' and 'method.callAnon'; change it so that any non-resume
login goes through 'method.callAnon' regardless of getUserId(): update the logic
that sets endpoint (the variable named endpoint) to check if methodName ===
'login' && !wasResumeLogin and force 'method.callAnon', otherwise keep the
existing branch between 'method.call' and 'method.callAnon' based on getUserId()
and wasResumeLogin.

Comment on lines +89 to +90
const TrackerDependency = (Tracker as unknown as { Dependency?: new () => { changed(): void } }).Dependency;
const statusListeners = TrackerDependency ? new TrackerDependency() : 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

🧩 Analysis chain

🏁 Script executed:

cat -n apps/meteor/client/meteor/overrides/stubMeteorStream.ts | head -140

Repository: RocketChat/Rocket.Chat

Length of output: 5808


🏁 Script executed:

rg "Tracker.Dependency|depend\(\)|\.changed\(" apps/meteor/client/meteor/overrides/stubMeteorStream.ts -B 2 -A 2

Repository: RocketChat/Rocket.Chat

Length of output: 48


🌐 Web query:

Meteor Tracker.Dependency API documentation depend changed

💡 Result:

Meteor Tracker.Dependency API documentation: the relevant API surface is Tracker.Dependency#depend([fromComputation]) and Tracker.Dependency#changed, and the docs consistently describe them the same way (no evidence found of a documentation “depend changed” breaking change in the API itself). 1) Tracker.Dependency#depend([fromComputation]) - Declares that the current reactive computation (or the computation passed via fromComputation) depends on the Dependency. The computation will be invalidated the next time the dependency changes. [1][2] - If there is no current computation and depend is called with no arguments, it does nothing and returns false. [1] - Returns true when the computation is a new dependent rather than an existing one. [1] 2) Tracker.Dependency#changed - Invalidate all dependent computations immediately and remove them as dependents. [1][2] 3) Docs example (getter calls depend; setter calls changed) - The official Tracker docs show a pattern where a getter calls weatherDep.depend and the setter calls weatherDep.changed to trigger reactivity. [2][1] So if your question is “what changed in the docs for depend?”, the current official documentation for Tracker.Dependency continues to define depend as “declare dependency” and changed as “invalidate dependents”, with the same getter/setter example pattern. [2][1] If instead you meant “Deps.Dependency” (older name) vs “Tracker.Dependency” (newer name), tell me your Meteor version and what exact page/wording you saw that looked different, and I can trace that specific doc change.

Citations:


Fix Tracker.Dependency usage—status() should call depend(), not changed().

The type definition at line 89 is incomplete (missing depend(): void), and the implementation is backwards. The status() method should register reactivity with depend() so that computations reading the status are invalidated when it changes. Currently, every call to status() invalidates all dependents immediately while reactive callers never register a dependency.

Suggested fix
-	const TrackerDependency = (Tracker as unknown as { Dependency?: new () => { changed(): void } }).Dependency;
+	const TrackerDependency = (Tracker as unknown as { Dependency?: new () => { depend(): void; changed(): void } }).Dependency;
 	const statusListeners = TrackerDependency ? new TrackerDependency() : undefined;
…
 		status() {
-			statusListeners?.changed?.();
+			statusListeners?.depend?.();
 			return this.currentStatus;
 		},

 		statusChanged() {
 			statusListeners?.changed?.();

Also applies to: 124–130

🤖 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/stubMeteorStream.ts` around lines 89 -
90, The Tracker.Dependency type and usage are inverted: extend the inline type
for TrackerDependency to include depend(): void (not just changed()), update
status() to call statusListeners.depend() so callers register a reactive
dependency, and ensure wherever you signal updates (the code that mutates
status) calls statusListeners.changed() to invalidate dependents; apply the same
fix to the other occurrence around the 124–130 block (use the same
TrackerDependency, call depend() in readers like status(), and call changed()
when the status value is updated).

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
from pathlib import Path
import re

pattern = re.compile(
    r'Meteor(?:\.connection)?\.subscribe\s*\((?:(?!\);).)*?\{(?:(?!\}).)*?\bonStop\s*:',
    re.S,
)

for path in Path('.').rglob('*'):
    if path.suffix not in {'.ts', '.tsx', '.js'}:
        continue
    try:
        text = path.read_text()
    except Exception:
        continue
    for match in pattern.finditer(text):
        line = text.count('\n', 0, match.start()) + 1
        print(f'{path}:{line}')
PY

Repository: RocketChat/Rocket.Chat

Length of output: 48


🏁 Script executed:

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

Repository: RocketChat/Rocket.Chat

Length of output: 2574


🏁 Script executed:

rg "onStop" --type ts --type tsx --type js -B 2 -A 2 | head -100

Repository: RocketChat/Rocket.Chat

Length of output: 94


🏁 Script executed:

rg "onStop" -B 2 -A 2 | head -150

Repository: RocketChat/Rocket.Chat

Length of output: 9486


Propagate failed subscriptions through onStop(err) to preserve Meteor compatibility.

The rejection path only calls onError, but Meteor reserves onStop(err) for server-initiated subscription failures. The adapter's promise rejection should invoke onStop with the error so callers relying on this path receive SDK-side failures.

Suggested fix
 		subscription
 			.ready()
 			.then(() => callbacks.onReady?.())
-			.catch((err: Error) => callbacks.onError?.(err));
+			.catch((err: Error) => {
+				callbacks.onError?.(err);
+				callbacks.onStop?.(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 rejection path currently only calls onError but must also propagate SDK
subscription failures via onStop(err) to match Meteor semantics; update the
subscribeViaSDK promise rejection handler (the code using SubscribeCallbacks and
its onError/onStop callbacks) to call onStop(err) with the error when a
subscription fails (in addition to or instead of onError as appropriate),
ensuring the error is passed through to callers expecting server-initiated
failure via onStop.

Comment on lines +80 to +98
let lastSyncedUid: string | undefined;
const syncOnce = (uid: string | undefined): void => {
// Reset on logout transitions so a subsequent re-login (same uid or different)
// runs a fresh sync. Force-logout via the SDK loginWithToken wrap clears
// creds via Accounts._unstoreLoginToken() + setUserId(null), which does NOT
// fire Accounts.onLogout — so without this branch, lastSyncedUid stays set,
// the next login is deduped, runUserDataSync is skipped, and
// useUserDataSyncReady stays false (page wedged on PageLoading).
if (!uid) {
lastSyncedUid = undefined;
return;
}
if (uid === lastSyncedUid) return;
lastSyncedUid = uid;
void runUserDataSync(uid).catch((err) => {
console.warn('[startup] runUserDataSync failed; clearing dedup to allow a retry', err);
if (lastSyncedUid === uid) lastSyncedUid = 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 | 🏗️ Heavy lift

lastSyncedUid can swallow the only retry for a login.

syncOnce() marks the uid as handled before runUserDataSync() settles, and this file can feed it from three places (onLoggedIn, userIdStore.subscribe, and the eager boot-time call). If the first attempt is still in flight, the later signals get deduped; then a failure at Lines 94-97 only clears the guard and never replays the consumed event. That still leaves SDK-mode re-logins vulnerable to getting stuck on the failed first sync.

Please track “in flight” separately from “successfully synced”, or queue a pending retry and replay it in finally when userIdStore.getState() === uid.

🤖 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 80 - 98, syncOnce
currently sets lastSyncedUid before runUserDataSync completes which causes
in-flight attempts to be deduped and lost on failure; change syncOnce to track
in-flight vs completed separately (e.g. introduce an inFlightUid or a Set
inFlightUids) and only set lastSyncedUid after runUserDataSync resolves
successfully, or alternatively record a pendingRetry flag and in the
runUserDataSync finally block check if userIdStore.getState() === uid and replay
the sync if needed; update references in syncOnce, lastSyncedUid,
runUserDataSync error/finally handling and any callers (onLoggedIn,
userIdStore.subscribe, boot-time call) so retries are not swallowed.

Comment on lines +139 to +148
const before = await this.messageListItems.count();

await this.composer.btnSend.click();

// Use `>=` rather than `==` because some flows (e.g. just-created
// encrypted channels) drop additional list items in alongside the
// user's send (other in-flight messages, decryption-status items),
// so an exact count is racy.
await expect.poll(() => this.messageListItems.count(), { timeout: 10_000 }).toBeGreaterThanOrEqual(before + 1);
await expect(this.lastUserMessage).not.toHaveClass(/rcx-message--pending/);

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

Wait on the submitted message, not just the new tail item.

messageListItems.count() plus lastUserMessage can succeed on an unrelated row when other messages land at the same time. In that case this helper returns even though the message you just sent is still pending or failed. Anchor the final assertion to the submitted message text, or to the exact row created after before, instead of the current list tail.

Possible adjustment
 		const before = await this.messageListItems.count();

 		await this.composer.btnSend.click();

 		await expect.poll(() => this.messageListItems.count(), { timeout: 10_000 }).toBeGreaterThanOrEqual(before + 1);
-		await expect(this.lastUserMessage).not.toHaveClass(/rcx-message--pending/);
+		await expect(this.getMessageByText(text).last()).not.toHaveClass(/rcx-message--pending/);
🤖 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/tests/e2e/page-objects/fragments/home-content.ts` around lines
139 - 148, The current wait uses messageListItems.count() and lastUserMessage
which can track the wrong tail item if other messages arrive; change the final
wait to target the specific message you sent by capturing the message text (or
the exact item index created: messageListItems.nth(before)) and poll/assert that
that specific element has left the pending state and contains the sent text.
Concretely, after btnSend.click() store the sent text (or compute the newItem =
this.messageListItems.nth(before)), then replace the count-based poll and
lastUserMessage assertion with a poll/expect against newItem (or the element
located by the sent text) toBeVisible/ not.toHaveClass(/rcx-message--pending/)
and toContainText(sentText).

@ggazzo ggazzo changed the title feat(@rocket.chat/ddp-client): replace Meteor DDP transport chore(@rocket.chat/ddp-client): replace Meteor DDP transport May 5, 2026
@d-gubert d-gubert changed the title chore(@rocket.chat/ddp-client): replace Meteor DDP transport chore(@rocket.chat/ddp-client): introduce Meteor independent DDP client May 6, 2026
@d-gubert d-gubert changed the title chore(@rocket.chat/ddp-client): introduce Meteor independent DDP client chore(@rocket.chat/ddp-client): introduce experimental Meteor independent DDP client May 6, 2026
cardoso
cardoso previously approved these changes May 6, 2026

@cardoso cardoso left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!!

ggazzo added 2 commits May 6, 2026 14:50
…llers

Previous fix collapsed `connecting` and `connected` into the same
`Promise.resolve(true)` short-circuit. That silenced the unhandled
rejection from the retry timer but, for the `connecting` half, replaced
one lie with another: a second caller that arrived mid-handshake
observed `true` even when the same handshake later received a `failed`
payload (handshake's owning promise rejected, but the second caller
saw success).

Track the in-flight handshake on `this.connectPromise` and return it
from both `connect()` and `reconnect()` while `status === 'connecting'`.
The `connected` branch is unchanged. The promise is published before any
status emit so a synchronous re-entrant call still observes a valid
promise to share.

Tests: a concurrent `connect()` call gets the same instance and surfaces
the eventual `failed`; same for a concurrent `reconnect()` reaching
success.
Base automatically changed from fix/ddp-dispatcher-non-method-frames to develop May 6, 2026 20:17
@ggazzo ggazzo dismissed cardoso’s stale review May 6, 2026 20:17

The base branch was changed.

@ggazzo ggazzo merged commit fa4b2b4 into develop May 6, 2026
80 of 83 checks passed
@ggazzo ggazzo deleted the worktree-sdk-over-ddp-client branch May 6, 2026 20:19
ggazzo added a commit that referenced this pull request May 6, 2026
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.

4 participants