Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
210 lines
7.2 KiB
TypeScript
210 lines
7.2 KiB
TypeScript
import type { Serialized } from '@rocket.chat/core-typings';
|
|
import type {
|
|
ServerMethodName,
|
|
ServerMethodParameters,
|
|
ServerMethodReturn,
|
|
StreamerCallbackArgs,
|
|
StreamerEvents,
|
|
StreamNames,
|
|
StreamKeys,
|
|
} from '@rocket.chat/ddp-client';
|
|
import type { Method, PathFor, OperationParams, OperationResult, UrlParams, PathPattern } from '@rocket.chat/rest-typings';
|
|
import type { UploadResult, ServerContextValue } from '@rocket.chat/ui-contexts';
|
|
import { ServerContext } from '@rocket.chat/ui-contexts';
|
|
import { Meteor } from 'meteor/meteor';
|
|
import { compile } from 'path-to-regexp';
|
|
import { useMemo, useSyncExternalStore, type ReactNode } from 'react';
|
|
|
|
import { Info as info } from '../../app/utils/rocketchat.info';
|
|
import { sdk } from '../lib/SDKClient';
|
|
import { absoluteUrl } from '../lib/absoluteUrl';
|
|
import { ensureConnectedAndAuthenticated, getDdpSdk } from '../lib/sdk/ddpSdk';
|
|
import { isSdkTransportEnabled } from '../lib/sdk/sdkTransportEnabled';
|
|
|
|
const sdkTransportEnabled = isSdkTransportEnabled();
|
|
|
|
const callMethod = <MethodName extends ServerMethodName>(
|
|
methodName: MethodName,
|
|
...args: ServerMethodParameters<MethodName>
|
|
): Promise<ServerMethodReturn<MethodName>> => sdk.call(methodName, ...(args as any)) as Promise<ServerMethodReturn<MethodName>>;
|
|
|
|
const callEndpoint = <TMethod extends Method, TPathPattern extends PathPattern>({
|
|
method,
|
|
pathPattern,
|
|
keys,
|
|
params,
|
|
signal,
|
|
}: {
|
|
method: TMethod;
|
|
pathPattern: TPathPattern;
|
|
keys: UrlParams<TPathPattern>;
|
|
params: OperationParams<TMethod, TPathPattern>;
|
|
signal?: AbortSignal;
|
|
}): Promise<Serialized<OperationResult<TMethod, TPathPattern>>> => {
|
|
const compiledPath = compile(pathPattern, { encode: encodeURIComponent })(keys) as any;
|
|
|
|
switch (method) {
|
|
case 'GET':
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unnecessary-type-assertion
|
|
return sdk.rest.get(compiledPath, params as any, { signal }) as any;
|
|
|
|
case 'POST':
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unnecessary-type-assertion
|
|
return sdk.rest.post(compiledPath, params as any, { signal }) as any;
|
|
|
|
case 'PUT':
|
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
return sdk.rest.put(compiledPath, params as never, { signal }) as never;
|
|
|
|
case 'DELETE':
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unnecessary-type-assertion
|
|
return sdk.rest.delete(compiledPath, params as any, { signal }) as any;
|
|
|
|
default:
|
|
throw new Error('Invalid HTTP method');
|
|
}
|
|
};
|
|
|
|
const uploadToEndpoint = (endpoint: PathFor<'POST'>, formData: any): Promise<UploadResult> => sdk.rest.post(endpoint as any, formData);
|
|
|
|
const getStream =
|
|
<N extends StreamNames>(
|
|
streamName: N,
|
|
_options?: {
|
|
retransmit?: boolean | undefined;
|
|
retransmitToSelf?: boolean | undefined;
|
|
},
|
|
) =>
|
|
<K extends StreamKeys<N>>(eventName: K, callback: (...args: StreamerCallbackArgs<N, K>) => void): (() => void) =>
|
|
sdk.stream(streamName, [eventName], callback).stop;
|
|
|
|
const getStreamAll =
|
|
<N extends StreamNames>(streamName: N) =>
|
|
(callback: (eventName: string, args: StreamerEvents[N][number]['args']) => void): (() => void) =>
|
|
sdk.onAnyStreamEvent(streamName, callback).stop;
|
|
|
|
const writeStream = <N extends StreamNames, K extends StreamKeys<N>>(streamName: N, streamKey: K, ...args: StreamerCallbackArgs<N, K>) =>
|
|
sdk.publish(streamName, [streamKey, ...args]);
|
|
|
|
const disconnect = sdkTransportEnabled
|
|
? () => {
|
|
Meteor.disconnect();
|
|
try {
|
|
getDdpSdk().connection.close();
|
|
} catch {
|
|
// no-op — DDPSDK may not be connected yet
|
|
}
|
|
}
|
|
: () => Meteor.disconnect();
|
|
|
|
const reconnect = sdkTransportEnabled
|
|
? () => {
|
|
Meteor.reconnect();
|
|
// ensureConnectedAndAuthenticated handles both 'connect' and loginWithToken,
|
|
// so reconnecting here also re-establishes the DDPSDK session with the
|
|
// same token Meteor resumes with.
|
|
void ensureConnectedAndAuthenticated();
|
|
}
|
|
: () => Meteor.reconnect();
|
|
|
|
type CombinedStatus = ReturnType<typeof Meteor.status>;
|
|
|
|
const sdkStatusToMeteor = (sdkStatus: string, meteor: CombinedStatus): CombinedStatus => {
|
|
const retry = { retryCount: meteor.retryCount, retryTime: meteor.retryTime };
|
|
|
|
switch (sdkStatus) {
|
|
case 'connected':
|
|
return { status: 'connected', connected: true, ...retry };
|
|
case 'connecting':
|
|
return { status: 'connecting', connected: false, ...retry };
|
|
case 'reconnecting':
|
|
return { status: 'connecting', connected: false, ...retry };
|
|
case 'failed':
|
|
return { status: 'failed', connected: false, ...retry };
|
|
case 'closed':
|
|
case 'disconnected':
|
|
return { status: 'waiting', connected: false, ...retry };
|
|
case 'idle':
|
|
default:
|
|
return { status: 'offline', connected: false, ...retry };
|
|
}
|
|
};
|
|
|
|
// With SDK transport on, combine Meteor's DDP status with DDPSDK's so the
|
|
// ConnectionStatusBar / idle-connection hooks reflect the worst-case of both
|
|
// transports. With the flag off, route status straight through Meteor —
|
|
// `meteorBackedSdk` already bridges Meteor's `_stream` events into
|
|
// `sdk.connection.on('connection')`, so the same subscription works in both
|
|
// modes.
|
|
const computeStatus: () => CombinedStatus = sdkTransportEnabled
|
|
? () => sdkStatusToMeteor(getDdpSdk().connection.status, Meteor.status())
|
|
: () => ({ ...Meteor.status() });
|
|
|
|
const isStatusEqual = (a: CombinedStatus, b: CombinedStatus): boolean =>
|
|
a.status === b.status && a.connected === b.connected && a.retryCount === b.retryCount && a.retryTime === b.retryTime;
|
|
|
|
let cachedStatus: CombinedStatus = computeStatus();
|
|
const statusListeners = new Set<() => void>();
|
|
let statusBridgeStarted = false;
|
|
|
|
const ensureStatusBridge = (): void => {
|
|
if (statusBridgeStarted) return;
|
|
statusBridgeStarted = true;
|
|
getDdpSdk().connection.on('connection', () => {
|
|
const next = computeStatus();
|
|
if (isStatusEqual(cachedStatus, next)) return;
|
|
cachedStatus = next;
|
|
statusListeners.forEach((cb) => cb());
|
|
});
|
|
};
|
|
|
|
const subscribeStatus = (cb: () => void): (() => void) => {
|
|
ensureStatusBridge();
|
|
// Close the race between the cachedStatus computed at module load and the
|
|
// first event delivered through the bridge: status may have changed in
|
|
// between (e.g. the socket connected before <ServerProvider> mounted), and
|
|
// useSyncExternalStore would otherwise surface the stale snapshot until
|
|
// the next external transition.
|
|
const next = computeStatus();
|
|
if (!isStatusEqual(cachedStatus, next)) {
|
|
cachedStatus = next;
|
|
cb();
|
|
}
|
|
statusListeners.add(cb);
|
|
return () => {
|
|
statusListeners.delete(cb);
|
|
};
|
|
};
|
|
|
|
const getStatusSnapshot = (): CombinedStatus => cachedStatus;
|
|
|
|
export type ServerProviderProps = { children?: ReactNode };
|
|
|
|
const ServerProvider = ({ children }: ServerProviderProps) => {
|
|
const { connected, status, retryCount, retryTime } = useSyncExternalStore(subscribeStatus, getStatusSnapshot);
|
|
|
|
const value = useMemo(
|
|
(): ServerContextValue => ({
|
|
connected,
|
|
status,
|
|
retryCount,
|
|
retryTime,
|
|
info,
|
|
absoluteUrl,
|
|
callMethod,
|
|
callEndpoint,
|
|
uploadToEndpoint,
|
|
getStream,
|
|
getStreamAll,
|
|
writeStream,
|
|
disconnect,
|
|
reconnect,
|
|
}),
|
|
[connected, retryCount, retryTime, status],
|
|
);
|
|
|
|
return <ServerContext.Provider value={value}>{children}</ServerContext.Provider>;
|
|
};
|
|
|
|
export default ServerProvider;
|