diff --git a/scripts/moduleReport.js b/scripts/moduleReport.js index 577371359f..c346615d36 100644 --- a/scripts/moduleReport.js +++ b/scripts/moduleReport.js @@ -1,7 +1,7 @@ const esbuild = require('esbuild'); // List of all modules accepted in ModulesMap -const moduleNames = ['Rest', 'Crypto', 'MsgPack']; +const moduleNames = ['Rest', 'Crypto', 'MsgPack', 'RealtimePresence']; // List of all free-standing functions exported by the library along with the // ModulesMap entries that we expect them to transitively import diff --git a/src/common/lib/client/baserealtime.ts b/src/common/lib/client/baserealtime.ts index 971ec09038..ff51a2a773 100644 --- a/src/common/lib/client/baserealtime.ts +++ b/src/common/lib/client/baserealtime.ts @@ -10,17 +10,20 @@ import { ChannelOptions } from '../../types/channel'; import ClientOptions from '../../types/ClientOptions'; import * as API from '../../../../ably'; import { ModulesMap } from './modulesmap'; +import RealtimePresence from './realtimepresence'; /** `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; _channels: any; connection: Connection; constructor(options: ClientOptions, modules: ModulesMap) { super(options, modules); Logger.logAction(Logger.LOG_MINOR, 'Realtime()', ''); + this._RealtimePresence = modules.RealtimePresence ?? null; this.connection = new Connection(this, this.options); this._channels = new Channels(this); if (options.autoConnect !== false) this.connect(); diff --git a/src/common/lib/client/channel.ts b/src/common/lib/client/channel.ts index ad4be4b767..ac7efe4d94 100644 --- a/src/common/lib/client/channel.ts +++ b/src/common/lib/client/channel.ts @@ -50,7 +50,10 @@ class Channel extends EventEmitter { client: BaseClient; name: string; basePath: string; - presence: Presence; + private _presence: Presence; + get presence(): Presence { + return this._presence; + } channelOptions: ChannelOptions; constructor(client: BaseClient, name: string, channelOptions?: ChannelOptions) { @@ -59,7 +62,7 @@ class Channel extends EventEmitter { this.client = client; this.name = name; this.basePath = '/channels/' + encodeURIComponent(name); - this.presence = new Presence(this); + this._presence = new Presence(this); this.channelOptions = normaliseChannelOptions(client._Crypto ?? null, channelOptions); } diff --git a/src/common/lib/client/defaultrealtime.ts b/src/common/lib/client/defaultrealtime.ts index 8a905b449d..68073accf0 100644 --- a/src/common/lib/client/defaultrealtime.ts +++ b/src/common/lib/client/defaultrealtime.ts @@ -7,6 +7,7 @@ import ProtocolMessage from '../types/protocolmessage'; import Platform from 'common/platform'; import { DefaultMessage } from '../types/defaultmessage'; import { MsgPack } from 'common/types/msgpack'; +import RealtimePresence from './realtimepresence'; /** `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. @@ -18,7 +19,7 @@ export class DefaultRealtime extends BaseRealtime { throw new Error('Expected DefaultRealtime._MsgPack to have been set'); } - super(options, { ...allCommonModules, Crypto: DefaultRealtime.Crypto ?? undefined, MsgPack }); + super(options, { ...allCommonModules, Crypto: DefaultRealtime.Crypto ?? undefined, MsgPack, RealtimePresence }); } static Utils = Utils; diff --git a/src/common/lib/client/modulesmap.ts b/src/common/lib/client/modulesmap.ts index 12ac9bc7f6..a4de0a0d51 100644 --- a/src/common/lib/client/modulesmap.ts +++ b/src/common/lib/client/modulesmap.ts @@ -1,11 +1,13 @@ import { Rest } from './rest'; import { IUntypedCryptoStatic } from '../../types/ICryptoStatic'; import { MsgPack } from 'common/types/msgpack'; +import RealtimePresence from './realtimepresence'; export interface ModulesMap { Rest?: typeof Rest; Crypto?: IUntypedCryptoStatic; MsgPack?: MsgPack; + RealtimePresence?: typeof RealtimePresence; } export const allCommonModules: ModulesMap = { Rest }; diff --git a/src/common/lib/client/realtimechannel.ts b/src/common/lib/client/realtimechannel.ts index 335d86c83a..3d6d7e4269 100644 --- a/src/common/lib/client/realtimechannel.ts +++ b/src/common/lib/client/realtimechannel.ts @@ -50,7 +50,13 @@ function validateChannelOptions(options?: API.Types.ChannelOptions) { class RealtimeChannel extends Channel { realtime: BaseRealtime; - presence: RealtimePresence; + private _realtimePresence: RealtimePresence | null; + get presence(): RealtimePresence { + if (!this._realtimePresence) { + Utils.throwMissingModuleError('RealtimePresence'); + } + return this._realtimePresence; + } connectionManager: ConnectionManager; state: API.Types.ChannelState; subscriptions: EventEmitter; @@ -84,7 +90,7 @@ class RealtimeChannel extends Channel { super(realtime, name, options); Logger.logAction(Logger.LOG_MINOR, 'RealtimeChannel()', 'started; name = ' + name); this.realtime = realtime; - this.presence = new RealtimePresence(this); + this._realtimePresence = realtime._RealtimePresence ? new realtime._RealtimePresence(this) : null; this.connectionManager = realtime.connection.connectionManager; this.state = 'initialized'; this.subscriptions = new EventEmitter(); @@ -616,7 +622,9 @@ class RealtimeChannel extends Channel { if (this.state === 'attached') { if (!resumed) { /* On a loss of continuity, the presence set needs to be re-synced */ - this.presence.onAttached(hasPresence); + if (this._realtimePresence) { + this._realtimePresence.onAttached(hasPresence); + } } const change = new ChannelStateChange(this.state, this.state, resumed, hasBacklog, message.error); this._allChannelChanges.emit('update', change); @@ -674,7 +682,9 @@ class RealtimeChannel extends Channel { Logger.logAction(Logger.LOG_ERROR, 'RealtimeChannel.processMessage()', (e as Error).toString()); } } - this.presence.setPresence(presence, isSync, syncChannelSerial as any); + if (this._realtimePresence) { + this._realtimePresence.setPresence(presence, isSync, syncChannelSerial as any); + } break; } case actions.MESSAGE: { @@ -810,7 +820,9 @@ class RealtimeChannel extends Channel { if (state === this.state) { return; } - this.presence.actOnChannelState(state, hasPresence, reason); + if (this._realtimePresence) { + this._realtimePresence.actOnChannelState(state, hasPresence, reason); + } if (state === 'suspended' && this.connectionManager.state.sendEvents) { this.startRetryTimer(); } else { diff --git a/src/platform/web/modules.ts b/src/platform/web/modules.ts index 516e220c84..5370247778 100644 --- a/src/platform/web/modules.ts +++ b/src/platform/web/modules.ts @@ -42,5 +42,6 @@ if (Platform.Config.noUpgrade) { export * from './modules/crypto'; export * from './modules/message'; export * from './modules/msgpack'; +export * from './modules/realtimepresence'; export { Rest } from '../../common/lib/client/rest'; export { BaseRest, BaseRealtime, ErrorInfo }; diff --git a/src/platform/web/modules/presencemessage.ts b/src/platform/web/modules/presencemessage.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/platform/web/modules/realtimepresence.ts b/src/platform/web/modules/realtimepresence.ts new file mode 100644 index 0000000000..425677272c --- /dev/null +++ b/src/platform/web/modules/realtimepresence.ts @@ -0,0 +1 @@ +export { default as RealtimePresence } from '../../../common/lib/client/realtimepresence'; diff --git a/test/browser/modules.test.js b/test/browser/modules.test.js index 6536f25f6e..e3d483b684 100644 --- a/test/browser/modules.test.js +++ b/test/browser/modules.test.js @@ -10,6 +10,7 @@ import { decodeEncryptedMessages, Crypto, MsgPack, + RealtimePresence, } from '../../build/modules/index.js'; describe('browser/modules', function () { @@ -20,11 +21,13 @@ describe('browser/modules', function () { let testResourcesPath; let loadTestData; let testMessageEquality; + let randomString; before((done) => { ablyClientOptions = window.ablyHelpers.ablyClientOptions; testResourcesPath = window.ablyHelpers.testResourcesPath; testMessageEquality = window.ablyHelpers.testMessageEquality; + randomString = window.ablyHelpers.randomString; loadTestData = async (dataPath) => { return new Promise((resolve, reject) => { @@ -317,4 +320,35 @@ describe('browser/modules', function () { }); }); }); + + describe('RealtimePresence', () => { + describe('BaseRealtime without RealtimePresence', () => { + it('throws an error when attempting to access the `presence` property', () => { + const client = new BaseRealtime(ablyClientOptions(), {}); + const channel = client.channels.get('channel'); + + expect(() => channel.presence).to.throw('RealtimePresence module not provided'); + }); + }); + + describe('BaseRealtime with RealtimePresence', () => { + it('offers realtime presence functionality', async () => { + const rxChannel = new BaseRealtime(ablyClientOptions(), { RealtimePresence }).channels.get('channel'); + const txClientId = randomString(); + const txChannel = new BaseRealtime(ablyClientOptions({ clientId: txClientId }), { + RealtimePresence, + }).channels.get('channel'); + + let resolveRxPresenceMessagePromise; + const rxPresenceMessagePromise = new Promise((resolve, reject) => { + resolveRxPresenceMessagePromise = resolve; + }); + await rxChannel.presence.subscribe('enter', resolveRxPresenceMessagePromise); + await txChannel.presence.enter(); + + const rxPresenceMessage = await rxPresenceMessagePromise; + expect(rxPresenceMessage.clientId).to.equal(txClientId); + }); + }); + }); }); diff --git a/test/common/modules/shared_helper.js b/test/common/modules/shared_helper.js index 4d8581ab22..c28bdd9b45 100644 --- a/test/common/modules/shared_helper.js +++ b/test/common/modules/shared_helper.js @@ -249,6 +249,10 @@ define([ expect(json1 === json2, 'JSON data contents mismatch.').to.be.ok; } + function randomString() { + return Math.random().toString().slice(2); + } + var exports = { setupApp: testAppModule.setup, tearDownApp: testAppModule.tearDown, @@ -283,6 +287,7 @@ define([ whenPromiseSettles: whenPromiseSettles, randomString: randomString, testMessageEquality: testMessageEquality, + randomString: randomString, }; if (typeof window !== 'undefined') {