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

Merged
GG
6mo ago
+1982-117

[!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.uid ↦ Meteor.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.ts — ws.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.ts — whenLoggedIn / 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.ts — runUserDataSync 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.