diff --git a/scripts/moduleReport.js b/scripts/moduleReport.js index c346615d36..4aa41cd949 100644 --- a/scripts/moduleReport.js +++ b/scripts/moduleReport.js @@ -12,6 +12,8 @@ const functions = [ { name: 'decodeEncryptedMessage', transitiveImports: ['Crypto'] }, { name: 'decodeMessages', transitiveImports: [] }, { name: 'decodeEncryptedMessages', transitiveImports: ['Crypto'] }, + { name: 'decodePresenceMessage', transitiveImports: [] }, + { name: 'decodePresenceMessages', transitiveImports: [] }, ]; function formatBytes(bytes) { diff --git a/src/common/lib/client/baseclient.ts b/src/common/lib/client/baseclient.ts index 2c7dc089a0..939257edb7 100644 --- a/src/common/lib/client/baseclient.ts +++ b/src/common/lib/client/baseclient.ts @@ -10,7 +10,6 @@ import ClientOptions, { NormalisedClientOptions } from '../../types/ClientOption import * as API from '../../../../ably'; import Platform from '../../platform'; -import PresenceMessage from '../types/presencemessage'; import { ModulesMap } from './modulesmap'; import { Rest } from './rest'; import { IUntypedCryptoStatic } from 'common/types/ICryptoStatic'; @@ -153,7 +152,6 @@ class BaseClient { } static Platform = Platform; - static PresenceMessage = PresenceMessage; } export default BaseClient; diff --git a/src/common/lib/client/defaultrealtime.ts b/src/common/lib/client/defaultrealtime.ts index 68073accf0..242e612288 100644 --- a/src/common/lib/client/defaultrealtime.ts +++ b/src/common/lib/client/defaultrealtime.ts @@ -8,6 +8,7 @@ import Platform from 'common/platform'; import { DefaultMessage } from '../types/defaultmessage'; import { MsgPack } from 'common/types/msgpack'; import RealtimePresence from './realtimepresence'; +import { DefaultPresenceMessage } from '../types/defaultpresencemessage'; /** `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. @@ -39,6 +40,7 @@ export class DefaultRealtime extends BaseRealtime { } static Message = DefaultMessage; + static PresenceMessage = DefaultPresenceMessage; static _MsgPack: MsgPack | null = null; } diff --git a/src/common/lib/client/defaultrest.ts b/src/common/lib/client/defaultrest.ts index e70a9eecf6..c03849af37 100644 --- a/src/common/lib/client/defaultrest.ts +++ b/src/common/lib/client/defaultrest.ts @@ -4,6 +4,7 @@ import { allCommonModules } from './modulesmap'; import Platform from 'common/platform'; import { DefaultMessage } from '../types/defaultmessage'; import { MsgPack } from 'common/types/msgpack'; +import { DefaultPresenceMessage } from '../types/defaultpresencemessage'; /** `DefaultRest` is the class that the non tree-shakable version of the SDK exports as `Rest`. It ensures that this version of the SDK includes all of the functionality which is optionally available in the tree-shakable version. @@ -35,6 +36,7 @@ export class DefaultRest extends BaseRest { } static Message = DefaultMessage; + static PresenceMessage = DefaultPresenceMessage; static _MsgPack: MsgPack | null = null; } diff --git a/src/common/lib/client/realtimechannel.ts b/src/common/lib/client/realtimechannel.ts index 3d6d7e4269..ee403f7bc5 100644 --- a/src/common/lib/client/realtimechannel.ts +++ b/src/common/lib/client/realtimechannel.ts @@ -7,7 +7,7 @@ import RealtimePresence from './realtimepresence'; import Message, { CipherOptions } from '../types/message'; import ChannelStateChange from './channelstatechange'; import ErrorInfo, { IPartialErrorInfo, PartialErrorInfo } from '../types/errorinfo'; -import PresenceMessage from '../types/presencemessage'; +import PresenceMessage, { fromValues as presenceMessageFromValues } from '../types/presencemessage'; import ConnectionErrors from '../transport/connectionerrors'; import * as API from '../../../../ably'; import ConnectionManager from '../transport/connectionmanager'; @@ -591,7 +591,7 @@ class RealtimeChannel extends Channel { channel: this.name, presence: Utils.isArray(presence) ? PresenceMessage.fromValuesArray(presence) - : [PresenceMessage.fromValues(presence)], + : [presenceMessageFromValues(presence)], }); this.sendMessage(msg, callback); } diff --git a/src/common/lib/client/realtimepresence.ts b/src/common/lib/client/realtimepresence.ts index f76c65021a..5d5e84c346 100644 --- a/src/common/lib/client/realtimepresence.ts +++ b/src/common/lib/client/realtimepresence.ts @@ -2,7 +2,7 @@ import * as Utils from '../util/utils'; import Presence from './presence'; import EventEmitter from '../util/eventemitter'; import Logger from '../util/logger'; -import PresenceMessage from '../types/presencemessage'; +import PresenceMessage, { fromValues as presenceMessageFromValues } from '../types/presencemessage'; import ErrorInfo, { IPartialErrorInfo, PartialErrorInfo } from '../types/errorinfo'; import RealtimeChannel from './realtimechannel'; import Multicaster from '../util/multicaster'; @@ -342,7 +342,7 @@ class RealtimePresence extends Presence { } for (let i = 0; i < presenceSet.length; i++) { - const presence = PresenceMessage.fromValues(presenceSet[i]); + const presence = presenceMessageFromValues(presenceSet[i]); switch (presence.action) { case 'leave': if (members.remove(presence)) { @@ -480,7 +480,7 @@ class RealtimePresence extends Presence { _synthesizeLeaves(items: PresenceMessage[]): void { const subscriptions = this.subscriptions; Utils.arrForEach(items, function (item) { - const presence = PresenceMessage.fromValues({ + const presence = presenceMessageFromValues({ action: 'leave', connectionId: item.connectionId, clientId: item.clientId, @@ -568,7 +568,7 @@ class PresenceMap extends EventEmitter { put(item: PresenceMessage) { if (item.action === 'enter' || item.action === 'update') { - item = PresenceMessage.fromValues(item); + item = presenceMessageFromValues(item); item.action = 'present'; } const map = this.map, @@ -606,7 +606,7 @@ class PresenceMap extends EventEmitter { /* RTP2f */ if (this.syncInProgress) { - item = PresenceMessage.fromValues(item); + item = presenceMessageFromValues(item); item.action = 'absent'; map[key] = item; } else { diff --git a/src/common/lib/types/defaultpresencemessage.ts b/src/common/lib/types/defaultpresencemessage.ts new file mode 100644 index 0000000000..5f1ceb6575 --- /dev/null +++ b/src/common/lib/types/defaultpresencemessage.ts @@ -0,0 +1,22 @@ +import * as API from '../../../../ably'; +import PresenceMessage, { fromEncoded, fromEncodedArray, fromValues } from './presencemessage'; + +/** + `DefaultPresenceMessage` is the class returned by `DefaultRest` and `DefaultRealtime`’s `PresenceMessage` static property. It introduces the static methods described in the `PresenceMessageStatic` interface of the public API of the non tree-shakable version of the library. + */ +export class DefaultPresenceMessage extends PresenceMessage { + static async fromEncoded(encoded: unknown, inputOptions?: API.Types.ChannelOptions): Promise { + return fromEncoded(encoded, inputOptions); + } + + static async fromEncodedArray( + encodedArray: Array, + options?: API.Types.ChannelOptions + ): Promise { + return fromEncodedArray(encodedArray, options); + } + + static fromValues(values: PresenceMessage | Record, stringifyAction?: boolean): PresenceMessage { + return fromValues(values, stringifyAction); + } +} diff --git a/src/common/lib/types/presencemessage.ts b/src/common/lib/types/presencemessage.ts index da6f3cc5c5..c1fa4db450 100644 --- a/src/common/lib/types/presencemessage.ts +++ b/src/common/lib/types/presencemessage.ts @@ -9,6 +9,39 @@ function toActionValue(actionString: string) { return PresenceMessage.Actions.indexOf(actionString); } +export async function fromEncoded(encoded: unknown, options?: API.Types.ChannelOptions): Promise { + const msg = fromValues(encoded as PresenceMessage | Record, true); + /* if decoding fails at any point, catch and return the message decoded to + * the fullest extent possible */ + try { + await PresenceMessage.decode(msg, options ?? {}); + } catch (e) { + Logger.logAction(Logger.LOG_ERROR, 'PresenceMessage.fromEncoded()', (e as Error).toString()); + } + return msg; +} + +export async function fromEncodedArray( + encodedArray: unknown[], + options?: API.Types.ChannelOptions +): Promise { + return Promise.all( + encodedArray.map(function (encoded) { + return fromEncoded(encoded, options); + }) + ); +} + +export function fromValues( + values: PresenceMessage | Record, + stringifyAction?: boolean +): PresenceMessage { + if (stringifyAction) { + values.action = PresenceMessage.Actions[values.action as number]; + } + return Object.assign(new PresenceMessage(), values); +} + class PresenceMessage { action?: string | number; id?: string; @@ -121,7 +154,7 @@ class PresenceMessage { } for (let i = 0; i < body.length; i++) { - const msg = (messages[i] = PresenceMessage.fromValues(body[i], true)); + const msg = (messages[i] = fromValues(body[i], true)); try { await PresenceMessage.decode(msg, options); } catch (e) { @@ -131,48 +164,18 @@ class PresenceMessage { return messages; } - static fromValues(values: PresenceMessage | Record, stringifyAction?: boolean): PresenceMessage { - if (stringifyAction) { - values.action = PresenceMessage.Actions[values.action as number]; - } - return Object.assign(new PresenceMessage(), values); - } - static fromValuesArray(values: unknown[]): PresenceMessage[] { const count = values.length, result = new Array(count); - for (let i = 0; i < count; i++) result[i] = PresenceMessage.fromValues(values[i] as Record); + for (let i = 0; i < count; i++) result[i] = fromValues(values[i] as Record); return result; } - static async fromEncoded(encoded: unknown, options?: API.Types.ChannelOptions): Promise { - const msg = PresenceMessage.fromValues(encoded as PresenceMessage | Record, true); - /* if decoding fails at any point, catch and return the message decoded to - * the fullest extent possible */ - try { - await PresenceMessage.decode(msg, options ?? {}); - } catch (e) { - Logger.logAction(Logger.LOG_ERROR, 'PresenceMessage.fromEncoded()', (e as Error).toString()); - } - return msg; - } - - static async fromEncodedArray( - encodedArray: unknown[], - options?: API.Types.ChannelOptions - ): Promise { - return Promise.all( - encodedArray.map(function (encoded) { - return PresenceMessage.fromEncoded(encoded, options); - }) - ); - } - static fromData(data: unknown): PresenceMessage { if (data instanceof PresenceMessage) { return data; } - return PresenceMessage.fromValues({ + return fromValues({ data, }); } diff --git a/src/common/lib/types/protocolmessage.ts b/src/common/lib/types/protocolmessage.ts index 46641b5c1c..fde1978f9a 100644 --- a/src/common/lib/types/protocolmessage.ts +++ b/src/common/lib/types/protocolmessage.ts @@ -3,7 +3,7 @@ import { Types } from '../../../../ably'; import * as Utils from '../util/utils'; import ErrorInfo from './errorinfo'; import Message from './message'; -import PresenceMessage from './presencemessage'; +import PresenceMessage, { fromValues as presenceMessageFromValues } from './presencemessage'; const actions = { HEARTBEAT: 0, @@ -121,7 +121,7 @@ class ProtocolMessage { const messages = deserialized.messages as Message[]; if (messages) for (let i = 0; i < messages.length; i++) messages[i] = Message.fromValues(messages[i]); const presence = deserialized.presence as PresenceMessage[]; - if (presence) for (let i = 0; i < presence.length; i++) presence[i] = PresenceMessage.fromValues(presence[i], true); + if (presence) for (let i = 0; i < presence.length; i++) presence[i] = presenceMessageFromValues(presence[i], true); return Object.assign(new ProtocolMessage(), deserialized); }; diff --git a/src/platform/web/modules.ts b/src/platform/web/modules.ts index 5370247778..0488f6bdf3 100644 --- a/src/platform/web/modules.ts +++ b/src/platform/web/modules.ts @@ -41,6 +41,7 @@ if (Platform.Config.noUpgrade) { export * from './modules/crypto'; export * from './modules/message'; +export * from './modules/presencemessage'; export * from './modules/msgpack'; export * from './modules/realtimepresence'; export { Rest } from '../../common/lib/client/rest'; diff --git a/src/platform/web/modules/presencemessage.ts b/src/platform/web/modules/presencemessage.ts index e69de29bb2..d90d2b42b5 100644 --- a/src/platform/web/modules/presencemessage.ts +++ b/src/platform/web/modules/presencemessage.ts @@ -0,0 +1,8 @@ +import * as API from '../../../../ably'; +import { fromEncoded, fromEncodedArray, fromValues } from '../../../common/lib/types/presencemessage'; + +// The type assertions for the functions below are due to https://github.com/ably/ably-js/issues/1421 + +export const decodePresenceMessage = fromEncoded as API.Types.PresenceMessageStatic['fromEncoded']; +export const decodePresenceMessages = fromEncodedArray as API.Types.PresenceMessageStatic['fromEncodedArray']; +export const constructPresenceMessage = fromValues as API.Types.PresenceMessageStatic['fromValues']; diff --git a/test/browser/modules.test.js b/test/browser/modules.test.js index e3d483b684..dc640e170b 100644 --- a/test/browser/modules.test.js +++ b/test/browser/modules.test.js @@ -11,6 +11,9 @@ import { Crypto, MsgPack, RealtimePresence, + decodePresenceMessage, + decodePresenceMessages, + constructPresenceMessage, } from '../../build/modules/index.js'; describe('browser/modules', function () { @@ -351,4 +354,47 @@ describe('browser/modules', function () { }); }); }); + + describe('PresenceMessage standalone functions', () => { + describe('decodePresenceMessage', () => { + it('decodes a presence message’s data', async () => { + const buffer = BufferUtils.utf8Encode('foo'); + const encodedMessage = { data: BufferUtils.base64Encode(buffer), encoding: 'base64' }; + + const decodedMessage = await decodePresenceMessage(encodedMessage); + + expect(BufferUtils.areBuffersEqual(decodedMessage.data, buffer)).to.be.true; + expect(decodedMessage.encoding).to.be.null; + }); + }); + + describe('decodeMessages', () => { + it('decodes presence messages’ data', async () => { + const buffers = ['foo', 'bar'].map((data) => BufferUtils.utf8Encode(data)); + const encodedMessages = buffers.map((buffer) => ({ + data: BufferUtils.base64Encode(buffer), + encoding: 'base64', + })); + + const decodedMessages = await decodePresenceMessages(encodedMessages); + + for (let i = 0; i < decodedMessages.length; i++) { + const decodedMessage = decodedMessages[i]; + + expect(BufferUtils.areBuffersEqual(decodedMessage.data, buffers[i])).to.be.true; + expect(decodedMessage.encoding).to.be.null; + } + }); + }); + + describe('constructPresenceMessage', () => { + it.only('creates a PresenceMessage instance', async () => { + const extras = { foo: 'bar' }; + const presenceMessage = constructPresenceMessage({ extras }); + + expect(presenceMessage.constructor.name).to.contain('PresenceMessage'); + expect(presenceMessage.extras).to.equal(extras); + }); + }); + }); });