diff --git a/apps/meteor/app/utils/client/lib/SDKClient.ts b/apps/meteor/app/utils/client/lib/SDKClient.ts index 387adde9fbe75..4bf19107e2857 100644 --- a/apps/meteor/app/utils/client/lib/SDKClient.ts +++ b/apps/meteor/app/utils/client/lib/SDKClient.ts @@ -1,10 +1,10 @@ import type { RestClientInterface } from '@rocket.chat/api-client'; import type { SDK, ClientStream, StreamKeys, StreamNames, StreamerCallbackArgs, ServerMethods } from '@rocket.chat/ddp-client'; import { Emitter } from '@rocket.chat/emitter'; -import { DDPCommon } from 'meteor/ddp-common'; import { Meteor } from 'meteor/meteor'; import { APIClient } from './RestApiClient'; +import { parseDDP } from '../../../../client/lib/sdk/ddpProtocol'; import { ensureConnectedAndAuthenticated, getDdpSdk } from '../../../../client/lib/sdk/ddpSdk'; import { isSdkTransportEnabled } from '../../../../client/lib/sdk/sdkTransportEnabled'; @@ -267,7 +267,7 @@ const createStreamManager = () => { // the SDK socket and createNewDdpSdkStream registers its own onCollection // listener instead. Meteor.connection._stream!.on('message', (rawMsg: string) => { - const msg = DDPCommon.parseDDP(rawMsg); + const msg = parseDDP(rawMsg); if (!isChangedCollectionPayload(msg)) { return; } diff --git a/apps/meteor/client/lib/presence.spec.ts b/apps/meteor/client/lib/presence.spec.ts index 5e630bd4f03da..a0ff998ca4f84 100644 --- a/apps/meteor/client/lib/presence.spec.ts +++ b/apps/meteor/client/lib/presence.spec.ts @@ -6,6 +6,10 @@ jest.mock('meteor/meteor', () => ({ Meteor: { subscribe: jest.fn(), }, + DDPCommon: { + parseDDP: jest.fn((msg: string) => JSON.parse(msg)), + stringifyDDP: jest.fn((msg: unknown) => JSON.stringify(msg)), + }, })); const mockGet = jest.fn(); diff --git a/apps/meteor/client/lib/sdk/ddpProtocol.ts b/apps/meteor/client/lib/sdk/ddpProtocol.ts new file mode 100644 index 0000000000000..4a9011e619cf8 --- /dev/null +++ b/apps/meteor/client/lib/sdk/ddpProtocol.ts @@ -0,0 +1,10 @@ +// Single point of access to the DDP wire codec. Today it forwards to Meteor's +// `ddp-common` package; the eventual replacement will be a standalone EJSON +// helper. Consumers MUST import parseDDP / stringifyDDP from here so the codec +// stays swappable. + +import { DDPCommon } from 'meteor/ddp-common'; + +export const { parseDDP, stringifyDDP } = DDPCommon; + +export type DDPMessage = Parameters[0]; diff --git a/apps/meteor/client/lib/sdk/meteorBackedSdk.ts b/apps/meteor/client/lib/sdk/meteorBackedSdk.ts index d50e3a70da4aa..f9ec3e825eb17 100644 --- a/apps/meteor/client/lib/sdk/meteorBackedSdk.ts +++ b/apps/meteor/client/lib/sdk/meteorBackedSdk.ts @@ -1,9 +1,10 @@ import type { DDPSDK } from '@rocket.chat/ddp-client'; import { Emitter } from '@rocket.chat/emitter'; import { Accounts } from 'meteor/accounts-base'; -import { DDPCommon } from 'meteor/ddp-common'; import { Meteor } from 'meteor/meteor'; +import { parseDDP } from './ddpProtocol'; + /** * Meteor-backed pass-through DDPSDK used when the SDK transport is OFF. * @@ -100,7 +101,7 @@ const createMeteorBackedClient = () => { const handler = (rawMsg: string): void => { let msg: unknown; try { - msg = DDPCommon.parseDDP(rawMsg); + msg = parseDDP(rawMsg); } catch { return; } diff --git a/apps/meteor/client/meteor/overrides/ddpOverREST.ts b/apps/meteor/client/meteor/overrides/ddpOverREST.ts index 210feb5d67eda..31e7fd660dcb2 100644 --- a/apps/meteor/client/meteor/overrides/ddpOverREST.ts +++ b/apps/meteor/client/meteor/overrides/ddpOverREST.ts @@ -1,7 +1,7 @@ -import { DDPCommon } from 'meteor/ddp-common'; import { Meteor } from 'meteor/meteor'; import { sdk } from '../../../app/utils/client/lib/SDKClient'; +import { parseDDP, stringifyDDP } from '../../lib/sdk/ddpProtocol'; import { getUserId } from '../../lib/user'; const bypassMethods: string[] = ['setUserStatus', 'logout']; @@ -79,7 +79,7 @@ const withDDPOverREST = (_send: (this: Meteor.IMeteorConnection, message: Meteor const endpoint = !getUserId() || wasResumeLogin ? 'method.callAnon' : 'method.call'; const restParams = { - message: DDPCommon.stringifyDDP({ ...message }), + message: stringifyDDP({ ...message }), }; const method = encodeURIComponent(message.method.replace(/\//g, ':')); @@ -92,7 +92,7 @@ const withDDPOverREST = (_send: (this: Meteor.IMeteorConnection, message: Meteor // processed, but the Accounts.onLogin callbacks fire before that follow-up request — so any requests // initiated inside onLogin callbacks queue behind the loginWithToken and only run after it completes. if (!wasResumeLogin && message.method === 'login') { - const parsedMessage = DDPCommon.parseDDP(_message) as { result?: { token?: string } }; + const parsedMessage = parseDDP(_message) as { result?: { token?: string } }; if (parsedMessage.result?.token) { Meteor.loginWithToken(parsedMessage.result.token); } @@ -110,7 +110,7 @@ const withDDPOverREST = (_send: (this: Meteor.IMeteorConnection, message: Meteor // open the 2FA modal. if (typeof e.message === 'string') { try { - const parsed = DDPCommon.parseDDP(e.message) as { msg?: string; id?: string }; + const parsed = parseDDP(e.message) as { msg?: string; id?: string }; if (parsed?.msg === 'result' && parsed.id === message.id) { processResult(e.message); console.error(error); @@ -126,7 +126,7 @@ const withDDPOverREST = (_send: (this: Meteor.IMeteorConnection, message: Meteor // plain string — not a DDP frame. Re-encode as a proper DDP error // result so Accounts' resume callback clears stale creds and the // user isn't wedged on /home with no main UI. - const errorMessage = DDPCommon.stringifyDDP({ + const errorMessage = stringifyDDP({ msg: 'result', id: message.id, error: { diff --git a/apps/meteor/client/meteor/overrides/ddpSdkCollectionBridge.ts b/apps/meteor/client/meteor/overrides/ddpSdkCollectionBridge.ts index 338969c365cce..1df808b1ac2eb 100644 --- a/apps/meteor/client/meteor/overrides/ddpSdkCollectionBridge.ts +++ b/apps/meteor/client/meteor/overrides/ddpSdkCollectionBridge.ts @@ -1,6 +1,6 @@ -import { DDPCommon } from 'meteor/ddp-common'; import { Meteor } from 'meteor/meteor'; +import { type DDPMessage, stringifyDDP } from '../../lib/sdk/ddpProtocol'; import { getDdpSdk } from '../../lib/sdk/ddpSdk'; import { isSdkTransportEnabled } from '../../lib/sdk/sdkTransportEnabled'; @@ -80,9 +80,7 @@ export const installDdpSdkCollectionBridge = (): void => { // rejections are contained — Meteor keeps draining the queue even when // individual frames hit dead invokers. try { - const result = Meteor.connection._streamHandlers.onMessage( - DDPCommon.stringifyDDP(frame as Parameters[0]), - ) as unknown; + const result = Meteor.connection._streamHandlers.onMessage(stringifyDDP(frame as DDPMessage)) as unknown; if (result && typeof (result as Promise).then === 'function') { (result as Promise).catch((err) => { console.warn('[ddpSdk] bridge frame drop (async)', frame.msg, err); diff --git a/apps/meteor/client/meteor/overrides/stubMeteorStream.ts b/apps/meteor/client/meteor/overrides/stubMeteorStream.ts index 8862926f1759b..13f09db0a8643 100644 --- a/apps/meteor/client/meteor/overrides/stubMeteorStream.ts +++ b/apps/meteor/client/meteor/overrides/stubMeteorStream.ts @@ -1,8 +1,8 @@ import { Accounts } from 'meteor/accounts-base'; -import { DDPCommon } from 'meteor/ddp-common'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; +import { type DDPMessage, parseDDP, stringifyDDP } from '../../lib/sdk/ddpProtocol'; import { adoptAccountFromMeteorLoginResult, getDdpSdk } from '../../lib/sdk/ddpSdk'; import { isSdkTransportEnabled } from '../../lib/sdk/sdkTransportEnabled'; @@ -85,7 +85,7 @@ function installStubMeteorStream(): void { send(data) { let frame: { msg?: string; id?: string; method?: string; name?: string; params?: unknown[] } | undefined; try { - frame = DDPCommon.parseDDP(data) as typeof frame; + frame = parseDDP(data) as typeof frame; } catch { return; } @@ -114,9 +114,7 @@ function installStubMeteorStream(): void { }; const bridgePongFor = (id?: string): void => { - conn._streamHandlers.onMessage( - DDPCommon.stringifyDDP({ msg: 'pong', ...(id != null && { id }) } as unknown as Parameters[0]), - ); + conn._streamHandlers.onMessage(stringifyDDP({ msg: 'pong', ...(id != null && { id }) } as unknown as DDPMessage)); }; type SdkDdp = { @@ -178,10 +176,10 @@ function installStubMeteorStream(): void { if (c._lastSessionId) return; try { conn._streamHandlers.onMessage( - DDPCommon.stringifyDDP({ + stringifyDDP({ msg: 'connected', session: 'sdk-bridged', - } as unknown as Parameters[0]), + } as unknown as DDPMessage), ); fire('reset'); } catch (err) { diff --git a/apps/meteor/tests/mocks/client/meteor.ts b/apps/meteor/tests/mocks/client/meteor.ts index d17b1f3691ac0..466ccc15b6b06 100644 --- a/apps/meteor/tests/mocks/client/meteor.ts +++ b/apps/meteor/tests/mocks/client/meteor.ts @@ -50,3 +50,8 @@ export const EJSON = { clone: jest.fn((obj) => obj), equals: jest.fn((a, b) => JSON.stringify(a) === JSON.stringify(b)), }; + +export const DDPCommon = { + parseDDP: jest.fn((msg: string) => JSON.parse(msg)), + stringifyDDP: jest.fn((msg: unknown) => JSON.stringify(msg)), +};