snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
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
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
This commit is contained in:
456
apps/meteor/client/lib/SDKClient.ts
Normal file
456
apps/meteor/client/lib/SDKClient.ts
Normal file
@@ -0,0 +1,456 @@
|
||||
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 { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { APIClient } from './RestApiClient';
|
||||
import { parseDDP } from './sdk/ddpProtocol';
|
||||
import { ensureConnectedAndAuthenticated, getDdpSdk } from './sdk/ddpSdk';
|
||||
import { isSdkTransportEnabled } from './sdk/sdkTransportEnabled';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface SDK {
|
||||
stream<N extends StreamNames, K extends StreamKeys<N>>(
|
||||
streamName: N,
|
||||
args: [key: K, ...args: unknown[]],
|
||||
callback: (...args: StreamerCallbackArgs<N, K>) => void,
|
||||
): ReturnType<ClientStream['subscribe']>;
|
||||
onAnyStreamEvent<N extends StreamNames>(name: N, callback: (eventName: string, args: unknown[]) => void): { stop: () => void };
|
||||
call<T extends keyof ServerMethods>(method: T, ...args: Parameters<ServerMethods[T]>): Promise<ReturnType<ServerMethods[T]>>;
|
||||
}
|
||||
}
|
||||
|
||||
const sdkTransportEnabled = isSdkTransportEnabled();
|
||||
|
||||
const isChangedCollectionPayload = (
|
||||
msg: any,
|
||||
): msg is { msg: 'changed'; collection: string; fields: { eventName: string; args: unknown[] } } => {
|
||||
if (typeof msg !== 'object' && (msg !== null || msg !== undefined)) {
|
||||
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)) {
|
||||
return false;
|
||||
}
|
||||
if (typeof msg.fields.eventName !== 'string') {
|
||||
return false;
|
||||
}
|
||||
if (!Array.isArray(msg.fields.args)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
type EventMap<N extends StreamNames = StreamNames, K extends StreamKeys<N> = StreamKeys<N>> = {
|
||||
[key in `stream-${N}/${K}`]: StreamerCallbackArgs<N, K>;
|
||||
};
|
||||
|
||||
type StreamMapValue = {
|
||||
stop: () => void;
|
||||
onError: (cb: (...args: any[]) => void) => () => void;
|
||||
onChange: ReturnType<ClientStream['subscribe']>['onChange'];
|
||||
onStop: (cb: () => void) => () => void;
|
||||
ready: () => Promise<void>;
|
||||
isReady: boolean;
|
||||
unsubList: Set<() => void>;
|
||||
};
|
||||
|
||||
const createNewMeteorStream = (streamName: StreamNames, key: StreamKeys<StreamNames>, args: unknown[]): StreamMapValue => {
|
||||
const ee = new Emitter<{
|
||||
ready: [error: any] | [undefined, any];
|
||||
error: [error: any];
|
||||
stop: undefined;
|
||||
}>();
|
||||
const meta = {
|
||||
ready: false,
|
||||
};
|
||||
|
||||
const sub = Meteor.connection.subscribe(
|
||||
`stream-${streamName}`,
|
||||
key,
|
||||
{ useCollection: false, args },
|
||||
{
|
||||
onReady: (args: any) => {
|
||||
meta.ready = true;
|
||||
ee.emit('ready', [undefined, args]);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
ee.emit('ready', [err]);
|
||||
ee.emit('error', err);
|
||||
},
|
||||
onStop: () => {
|
||||
ee.emit('stop');
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const onChange: ReturnType<ClientStream['subscribe']>['onChange'] = (cb) => {
|
||||
if (meta.ready) {
|
||||
cb({
|
||||
msg: 'ready',
|
||||
|
||||
subs: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
ee.once('ready', ([error, result]) => {
|
||||
if (error) {
|
||||
cb({
|
||||
msg: 'nosub',
|
||||
|
||||
id: '',
|
||||
error,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
cb(result);
|
||||
});
|
||||
};
|
||||
|
||||
const ready = () => {
|
||||
if (meta.ready) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
ee.once('ready', ([err]) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
stop: sub.stop,
|
||||
onChange,
|
||||
ready,
|
||||
onError: (cb: (...args: any[]) => void) =>
|
||||
ee.once('error', (error) => {
|
||||
cb(error);
|
||||
}),
|
||||
|
||||
onStop: (cb: () => void) => ee.once('stop', cb),
|
||||
get isReady() {
|
||||
return meta.ready;
|
||||
},
|
||||
unsubList: new Set(),
|
||||
};
|
||||
};
|
||||
|
||||
const createNewDdpSdkStream = (
|
||||
streamProxy: Emitter<EventMap>,
|
||||
streamName: StreamNames,
|
||||
key: StreamKeys<StreamNames>,
|
||||
args: unknown[],
|
||||
): StreamMapValue => {
|
||||
const ee = new Emitter<{
|
||||
ready: [error: any] | [undefined, any];
|
||||
error: [error: any];
|
||||
stop: undefined;
|
||||
}>();
|
||||
const meta = { ready: false };
|
||||
|
||||
// Defer the actual `subscribe` until DDPSDK is authenticated. Without this,
|
||||
// stream subscriptions fired immediately after re-login (e.g. the
|
||||
// SubscriptionsCachedStore's `notify-user/<uid>/subscriptions-changed`
|
||||
// listener that re-arms via onLoggedIn) hit the SDK socket while it's
|
||||
// still anonymous — server rejects with `not-allowed`/`nosub`, the
|
||||
// stream's `ready` promise emits an error, and the cached store never
|
||||
// receives subsequent server events. The visible failure: an agent that
|
||||
// just took a livechat chat post-relogin sees the chat work but the
|
||||
// "Move to the queue" button never appears, because the new subscription
|
||||
// the server creates for that agent is never replicated to the client's
|
||||
// Subscriptions store, and pseudoRoom (= {...sub, ...room}) ends up with
|
||||
// no `u` for the canMoveQueue check.
|
||||
let subscription: ReturnType<ReturnType<typeof getDdpSdk>['client']['subscribe']> | undefined;
|
||||
let offCollection: (() => void) | undefined;
|
||||
let stopped = false;
|
||||
|
||||
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}`, data.fields.args);
|
||||
});
|
||||
});
|
||||
|
||||
const onChange: ReturnType<ClientStream['subscribe']>['onChange'] = (cb) => {
|
||||
if (meta.ready) {
|
||||
cb({ msg: 'ready', subs: [] });
|
||||
return;
|
||||
}
|
||||
ee.once('ready', ([error, result]) => {
|
||||
if (error) {
|
||||
cb({ msg: 'nosub', id: '', error });
|
||||
return;
|
||||
}
|
||||
cb(result);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
stop: () => {
|
||||
// Mirror Meteor's subscription semantics: explicit stop() does not fire the
|
||||
// 'stop' event (onStop is reserved for server-initiated closures).
|
||||
// Emitting it here would recurse through the onStop handler that
|
||||
// createStreamManager registers, which itself iterates the unsubList.
|
||||
stopped = true;
|
||||
offCollection?.();
|
||||
subscription?.stop();
|
||||
},
|
||||
onChange,
|
||||
ready: () => {
|
||||
if (meta.ready) return Promise.resolve();
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
ee.once('ready', ([err]) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
onError: (cb: (...args: any[]) => void) => ee.once('error', (error) => cb(error)),
|
||||
onStop: (cb: () => void) => ee.once('stop', cb),
|
||||
get isReady() {
|
||||
return meta.ready;
|
||||
},
|
||||
unsubList: new Set(),
|
||||
};
|
||||
};
|
||||
|
||||
const createStreamManager = () => {
|
||||
// Emitter that replicates stream messages to registered callbacks
|
||||
const streamProxy = new Emitter<EventMap>();
|
||||
|
||||
const streams = new Map<string, StreamMapValue>();
|
||||
|
||||
getDdpSdk().account.onLogout(() => {
|
||||
streams.forEach((stream) => {
|
||||
stream.unsubList.forEach((stop) => stop());
|
||||
});
|
||||
});
|
||||
|
||||
if (!sdkTransportEnabled) {
|
||||
// In legacy Meteor mode, stream frames arrive on Meteor.connection._stream
|
||||
// as `changed` collection messages — bridge them into streamProxy so the
|
||||
// per-stream callbacks fire. With SDK transport on, the frames arrive on
|
||||
// the SDK socket and createNewDdpSdkStream registers its own onCollection
|
||||
// listener instead.
|
||||
Meteor.connection._stream!.on('message', (rawMsg: string) => {
|
||||
const msg = parseDDP(rawMsg);
|
||||
if (!isChangedCollectionPayload(msg)) {
|
||||
return;
|
||||
}
|
||||
streamProxy.emit(`${msg.collection}/${msg.fields.eventName}` as any, msg.fields.args as any);
|
||||
});
|
||||
}
|
||||
|
||||
const stream: SDK['stream'] = <N extends StreamNames, K extends StreamKeys<N>>(
|
||||
name: N,
|
||||
data: [key: K, ...args: unknown[]],
|
||||
callback: (...args: StreamerCallbackArgs<N, K>) => void,
|
||||
_options?: {
|
||||
retransmit?: boolean | undefined;
|
||||
retransmitToSelf?: boolean | undefined;
|
||||
},
|
||||
): ReturnType<ClientStream['subscribe']> => {
|
||||
const [key, ...args] = data;
|
||||
const eventLiteral = `stream-${name}/${key}` as const;
|
||||
|
||||
const proxyCallback = (args?: unknown): void => {
|
||||
if (!args || !Array.isArray(args)) {
|
||||
throw new Error('Invalid streamer callback');
|
||||
}
|
||||
callback(...(args as StreamerCallbackArgs<N, K>));
|
||||
};
|
||||
|
||||
streamProxy.on(eventLiteral, proxyCallback);
|
||||
|
||||
const stream =
|
||||
streams.get(eventLiteral) ||
|
||||
(sdkTransportEnabled ? createNewDdpSdkStream(streamProxy, name, key, args) : createNewMeteorStream(name, key, args));
|
||||
|
||||
const stop = (): void => {
|
||||
streamProxy.off(eventLiteral, proxyCallback);
|
||||
// If someone is still listening, don't unsubscribe
|
||||
if (streamProxy.has(eventLiteral)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (stream) {
|
||||
stream.stop();
|
||||
streams.delete(eventLiteral);
|
||||
}
|
||||
};
|
||||
|
||||
stream.unsubList.add(stop);
|
||||
if (!streams.has(eventLiteral)) {
|
||||
const offError = stream.onError(() => {
|
||||
stream.unsubList.forEach((stop) => stop());
|
||||
});
|
||||
|
||||
const offStop = stream.onStop(() => {
|
||||
stream.unsubList.forEach((stop) => stop());
|
||||
});
|
||||
|
||||
stream.unsubList.add(offError);
|
||||
stream.unsubList.add(offStop);
|
||||
streams.set(eventLiteral, stream);
|
||||
}
|
||||
|
||||
return {
|
||||
id: '',
|
||||
name,
|
||||
params: data as any,
|
||||
stop,
|
||||
ready: stream.ready,
|
||||
onChange: stream.onChange,
|
||||
isReady: stream.isReady,
|
||||
};
|
||||
};
|
||||
|
||||
const stopAll = (streamName: string, key: string) => {
|
||||
const stream = streams.get(`stream-${streamName}/${key}`);
|
||||
|
||||
if (stream) {
|
||||
stream.unsubList.forEach((stop) => stop());
|
||||
}
|
||||
};
|
||||
|
||||
return { stream, stopAll };
|
||||
};
|
||||
|
||||
// Per-stream wildcard emitters for `onAnyStreamEvent`. Each emitter is fed by
|
||||
// up to two sources (de-duplicated by the underlying bridges):
|
||||
// 1) `getDdpSdk().client.onCollection(...)` — the canonical bridge. Covers
|
||||
// both transport-OFF (via `meteorBackedSdk.onCollection`, which listens
|
||||
// on `Meteor.connection._stream`) and transport-ON (via the real DDPSDK
|
||||
// socket).
|
||||
// 2) When SDK transport is ON, a direct `Meteor.connection._stream` bridge
|
||||
// to catch frames that land on Meteor while the SDK socket is still
|
||||
// authenticating — see `apps/meteor/client/lib/presence.ts`'s fallback
|
||||
// to `Meteor.subscribe('stream-user-presence', ...)`. This is the
|
||||
// "artificial trigger" and is TEMPORARY: it disappears once SDK
|
||||
// transport rollout completes and the Meteor fallback is removed.
|
||||
//
|
||||
// Bridges are wired exactly once per stream name (singleton listeners on the
|
||||
// underlying Meteor stream — Meteor's `on()` has no `off()`, so we can't
|
||||
// detach them per subscription anyway). Consumers register/unregister on the
|
||||
// per-stream Emitter, which DOES support `off`, so unsubscription is clean.
|
||||
const anyStreamEmitters = new Map<string, Emitter<Record<string, [eventName: string, args: unknown[]]>>>();
|
||||
const anyStreamBridged = new Set<string>();
|
||||
|
||||
const createOnAnyStreamEvent = () => {
|
||||
return <N extends StreamNames>(name: N, callback: (eventName: string, args: unknown[]) => void): { stop: () => void } => {
|
||||
const collectionId = `stream-${name}`;
|
||||
|
||||
let emitter = anyStreamEmitters.get(collectionId);
|
||||
if (!emitter) {
|
||||
emitter = new Emitter<Record<string, [eventName: string, args: unknown[]]>>();
|
||||
anyStreamEmitters.set(collectionId, emitter);
|
||||
}
|
||||
|
||||
if (!anyStreamBridged.has(collectionId)) {
|
||||
anyStreamBridged.add(collectionId);
|
||||
const bridgeEmitter = emitter;
|
||||
|
||||
// Primary bridge — works for both transport-OFF (meteorBackedSdk's
|
||||
// onCollection listens on Meteor.connection._stream) and transport-ON
|
||||
// (real DDPSDK socket).
|
||||
getDdpSdk().client.onCollection(collectionId, (data: unknown) => {
|
||||
if (!isChangedCollectionPayload(data)) return;
|
||||
if (data.collection !== collectionId) return;
|
||||
bridgeEmitter.emit('event', [data.fields.eventName, data.fields.args]);
|
||||
});
|
||||
|
||||
// Temporary secondary bridge — see block comment above. Only needed
|
||||
// while SDK transport is on AND Meteor.connection may still receive
|
||||
// frames during the SDK socket's anonymous window.
|
||||
if (sdkTransportEnabled) {
|
||||
Meteor.connection._stream!.on('message', (rawMsg: string) => {
|
||||
let msg: unknown;
|
||||
try {
|
||||
msg = parseDDP(rawMsg);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!isChangedCollectionPayload(msg)) return;
|
||||
if (msg.collection !== collectionId) return;
|
||||
bridgeEmitter.emit('event', [msg.fields.eventName, msg.fields.args]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const handler = ([eventName, args]: [string, unknown[]]): void => {
|
||||
callback(eventName, args);
|
||||
};
|
||||
emitter.on('event', handler);
|
||||
|
||||
return {
|
||||
stop: () => {
|
||||
emitter.off('event', handler);
|
||||
},
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const createSDK = (rest: RestClientInterface) => {
|
||||
const { stream, stopAll } = createStreamManager();
|
||||
const onAnyStreamEvent = createOnAnyStreamEvent();
|
||||
|
||||
const publish = sdkTransportEnabled
|
||||
? (name: string, args: unknown[]) => {
|
||||
// DDPSDK queues outbound frames until the WebSocket handshake completes,
|
||||
// so there's no need to gate on an isReady flag here.
|
||||
void getDdpSdk().client.callAsync(`stream-${name}`, ...args);
|
||||
}
|
||||
: (name: string, args: unknown[]) => {
|
||||
Meteor.call(`stream-${name}`, ...args);
|
||||
};
|
||||
|
||||
const call = <T extends keyof ServerMethods>(method: T, ...args: Parameters<ServerMethods[T]>): Promise<ReturnType<ServerMethods[T]>> => {
|
||||
return Meteor.callAsync(method, ...args);
|
||||
};
|
||||
|
||||
return {
|
||||
rest,
|
||||
stop: stopAll,
|
||||
stream,
|
||||
onAnyStreamEvent,
|
||||
publish,
|
||||
call,
|
||||
};
|
||||
};
|
||||
|
||||
export const sdk = createSDK(APIClient);
|
||||
Reference in New Issue
Block a user