diff --git a/src/common/lib/client/baserealtime.ts b/src/common/lib/client/baserealtime.ts index 47e1c54d4a..6e4cd67058 100644 --- a/src/common/lib/client/baserealtime.ts +++ b/src/common/lib/client/baserealtime.ts @@ -9,8 +9,7 @@ import ProtocolMessage from '../types/protocolmessage'; import { ChannelOptions } from '../../types/channel'; import ClientOptions from '../../types/ClientOptions'; import * as API from '../../../../ably'; -import { ModulesMap } from './modulesmap'; -import RealtimePresence from './realtimepresence'; +import { ModulesMap, RealtimePresenceModule } from './modulesmap'; import { TransportNames } from 'common/constants/TransportName'; import { TransportImplementations } from 'common/platform'; import { RealtimePublishing } from './realtimepublishing'; @@ -19,7 +18,7 @@ import { RealtimePublishing } from './realtimepublishing'; `BaseRealtime` is an export of the tree-shakable version of the SDK, and acts as the base class for the `DefaultRealtime` class exported by the non tree-shakable version. */ class BaseRealtime extends BaseClient { - readonly _RealtimePresence: typeof RealtimePresence | null; + readonly _RealtimePresence: RealtimePresenceModule | null; readonly __RealtimePublishing: typeof RealtimePublishing | null; // Extra transport implementations available to this client, in addition to those in Platform.Transports.bundledImplementations readonly _additionalTransportImplementations: TransportImplementations; diff --git a/src/common/lib/client/defaultrealtime.ts b/src/common/lib/client/defaultrealtime.ts index 235e585cb7..6982d76eab 100644 --- a/src/common/lib/client/defaultrealtime.ts +++ b/src/common/lib/client/defaultrealtime.ts @@ -12,6 +12,10 @@ import { DefaultPresenceMessage } from '../types/defaultpresencemessage'; import initialiseWebSocketTransport from '../transport/websockettransport'; import { FilteredSubscriptions } from './filteredsubscriptions'; import { RealtimePublishing } from './realtimepublishing'; +import { + fromValues as presenceMessageFromValues, + fromValuesArray as presenceMessagesFromValuesArray, +} from '../types/presencemessage'; /** `DefaultRealtime` is the class that the non tree-shakable version of the SDK exports as `Realtime`. It ensures that this version of the SDK includes all of the functionality which is optionally available in the tree-shakable version. @@ -27,7 +31,11 @@ export class DefaultRealtime extends BaseRealtime { ...allCommonModules, Crypto: DefaultRealtime.Crypto ?? undefined, MsgPack, - RealtimePresence, + RealtimePresence: { + RealtimePresence, + presenceMessageFromValues, + presenceMessagesFromValuesArray, + }, WebSocketTransport: initialiseWebSocketTransport, MessageInteractions: FilteredSubscriptions, RealtimePublishing: RealtimePublishing, diff --git a/src/common/lib/client/modulesmap.ts b/src/common/lib/client/modulesmap.ts index 26aff02166..17d1a9e6ad 100644 --- a/src/common/lib/client/modulesmap.ts +++ b/src/common/lib/client/modulesmap.ts @@ -7,12 +7,25 @@ import XHRRequest from 'platform/web/lib/http/request/xhrrequest'; import fetchRequest from 'platform/web/lib/http/request/fetchrequest'; import { FilteredSubscriptions } from './filteredsubscriptions'; import { RealtimePublishing } from './realtimepublishing'; +import { + fromValues as presenceMessageFromValues, + fromValuesArray as presenceMessagesFromValuesArray, +} from '../types/presencemessage'; + +export interface PresenceMessageModule { + presenceMessageFromValues: typeof presenceMessageFromValues; + presenceMessagesFromValuesArray: typeof presenceMessagesFromValuesArray; +} + +export type RealtimePresenceModule = PresenceMessageModule & { + RealtimePresence: typeof RealtimePresence; +}; export interface ModulesMap { Rest?: typeof Rest; Crypto?: IUntypedCryptoStatic; MsgPack?: MsgPack; - RealtimePresence?: typeof RealtimePresence; + RealtimePresence?: RealtimePresenceModule; WebSocketTransport?: TransportInitialiser; XHRPolling?: TransportInitialiser; XHRStreaming?: TransportInitialiser; diff --git a/src/common/lib/client/realtimechannel.ts b/src/common/lib/client/realtimechannel.ts index 33cd1173c9..67ebc138e4 100644 --- a/src/common/lib/client/realtimechannel.ts +++ b/src/common/lib/client/realtimechannel.ts @@ -10,11 +10,7 @@ import RealtimePresence from './realtimepresence'; import Message, { decode as decodeMessage } from '../types/message'; import ChannelStateChange from './channelstatechange'; import ErrorInfo, { IPartialErrorInfo, PartialErrorInfo } from '../types/errorinfo'; -import PresenceMessage, { - fromValues as presenceMessageFromValues, - fromValuesArray as presenceMessagesFromValuesArray, - decode as decodePresenceMessage, -} from '../types/presencemessage'; +import PresenceMessage, { decode as decodePresenceMessage } from '../types/presencemessage'; import ConnectionErrors from '../transport/connectionerrors'; import * as API from '../../../../ably'; import ConnectionManager from '../transport/connectionmanager'; @@ -102,7 +98,7 @@ class RealtimeChannel extends EventEmitter { this.name = name; this.channelOptions = normaliseChannelOptions(client._Crypto ?? null, options); this.client = client; - this._presence = client._RealtimePresence ? new client._RealtimePresence(this) : null; + this._presence = client._RealtimePresence ? new client._RealtimePresence.RealtimePresence(this) : null; this.connectionManager = client.connection.connectionManager; this.state = 'initialized'; this.subscriptions = new EventEmitter(); @@ -440,8 +436,8 @@ class RealtimeChannel extends EventEmitter { action: actions.PRESENCE, channel: this.name, presence: Utils.isArray(presence) - ? presenceMessagesFromValuesArray(presence) - : [presenceMessageFromValues(presence)], + ? this.client._RealtimePresence!.presenceMessagesFromValuesArray(presence) + : [this.client._RealtimePresence!.presenceMessageFromValues(presence)], }); this.sendMessage(msg, callback); } @@ -516,7 +512,12 @@ class RealtimeChannel extends EventEmitter { if (!message.presence) break; // eslint-disable-next-line no-fallthrough case actions.PRESENCE: { - const presence = message.presence as Array; + const presence = message.presence; + + if (!presence) { + break; + } + const { id, connectionId, timestamp } = message; const options = this.channelOptions; diff --git a/src/common/lib/transport/comettransport.ts b/src/common/lib/transport/comettransport.ts index 2a68701bd1..af1f9a020d 100644 --- a/src/common/lib/transport/comettransport.ts +++ b/src/common/lib/transport/comettransport.ts @@ -348,7 +348,10 @@ abstract class CometTransport extends Transport { try { const items = this.decodeResponse(responseData); if (items && items.length) - for (let i = 0; i < items.length; i++) this.onProtocolMessage(protocolMessageFromDeserialized(items[i])); + for (let i = 0; i < items.length; i++) + this.onProtocolMessage( + protocolMessageFromDeserialized(items[i], this.connectionManager.realtime._RealtimePresence) + ); } catch (e) { Logger.logAction( Logger.LOG_ERROR, diff --git a/src/common/lib/transport/connectionmanager.ts b/src/common/lib/transport/connectionmanager.ts index 8e38f79f79..3b4484fde4 100644 --- a/src/common/lib/transport/connectionmanager.ts +++ b/src/common/lib/transport/connectionmanager.ts @@ -1914,7 +1914,11 @@ class ConnectionManager extends EventEmitter { return; } if (Logger.shouldLog(Logger.LOG_MICRO)) { - Logger.logAction(Logger.LOG_MICRO, 'ConnectionManager.send()', 'queueing msg; ' + stringifyProtocolMessage(msg)); + Logger.logAction( + Logger.LOG_MICRO, + 'ConnectionManager.send()', + 'queueing msg; ' + stringifyProtocolMessage(msg, this.realtime._RealtimePresence) + ); } this.queue(msg, callback); } diff --git a/src/common/lib/transport/protocol.ts b/src/common/lib/transport/protocol.ts index 7725cd478a..436fb7d202 100644 --- a/src/common/lib/transport/protocol.ts +++ b/src/common/lib/transport/protocol.ts @@ -74,7 +74,8 @@ class Protocol extends EventEmitter { Logger.logAction( Logger.LOG_MICRO, 'Protocol.send()', - 'sending msg; ' + stringifyProtocolMessage(pendingMessage.message) + 'sending msg; ' + + stringifyProtocolMessage(pendingMessage.message, this.transport.connectionManager.realtime._RealtimePresence) ); } pendingMessage.sendAttempted = true; diff --git a/src/common/lib/transport/transport.ts b/src/common/lib/transport/transport.ts index 3c83644aba..76e9b49f94 100644 --- a/src/common/lib/transport/transport.ts +++ b/src/common/lib/transport/transport.ts @@ -123,7 +123,7 @@ abstract class Transport extends EventEmitter { 'received on ' + this.shortName + ': ' + - stringifyProtocolMessage(message) + + stringifyProtocolMessage(message, this.connectionManager.realtime._RealtimePresence) + '; connectionId = ' + this.connectionManager.connectionId ); diff --git a/src/common/lib/transport/websockettransport.ts b/src/common/lib/transport/websockettransport.ts index 43c53c986b..d56848ec6b 100644 --- a/src/common/lib/transport/websockettransport.ts +++ b/src/common/lib/transport/websockettransport.ts @@ -125,7 +125,14 @@ class WebSocketTransport extends Transport { 'data received; length = ' + data.length + '; type = ' + typeof data ); try { - this.onProtocolMessage(deserializeProtocolMessage(data, this.connectionManager.realtime._MsgPack, this.format)); + this.onProtocolMessage( + deserializeProtocolMessage( + data, + this.connectionManager.realtime._MsgPack, + this.connectionManager.realtime._RealtimePresence, + this.format + ) + ); } catch (e) { Logger.logAction( Logger.LOG_ERROR, diff --git a/src/common/lib/types/defaultprotocolmessage.ts b/src/common/lib/types/defaultprotocolmessage.ts index 4c2c6f92aa..91d210064f 100644 --- a/src/common/lib/types/defaultprotocolmessage.ts +++ b/src/common/lib/types/defaultprotocolmessage.ts @@ -1,10 +1,14 @@ import ProtocolMessage, { fromDeserialized } from './protocolmessage'; +import { + fromValues as presenceMessageFromValues, + fromValuesArray as presenceMessagesFromValuesArray, +} from './presencemessage'; /** `DefaultProtocolMessage` is the class returned by `DefaultRealtime`’s `ProtocolMessage` static property. It exposes the static methods that are used by the tests. */ export class DefaultProtocolMessage extends ProtocolMessage { static fromDeserialized(deserialized: Record): ProtocolMessage { - return fromDeserialized(deserialized); + return fromDeserialized(deserialized, { presenceMessageFromValues, presenceMessagesFromValuesArray }); } } diff --git a/src/common/lib/types/protocolmessage.ts b/src/common/lib/types/protocolmessage.ts index fdcedf6c7d..cb47e4cc33 100644 --- a/src/common/lib/types/protocolmessage.ts +++ b/src/common/lib/types/protocolmessage.ts @@ -1,12 +1,10 @@ import { MsgPack } from 'common/types/msgpack'; import { Types } from '../../../../ably'; +import { PresenceMessageModule } from '../client/modulesmap'; import * as Utils from '../util/utils'; import ErrorInfo from './errorinfo'; import Message, { fromValues as messageFromValues, fromValuesArray as messagesFromValuesArray } from './message'; -import PresenceMessage, { - fromValues as presenceMessageFromValues, - fromValuesArray as presenceMessagesFromValuesArray, -} from './presencemessage'; +import PresenceMessage from './presencemessage'; export const actions = { HEARTBEAT: 0, @@ -65,26 +63,39 @@ export const channelModes = ['PRESENCE', 'PUBLISH', 'SUBSCRIBE', 'PRESENCE_SUBSC export const serialize = Utils.encodeBody; -export function deserialize(serialized: unknown, MsgPack: MsgPack | null, format?: Utils.Format): ProtocolMessage { +export function deserialize( + serialized: unknown, + MsgPack: MsgPack | null, + presenceMessageModule: PresenceMessageModule | null, + format?: Utils.Format +): ProtocolMessage { const deserialized = Utils.decodeBody>(serialized, MsgPack, format); - return fromDeserialized(deserialized); + return fromDeserialized(deserialized, presenceMessageModule); } -export function fromDeserialized(deserialized: Record): ProtocolMessage { +export function fromDeserialized( + deserialized: Record, + presenceMessageModule: PresenceMessageModule | null +): ProtocolMessage { const error = deserialized.error; if (error) deserialized.error = ErrorInfo.fromValues(error as ErrorInfo); const messages = deserialized.messages as Message[]; if (messages) for (let i = 0; i < messages.length; i++) messages[i] = messageFromValues(messages[i]); - const presence = deserialized.presence as PresenceMessage[]; - if (presence) for (let i = 0; i < presence.length; i++) presence[i] = presenceMessageFromValues(presence[i], true); - return Object.assign(new ProtocolMessage(), deserialized); + + const presence = presenceMessageModule ? (deserialized.presence as PresenceMessage[]) : undefined; + if (presenceMessageModule) { + if (presence && presenceMessageModule) + for (let i = 0; i < presence.length; i++) + presence[i] = presenceMessageModule.presenceMessageFromValues(presence[i], true); + } + return Object.assign(new ProtocolMessage(), { ...deserialized, presence }); } export function fromValues(values: unknown): ProtocolMessage { return Object.assign(new ProtocolMessage(), values); } -export function stringify(msg: any): string { +export function stringify(msg: any, presenceMessageModule: PresenceMessageModule | null): string { let result = '[ProtocolMessage'; if (msg.action !== undefined) result += '; action=' + ActionName[msg.action] || msg.action; @@ -96,7 +107,8 @@ export function stringify(msg: any): string { } if (msg.messages) result += '; messages=' + toStringArray(messagesFromValuesArray(msg.messages)); - if (msg.presence) result += '; presence=' + toStringArray(presenceMessagesFromValuesArray(msg.presence)); + if (msg.presence && presenceMessageModule) + result += '; presence=' + toStringArray(presenceMessageModule.presenceMessagesFromValuesArray(msg.presence)); if (msg.error) result += '; error=' + ErrorInfo.fromValues(msg.error).toString(); if (msg.auth && msg.auth.accessToken) result += '; token=' + msg.auth.accessToken; if (msg.flags) result += '; flags=' + flagNames.filter(msg.hasFlag).join(','); @@ -128,6 +140,7 @@ class ProtocolMessage { channelSerial?: string | null; msgSerial?: number; messages?: Message[]; + // This will be undefined if we skipped decoding this property due to user not requesting presence functionality — see `fromDeserialized` presence?: PresenceMessage[]; auth?: unknown; connectionDetails?: Record; diff --git a/src/platform/web/modules/realtimepresence.ts b/src/platform/web/modules/realtimepresence.ts index 425677272c..850bcfa2ad 100644 --- a/src/platform/web/modules/realtimepresence.ts +++ b/src/platform/web/modules/realtimepresence.ts @@ -1 +1,14 @@ -export { default as RealtimePresence } from '../../../common/lib/client/realtimepresence'; +import { RealtimePresenceModule } from 'common/lib/client/modulesmap'; +import { default as realtimePresenceClass } from '../../../common/lib/client/realtimepresence'; +import { + fromValues as presenceMessageFromValues, + fromValuesArray as presenceMessagesFromValuesArray, +} from '../../../common/lib/types/presencemessage'; + +const RealtimePresence: RealtimePresenceModule = { + RealtimePresence: realtimePresenceClass, + presenceMessageFromValues, + presenceMessagesFromValuesArray, +}; + +export { RealtimePresence }; diff --git a/test/browser/modules.test.js b/test/browser/modules.test.js index 9f001a234e..9b5f605c75 100644 --- a/test/browser/modules.test.js +++ b/test/browser/modules.test.js @@ -466,6 +466,30 @@ describe('browser/modules', function () { expect(() => channel.presence).to.throw('RealtimePresence module not provided'); }); + + it('doesn’t break when it receives a PRESENCE ProtocolMessage', async () => { + const rxClient = new BaseRealtime(ablyClientOptions(), { WebSocketTransport, FetchRequest }); + const rxChannel = rxClient.channels.get('channel'); + + await rxChannel.attach(); + + const receivedMessagePromise = new Promise((resolve) => rxChannel.subscribe(resolve)); + + const txClient = new BaseRealtime(ablyClientOptions({ clientId: randomString() }), { + WebSocketTransport, + FetchRequest, + RealtimePublishing, + RealtimePresence, + }); + const txChannel = txClient.channels.get('channel'); + + await txChannel.publish('message', 'body'); + await txChannel.presence.enter(); + + // The idea being here that in order for receivedMessagePromise to resolve, rxClient must have first processed the PRESENCE ProtocolMessage that resulted from txChannel.presence.enter() + + await receivedMessagePromise; + }); }); describe('BaseRealtime with RealtimePresence', () => {