Skip to content

Commit fe65e8f

Browse files
ggazzotassoevan
andauthored
refactor(client): use SDK storage helper in AuthenticationProvider.getLoginToken (#40482)
Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat>
1 parent 7f61187 commit fe65e8f

8 files changed

Lines changed: 84 additions & 65 deletions

File tree

apps/meteor/client/hooks/useReactiveValue.ts

Lines changed: 0 additions & 9 deletions
This file was deleted.

apps/meteor/client/lib/cachedStores/CachedStoresManager.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { IWithManageableCache } from './CachedStore';
2+
import { getDdpSdk } from '../sdk/ddpSdk';
23

34
class CachedStoresManager {
45
private items = new Set<IWithManageableCache>();
@@ -16,6 +17,8 @@ class CachedStoresManager {
1617

1718
const instance = new CachedStoresManager();
1819

20+
getDdpSdk().account.onLogout(() => instance.clearAllCachesOnLogout());
21+
1922
export {
2023
/** @deprecated */
2124
instance as CachedStoresManager,

apps/meteor/client/lib/sdk/ddpSdk.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { Meteor } from 'meteor/meteor';
55

66
import { createMeteorBackedSdk } from './meteorBackedSdk';
77
import { isSdkTransportEnabled } from './sdkTransportEnabled';
8-
import { STORAGE_KEYS, getStoredItem } from './storage';
98
import { getRootUrl } from '../meteorRuntimeConfig';
9+
import { STORAGE_KEYS, getStoredItem, removeStoredItem } from './storage';
1010
import { userIdStore } from '../user';
1111

1212
const sdkTransportEnabled = isSdkTransportEnabled();
@@ -143,7 +143,9 @@ export const ensureConnectedAndAuthenticated = async (): Promise<void> => {
143143
// latter dispatches a `logout` method which itself races against
144144
// parallel re-auth flows in CI's parallel-shard environment and
145145
// kicked otherwise-healthy tests out.
146-
Accounts._unstoreLoginToken();
146+
removeStoredItem(STORAGE_KEYS.USER_ID);
147+
removeStoredItem(STORAGE_KEYS.LOGIN_TOKEN);
148+
removeStoredItem(STORAGE_KEYS.LOGIN_TOKEN_EXPIRES);
147149
Meteor.connection.setUserId(null);
148150
return;
149151
}

apps/meteor/client/lib/sdk/storage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
export const STORAGE_KEYS = {
77
USER_ID: 'Meteor.userId',
88
LOGIN_TOKEN: 'Meteor.loginToken',
9+
LOGIN_TOKEN_EXPIRES: 'Meteor.loginTokenExpires',
910
E2EE_PUBLIC_KEY: 'public_key',
1011
E2EE_PRIVATE_KEY: 'private_key',
1112
E2EE_RANDOM_PASSWORD: 'e2e.randomPassword',

apps/meteor/client/meteor/overrides/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,4 @@ import './desktopInjection';
77
import './oauthRedirectUri';
88
import './settings';
99
import './totpOnCall';
10-
import './unstoreLoginToken';
1110
import './userAndUsers';

apps/meteor/client/meteor/overrides/unstoreLoginToken.ts

Lines changed: 0 additions & 9 deletions
This file was deleted.

apps/meteor/client/providers/AuthenticationProvider/AuthenticationProvider.tsx

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import { AuthenticationContext, useSetting } from '@rocket.chat/ui-contexts';
44
import { Accounts } from 'meteor/accounts-base';
55
import { Meteor } from 'meteor/meteor';
66
import type { ContextType, ReactElement, ReactNode } from 'react';
7-
import { useMemo } from 'react';
7+
import { useMemo, useSyncExternalStore } from 'react';
88

99
import { useLDAPAndCrowdCollisionWarning } from './hooks/useLDAPAndCrowdCollisionWarning';
1010
import { capitalize as capitalizeService } from '../../../lib/utils/stringUtils';
11-
import { useReactiveValue } from '../../hooks/useReactiveValue';
1211
import { loginServices } from '../../lib/loginServices';
12+
import { getDdpSdk } from '../../lib/sdk/ddpSdk';
13+
import { STORAGE_KEYS, getStoredItem, removeStoredItem } from '../../lib/sdk/storage';
1314

1415
export type LoginMethods = keyof typeof Meteor extends infer T ? (T extends `loginWith${string}` ? T : never) : never;
1516

@@ -27,7 +28,34 @@ const callLoginMethod = (
2728
});
2829
};
2930

30-
const getLoggingIn = () => Accounts.loggingIn();
31+
// Bridge Accounts.loggingIn() — Meteor's Tracker-reactive flag — into a
32+
// non-reactive subscribe/getSnapshot pair for useSyncExternalStore. We hook
33+
// `_setLoggingIn` (Meteor's internal flip, also accessed in
34+
// apps/meteor/client/meteor/overrides/killMeteorStream.ts) to fan out
35+
// transitions without entering a Tracker computation.
36+
const loggingInListeners = new Set<() => void>();
37+
let loggingInBridgeInstalled = false;
38+
const installLoggingInBridge = (): void => {
39+
if (loggingInBridgeInstalled) return;
40+
loggingInBridgeInstalled = true;
41+
const wrap = Accounts as unknown as { _setLoggingIn?: (v: boolean) => void };
42+
const original = wrap._setLoggingIn;
43+
if (typeof original !== 'function') return;
44+
wrap._setLoggingIn = function (this: typeof Accounts, v: boolean) {
45+
original.call(this, v);
46+
loggingInListeners.forEach((cb) => cb());
47+
};
48+
};
49+
50+
const subscribeLoggingIn = (cb: () => void): (() => void) => {
51+
installLoggingInBridge();
52+
loggingInListeners.add(cb);
53+
return () => {
54+
loggingInListeners.delete(cb);
55+
};
56+
};
57+
58+
const getLoggingInSnapshot = (): boolean => Accounts.loggingIn();
3159

3260
const AuthenticationProvider = ({ children }: AuthenticationProviderProps): ReactElement => {
3361
const isLdapEnabled = useSetting('LDAP_Enable', false);
@@ -37,7 +65,7 @@ const AuthenticationProvider = ({ children }: AuthenticationProviderProps): Reac
3765

3866
useLDAPAndCrowdCollisionWarning();
3967

40-
const isLoggingIn = useReactiveValue(getLoggingIn);
68+
const isLoggingIn = useSyncExternalStore(subscribeLoggingIn, getLoggingInSnapshot);
4169

4270
const contextValue = useMemo(
4371
(): ContextType<typeof AuthenticationContext> => ({
@@ -123,29 +151,18 @@ const AuthenticationProvider = ({ children }: AuthenticationProviderProps): Reac
123151
resolve();
124152
});
125153
}),
126-
getLoginToken: () => Accounts.storageLocation.getItem(Accounts.LOGIN_TOKEN_KEY) ?? null,
154+
getLoginToken: () => getStoredItem(STORAGE_KEYS.LOGIN_TOKEN),
127155
wipeLocalAuth: () => {
128-
try {
129-
Accounts._unstoreLoginToken();
130-
} catch {
131-
// ignore
132-
}
156+
removeStoredItem(STORAGE_KEYS.USER_ID);
157+
removeStoredItem(STORAGE_KEYS.LOGIN_TOKEN);
158+
removeStoredItem(STORAGE_KEYS.LOGIN_TOKEN_EXPIRES);
133159
try {
134160
Meteor.connection.setUserId(null);
135161
} catch {
136162
// ignore
137163
}
138164
},
139-
unstoreLoginToken: (callback) => {
140-
const { _unstoreLoginToken } = Accounts;
141-
Accounts._unstoreLoginToken = function (...args) {
142-
callback();
143-
_unstoreLoginToken.apply(Accounts, args);
144-
};
145-
return () => {
146-
Accounts._unstoreLoginToken = _unstoreLoginToken;
147-
};
148-
},
165+
unstoreLoginToken: (callback) => getDdpSdk().account.onLogout(callback),
149166
queryLoginServices: {
150167
getCurrentValue: () => loginServices.getLoginServiceButtons(),
151168
subscribe: (onStoreChange: () => void) => loginServices.on('changed', onStoreChange),

apps/meteor/client/providers/ServerProvider.tsx

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,11 @@ import type { Method, PathFor, OperationParams, OperationResult, UrlParams, Path
1111
import type { UploadResult, ServerContextValue } from '@rocket.chat/ui-contexts';
1212
import { ServerContext } from '@rocket.chat/ui-contexts';
1313
import { Meteor } from 'meteor/meteor';
14-
import { Tracker } from 'meteor/tracker';
1514
import { compile } from 'path-to-regexp';
16-
import { useMemo, type ReactNode } from 'react';
15+
import { useMemo, useSyncExternalStore, type ReactNode } from 'react';
1716

1817
import { sdk } from '../../app/utils/client/lib/SDKClient';
1918
import { Info as info } from '../../app/utils/rocketchat.info';
20-
import { useReactiveValue } from '../hooks/useReactiveValue';
2119
import { absoluteUrl } from '../lib/absoluteUrl';
2220
import { ensureConnectedAndAuthenticated, getDdpSdk } from '../lib/sdk/ddpSdk';
2321
import { isSdkTransportEnabled } from '../lib/sdk/sdkTransportEnabled';
@@ -99,16 +97,6 @@ const reconnect = sdkTransportEnabled
9997
}
10098
: () => Meteor.reconnect();
10199

102-
// With SDK transport on, combine Meteor's DDP status with DDPSDK's so the
103-
// ConnectionStatusBar / idle-connection hooks reflect the worst-case of both
104-
// transports. With the flag off, route status straight through Meteor —
105-
// Meteor.status() is already Tracker-reactive, so adding a second dependency
106-
// via the meteor-backed proxy's emitter would just double-fire the autorun.
107-
const ddpSdkStatusDep = sdkTransportEnabled ? new Tracker.Dependency() : undefined;
108-
if (sdkTransportEnabled) {
109-
getDdpSdk().connection.on('connection', () => ddpSdkStatusDep!.changed());
110-
}
111-
112100
type CombinedStatus = ReturnType<typeof Meteor.status>;
113101

114102
const sdkStatusToMeteor = (sdkStatus: string, meteor: CombinedStatus): CombinedStatus => {
@@ -132,21 +120,48 @@ const sdkStatusToMeteor = (sdkStatus: string, meteor: CombinedStatus): CombinedS
132120
}
133121
};
134122

135-
const getStatus = sdkTransportEnabled
136-
? () => {
137-
ddpSdkStatusDep!.depend();
138-
return sdkStatusToMeteor(getDdpSdk().connection.status, Meteor.status());
139-
}
140-
: // useReactiveValue stores the snapshot in useSyncExternalStore, which
141-
// compares by identity. Meteor.status() reuses the same internal object
142-
// (mutated in place), so without spreading the snapshot reference never
143-
// changes and ConnectionStatusBar stops re-rendering on connect/drop.
144-
() => ({ ...Meteor.status() });
123+
// With SDK transport on, combine Meteor's DDP status with DDPSDK's so the
124+
// ConnectionStatusBar / idle-connection hooks reflect the worst-case of both
125+
// transports. With the flag off, route status straight through Meteor —
126+
// `meteorBackedSdk` already bridges Meteor's `_stream` events into
127+
// `sdk.connection.on('connection')`, so the same subscription works in both
128+
// modes.
129+
const computeStatus: () => CombinedStatus = sdkTransportEnabled
130+
? () => sdkStatusToMeteor(getDdpSdk().connection.status, Meteor.status())
131+
: () => ({ ...Meteor.status() });
132+
133+
const isStatusEqual = (a: CombinedStatus, b: CombinedStatus): boolean =>
134+
a.status === b.status && a.connected === b.connected && a.retryCount === b.retryCount && a.retryTime === b.retryTime;
135+
136+
let cachedStatus: CombinedStatus = computeStatus();
137+
const statusListeners = new Set<() => void>();
138+
let statusBridgeStarted = false;
139+
140+
const ensureStatusBridge = (): void => {
141+
if (statusBridgeStarted) return;
142+
statusBridgeStarted = true;
143+
getDdpSdk().connection.on('connection', () => {
144+
const next = computeStatus();
145+
if (isStatusEqual(cachedStatus, next)) return;
146+
cachedStatus = next;
147+
statusListeners.forEach((cb) => cb());
148+
});
149+
};
150+
151+
const subscribeStatus = (cb: () => void): (() => void) => {
152+
ensureStatusBridge();
153+
statusListeners.add(cb);
154+
return () => {
155+
statusListeners.delete(cb);
156+
};
157+
};
158+
159+
const getStatusSnapshot = (): CombinedStatus => cachedStatus;
145160

146161
type ServerProviderProps = { children?: ReactNode };
147162

148163
const ServerProvider = ({ children }: ServerProviderProps) => {
149-
const { connected, status, retryCount, retryTime } = useReactiveValue(getStatus);
164+
const { connected, status, retryCount, retryTime } = useSyncExternalStore(subscribeStatus, getStatusSnapshot);
150165

151166
const value = useMemo(
152167
(): ServerContextValue => ({

0 commit comments

Comments
 (0)