From a9d85a54243e96af118ddcb0883bd547b917c949 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 9 Nov 2023 13:33:54 -0300 Subject: [PATCH] Make Realtime Channel and Presence not inherit from REST MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There is code that’s exclusively needed by the REST version of these classes (e.g. presence `get()`, channel `publish()`), and which, in order to reduce bundle size, we don’t want to pull in when importing the BaseRealtime class. In order to do this, we need to sever the inheritance relation between the Realtime and REST version of these classes. (It’s also worth noting that, similarly to what I mentioned in 69c35f1, the IDL doesn’t mention any inheritance relation.) The REST versions of these classes also contain functionality (channel history and presence history, channel status) that should only be available to a BaseRealtime client if the user has explicitly requested REST functionality (by importing the Rest module). So, we gate this functionality behind the Rest module (I should really have done this in 89c0761) and in doing so further reduce the bundle size of a REST-less BaseRealtime. I’ve moved the channel and presence REST code that’s also conditionally needed by Realtime into classes called RestChannelMixin and RestPresenceMixin. There’s no massively compelling reason for these classes to exist, I just thought it might be good not to dump everything directly inside the Rest module. Resolves #1489. --- src/common/lib/client/realtimechannel.ts | 65 +++++++++------- src/common/lib/client/realtimepresence.ts | 21 +++-- src/common/lib/client/rest.ts | 5 ++ src/common/lib/client/restchannel.ts | 91 +++++----------------- src/common/lib/client/restchannelmixin.ts | 67 ++++++++++++++++ src/common/lib/client/restpresence.ts | 71 +++++------------ src/common/lib/client/restpresencemixin.ts | 51 ++++++++++++ src/common/lib/util/defaults.ts | 18 +++++ test/browser/modules.test.js | 27 ++++++- 9 files changed, 256 insertions(+), 160 deletions(-) create mode 100644 src/common/lib/client/restchannelmixin.ts create mode 100644 src/common/lib/client/restpresencemixin.ts diff --git a/src/common/lib/client/realtimechannel.ts b/src/common/lib/client/realtimechannel.ts index 3200088bb6..e3d054ca15 100644 --- a/src/common/lib/client/realtimechannel.ts +++ b/src/common/lib/client/realtimechannel.ts @@ -1,7 +1,6 @@ import ProtocolMessage from '../types/protocolmessage'; import EventEmitter from '../util/eventemitter'; import * as Utils from '../util/utils'; -import RestChannel from './restchannel'; import Logger from '../util/logger'; import RealtimePresence from './realtimepresence'; import Message, { CipherOptions } from '../types/message'; @@ -14,6 +13,8 @@ import ConnectionManager from '../transport/connectionmanager'; import ConnectionStateChange from './connectionstatechange'; import { ErrCallback, PaginatedResultCallback, StandardCallback } from '../../types/utils'; import BaseRealtime from './baserealtime'; +import { ChannelOptions } from '../../types/channel'; +import { normaliseChannelOptions } from '../util/defaults'; interface RealtimeHistoryParams { start?: number; @@ -48,14 +49,16 @@ function validateChannelOptions(options?: API.Types.ChannelOptions) { } } -class RealtimeChannel extends RestChannel { - realtime: BaseRealtime; - private _realtimePresence: RealtimePresence | null; +class RealtimeChannel extends EventEmitter { + name: string; + channelOptions: ChannelOptions; + client: BaseRealtime; + private _presence: RealtimePresence | null; get presence(): RealtimePresence { - if (!this._realtimePresence) { + if (!this._presence) { Utils.throwMissingModuleError('RealtimePresence'); } - return this._realtimePresence; + return this._presence; } connectionManager: ConnectionManager; state: API.Types.ChannelState; @@ -86,12 +89,14 @@ class RealtimeChannel extends RestChannel { retryTimer?: number | NodeJS.Timeout | null; retryCount: number = 0; - constructor(realtime: BaseRealtime, name: string, options?: API.Types.ChannelOptions) { - super(realtime, name, options); + constructor(client: BaseRealtime, name: string, options?: API.Types.ChannelOptions) { + super(); Logger.logAction(Logger.LOG_MINOR, 'RealtimeChannel()', 'started; name = ' + name); - this.realtime = realtime; - this._realtimePresence = realtime._RealtimePresence ? new realtime._RealtimePresence(this) : null; - this.connectionManager = realtime.connection.connectionManager; + this.name = name; + this.channelOptions = normaliseChannelOptions(client._Crypto ?? null, options); + this.client = client; + this._presence = client._RealtimePresence ? new client._RealtimePresence(this) : null; + this.connectionManager = client.connection.connectionManager; this.state = 'initialized'; this.subscriptions = new EventEmitter(); this.syncChannelSerial = undefined; @@ -106,7 +111,7 @@ class RealtimeChannel extends RestChannel { this._attachResume = false; this._decodingContext = { channelOptions: this.channelOptions, - plugins: realtime.options.plugins || {}, + plugins: client.options.plugins || {}, baseEncodedPreviousPayload: undefined, }; this._lastPayload = { @@ -156,7 +161,7 @@ class RealtimeChannel extends RestChannel { _callback(err); return; } - RestChannel.prototype.setOptions.call(this, options); + this.channelOptions = normaliseChannelOptions(this.client._Crypto ?? null, options); if (this._decodingContext) this._decodingContext.channelOptions = this.channelOptions; if (this._shouldReattachToSetOptions(options)) { /* This does not just do _attach(true, null, callback) because that would put us @@ -236,7 +241,7 @@ class RealtimeChannel extends RestChannel { } else { messages = [Message.fromValues({ name: args[0], data: args[1] })]; } - const maxMessageSize = this.realtime.options.maxMessageSize; + const maxMessageSize = this.client.options.maxMessageSize; Message.encodeArray(messages, this.channelOptions as CipherOptions, (err: Error | null) => { if (err) { callback(err); @@ -258,12 +263,11 @@ class RealtimeChannel extends RestChannel { ); return; } - this.__publish(messages, callback); + this._publish(messages, callback); }); } - // Double underscore used to prevent type conflict with underlying Channel._publish method - __publish(messages: Array, callback: ErrCallback) { + _publish(messages: Array, callback: ErrCallback) { Logger.logAction(Logger.LOG_MICRO, 'RealtimeChannel.publish()', 'message count = ' + messages.length); const state = this.state; switch (state) { @@ -483,7 +487,7 @@ class RealtimeChannel extends RestChannel { } sendMessage(msg: ProtocolMessage, callback?: ErrCallback): void { - this.connectionManager.send(msg, this.realtime.options.queueMessages, callback); + this.connectionManager.send(msg, this.client.options.queueMessages, callback); } sendPresence(presence: PresenceMessage | PresenceMessage[], callback?: ErrCallback): void { @@ -523,8 +527,8 @@ class RealtimeChannel extends RestChannel { if (this.state === 'attached') { if (!resumed) { /* On a loss of continuity, the presence set needs to be re-synced */ - if (this._realtimePresence) { - this._realtimePresence.onAttached(hasPresence); + if (this._presence) { + this._presence.onAttached(hasPresence); } } const change = new ChannelStateChange(this.state, this.state, resumed, hasBacklog, message.error); @@ -583,8 +587,8 @@ class RealtimeChannel extends RestChannel { Logger.logAction(Logger.LOG_ERROR, 'RealtimeChannel.processMessage()', (e as Error).toString()); } } - if (this._realtimePresence) { - this._realtimePresence.setPresence(presence, isSync, syncChannelSerial as any); + if (this._presence) { + this._presence.setPresence(presence, isSync, syncChannelSerial as any); } break; } @@ -721,8 +725,8 @@ class RealtimeChannel extends RestChannel { if (state === this.state) { return; } - if (this._realtimePresence) { - this._realtimePresence.actOnChannelState(state, hasPresence, reason); + if (this._presence) { + this._presence.actOnChannelState(state, hasPresence, reason); } if (state === 'suspended' && this.connectionManager.state.sendEvents) { this.startRetryTimer(); @@ -829,7 +833,7 @@ class RealtimeChannel extends RestChannel { Logger.logAction(Logger.LOG_MINOR, 'RealtimeChannel.startStateTimerIfNotRunning', 'timer expired'); this.stateTimer = null; this.timeoutPendingState(); - }, this.realtime.options.timeouts.realtimeRequestTimeout); + }, this.client.options.timeouts.realtimeRequestTimeout); } } @@ -845,7 +849,7 @@ class RealtimeChannel extends RestChannel { if (this.retryTimer) return; this.retryCount++; - const retryDelay = Utils.getRetryTime(this.realtime.options.timeouts.channelRetryTimeout, this.retryCount); + const retryDelay = Utils.getRetryTime(this.client.options.timeouts.channelRetryTimeout, this.retryCount); this.retryTimer = setTimeout(() => { /* If connection is not connected, just leave in suspended, a reattach @@ -881,6 +885,9 @@ class RealtimeChannel extends RestChannel { } } + // We fetch this first so that any module-not-provided error takes priority over other errors + const restMixin = this.client.rest.channelMixin; + if (params && params.untilAttach) { if (this.state !== 'attached') { callback(new ErrorInfo('option untilAttach requires the channel to be attached', 40000, 400)); @@ -900,7 +907,7 @@ class RealtimeChannel extends RestChannel { params.from_serial = this.properties.attachSerial; } - RestChannel.prototype._history.call(this, params, callback); + return restMixin.history(this, params, callback); } as any; whenState = ((state: string, listener: ErrCallback) => { @@ -934,6 +941,10 @@ class RealtimeChannel extends RestChannel { this.properties.channelSerial = channelSerial; } } + + status(callback?: StandardCallback): void | Promise { + return this.client.rest.channelMixin.status(this, callback); + } } export default RealtimeChannel; diff --git a/src/common/lib/client/realtimepresence.ts b/src/common/lib/client/realtimepresence.ts index 7fc5a63c47..0027e22e20 100644 --- a/src/common/lib/client/realtimepresence.ts +++ b/src/common/lib/client/realtimepresence.ts @@ -1,5 +1,4 @@ import * as Utils from '../util/utils'; -import RestPresence from './restpresence'; import EventEmitter from '../util/eventemitter'; import Logger from '../util/logger'; import PresenceMessage, { fromValues as presenceMessageFromValues } from '../types/presencemessage'; @@ -27,11 +26,11 @@ interface RealtimeHistoryParams { } function getClientId(realtimePresence: RealtimePresence) { - return realtimePresence.channel.realtime.auth.clientId; + return realtimePresence.channel.client.auth.clientId; } function isAnonymousOrWildcard(realtimePresence: RealtimePresence) { - const realtime = realtimePresence.channel.realtime; + const realtime = realtimePresence.channel.client; /* If not currently connected, we can't assume that we're an anonymous * client, as realtime may inform us of our clientId in the CONNECTED * message. So assume we're not anonymous and leave it to realtime to @@ -78,7 +77,7 @@ function newerThan(item: PresenceMessage, existing: PresenceMessage) { } } -class RealtimePresence extends RestPresence { +class RealtimePresence extends EventEmitter { channel: RealtimeChannel; pendingPresence: { presence: PresenceMessage; callback: ErrCallback }[]; syncComplete: boolean; @@ -88,7 +87,7 @@ class RealtimePresence extends RestPresence { name?: string; constructor(channel: RealtimeChannel) { - super(channel); + super(); this.channel = channel; this.syncComplete = false; this.members = new PresenceMap(this, (item) => item.clientId + ':' + item.connectionId); @@ -244,8 +243,11 @@ class RealtimePresence extends RestPresence { } } - // Return type is any to avoid conflict with base Presence class - get(this: RealtimePresence, params: RealtimePresenceParams, callback: StandardCallback): any { + get( + this: RealtimePresence, + params: RealtimePresenceParams, + callback: StandardCallback + ): void | Promise { const args = Array.prototype.slice.call(arguments); if (args.length == 1 && typeof args[0] == 'function') args.unshift(null); @@ -304,6 +306,9 @@ class RealtimePresence extends RestPresence { } } + // We fetch this first so that any module-not-provided error takes priority over other errors + const restMixin = this.channel.client.rest.presenceMixin; + if (params && params.untilAttach) { if (this.channel.state === 'attached') { delete params.untilAttach; @@ -319,7 +324,7 @@ class RealtimePresence extends RestPresence { } } - RestPresence.prototype._history.call(this, params, callback); + return restMixin.history(this, params, callback); } setPresence(presenceSet: PresenceMessage[], isSync: boolean, syncChannelSerial?: string): void { diff --git a/src/common/lib/client/rest.ts b/src/common/lib/client/rest.ts index f1fb22badd..c9a26f0061 100644 --- a/src/common/lib/client/rest.ts +++ b/src/common/lib/client/rest.ts @@ -16,6 +16,8 @@ import Resource from './resource'; import Platform from '../../platform'; import BaseClient from './baseclient'; import { useTokenAuth } from './auth'; +import { RestChannelMixin } from './restchannelmixin'; +import { RestPresenceMixin } from './restpresencemixin'; type BatchResult = API.Types.BatchResult; @@ -39,6 +41,9 @@ export class Rest { readonly channels: Channels; readonly push: Push; + readonly channelMixin = RestChannelMixin; + readonly presenceMixin = RestPresenceMixin; + constructor(client: BaseClient) { this.client = client; this.channels = new Channels(this.client); diff --git a/src/common/lib/client/restchannel.ts b/src/common/lib/client/restchannel.ts index b88fa807b9..6197c65191 100644 --- a/src/common/lib/client/restchannel.ts +++ b/src/common/lib/client/restchannel.ts @@ -1,26 +1,16 @@ import * as Utils from '../util/utils'; -import EventEmitter from '../util/eventemitter'; import Logger from '../util/logger'; import RestPresence from './restpresence'; import Message, { CipherOptions } from '../types/message'; import ErrorInfo from '../types/errorinfo'; -import PaginatedResource, { PaginatedResult } from './paginatedresource'; +import { PaginatedResult } from './paginatedresource'; import Resource, { ResourceCallback } from './resource'; import { ChannelOptions } from '../../types/channel'; import { PaginatedResultCallback, StandardCallback } from '../../types/utils'; -import BaseClient from './baseclient'; +import BaseRest from './baseclient'; import * as API from '../../../../ably'; -import Defaults from '../util/defaults'; -import { IUntypedCryptoStatic } from 'common/types/ICryptoStatic'; - -interface RestHistoryParams { - start?: number; - end?: number; - direction?: string; - limit?: number; -} - -function noop() {} +import Defaults, { normaliseChannelOptions } from '../util/defaults'; +import { RestHistoryParams } from './restchannelmixin'; const MSG_ID_ENTROPY_BYTES = 9; @@ -30,39 +20,17 @@ function allEmptyIds(messages: Array) { }); } -function normaliseChannelOptions(Crypto: IUntypedCryptoStatic | null, options?: ChannelOptions) { - const channelOptions = options || {}; - if (channelOptions.cipher) { - if (!Crypto) Utils.throwMissingModuleError('Crypto'); - const cipher = Crypto.getCipher(channelOptions.cipher); - channelOptions.cipher = cipher.cipherParams; - channelOptions.channelCipher = cipher.cipher; - } else if ('cipher' in channelOptions) { - /* Don't deactivate an existing cipher unless options - * has a 'cipher' key that's falsey */ - channelOptions.cipher = undefined; - channelOptions.channelCipher = null; - } - return channelOptions; -} - -class RestChannel extends EventEmitter { - client: BaseClient; +class RestChannel { + client: BaseRest; name: string; - basePath: string; - private _presence: RestPresence; - get presence(): RestPresence { - return this._presence; - } + presence: RestPresence; channelOptions: ChannelOptions; - constructor(client: BaseClient, name: string, channelOptions?: ChannelOptions) { - super(); + constructor(client: BaseRest, name: string, channelOptions?: ChannelOptions) { Logger.logAction(Logger.LOG_MINOR, 'RestChannel()', 'started; name = ' + name); - this.client = client; this.name = name; - this.basePath = '/channels/' + encodeURIComponent(name); - this._presence = new RestPresence(this); + this.client = client; + this.presence = new RestPresence(this); this.channelOptions = normaliseChannelOptions(client._Crypto ?? null, channelOptions); } @@ -85,25 +53,7 @@ class RestChannel extends EventEmitter { } } - this._history(params, callback); - } - - _history(params: RestHistoryParams | null, callback: PaginatedResultCallback): void { - const client = this.client, - format = client.options.useBinaryProtocol ? Utils.Format.msgpack : Utils.Format.json, - envelope = this.client.http.supportsLinkHeaders ? undefined : format, - headers = Defaults.defaultGetHeaders(client.options, { format }); - - Utils.mixin(headers, client.options.headers); - - const options = this.channelOptions; - new PaginatedResource(client, this.basePath + '/messages', headers, envelope, async function ( - body, - headers, - unpacked - ) { - return await Message.fromResponseBody(body as Message[], options, client._MsgPack, unpacked ? undefined : format); - }).get(params as Record, callback); + this.client.rest.channelMixin.history(this, params, callback); } publish(): void | Promise { @@ -185,18 +135,19 @@ class RestChannel extends EventEmitter { } _publish(requestBody: unknown, headers: Record, params: any, callback: ResourceCallback): void { - Resource.post(this.client, this.basePath + '/messages', requestBody, headers, params, null, callback); + Resource.post( + this.client, + this.client.rest.channelMixin.basePath(this) + '/messages', + requestBody, + headers, + params, + null, + callback + ); } status(callback?: StandardCallback): void | Promise { - if (typeof callback !== 'function') { - return Utils.promisify(this, 'status', []); - } - - const format = this.client.options.useBinaryProtocol ? Utils.Format.msgpack : Utils.Format.json; - const headers = Defaults.defaultPostHeaders(this.client.options, { format }); - - Resource.get(this.client, this.basePath, headers, {}, format, callback || noop); + return this.client.rest.channelMixin.status(this, callback); } } diff --git a/src/common/lib/client/restchannelmixin.ts b/src/common/lib/client/restchannelmixin.ts new file mode 100644 index 0000000000..9986dc4e74 --- /dev/null +++ b/src/common/lib/client/restchannelmixin.ts @@ -0,0 +1,67 @@ +import * as API from '../../../../ably'; +import RestChannel from './restchannel'; +import RealtimeChannel from './realtimechannel'; +import * as Utils from '../util/utils'; +import { PaginatedResultCallback, StandardCallback } from '../../types/utils'; +import Message from '../types/message'; +import Defaults from '../util/defaults'; +import PaginatedResource from './paginatedresource'; +import Resource from './resource'; + +export interface RestHistoryParams { + start?: number; + end?: number; + direction?: string; + limit?: number; +} + +const noop = function () {}; + +export class RestChannelMixin { + static basePath(channel: RestChannel | RealtimeChannel) { + return '/channels/' + encodeURIComponent(channel.name); + } + + static history( + channel: RestChannel | RealtimeChannel, + params: RestHistoryParams | null, + callback: PaginatedResultCallback + ): void { + const client = channel.client, + format = client.options.useBinaryProtocol ? Utils.Format.msgpack : Utils.Format.json, + envelope = channel.client.http.supportsLinkHeaders ? undefined : format, + headers = Defaults.defaultGetHeaders(client.options, { format }); + + Utils.mixin(headers, client.options.headers); + + const options = channel.channelOptions; + new PaginatedResource(client, this.basePath(channel) + '/messages', headers, envelope, async function ( + body, + headers, + unpacked + ) { + return await Message.fromResponseBody(body as Message[], options, client._MsgPack, unpacked ? undefined : format); + }).get(params as Record, callback); + } + + static status( + channel: RestChannel | RealtimeChannel, + callback?: StandardCallback + ): void | Promise { + if (typeof callback !== 'function') { + return Utils.promisify(this, 'status', [channel]); + } + + const format = channel.client.options.useBinaryProtocol ? Utils.Format.msgpack : Utils.Format.json; + const headers = Defaults.defaultPostHeaders(channel.client.options, { format }); + + Resource.get( + channel.client, + this.basePath(channel), + headers, + {}, + format, + callback || noop + ); + } +} diff --git a/src/common/lib/client/restpresence.ts b/src/common/lib/client/restpresence.ts index 7a4f73960e..7aec14dcb2 100644 --- a/src/common/lib/client/restpresence.ts +++ b/src/common/lib/client/restpresence.ts @@ -1,22 +1,17 @@ import * as Utils from '../util/utils'; -import EventEmitter from '../util/eventemitter'; import Logger from '../util/logger'; import PaginatedResource, { PaginatedResult } from './paginatedresource'; import PresenceMessage from '../types/presencemessage'; import { CipherOptions } from '../types/message'; import { PaginatedResultCallback } from '../../types/utils'; import RestChannel from './restchannel'; -import RealtimeChannel from './realtimechannel'; import Defaults from '../util/defaults'; -class RestPresence extends EventEmitter { - channel: RealtimeChannel | RestChannel; - basePath: string; +class RestPresence { + channel: RestChannel; - constructor(channel: RealtimeChannel | RestChannel) { - super(); + constructor(channel: RestChannel) { this.channel = channel; - this.basePath = channel.basePath + '/presence'; } get(params: any, callback: PaginatedResultCallback): void | Promise { @@ -38,14 +33,20 @@ class RestPresence extends EventEmitter { Utils.mixin(headers, client.options.headers); const options = this.channel.channelOptions; - new PaginatedResource(client, this.basePath, headers, envelope, async function (body, headers, unpacked) { - return await PresenceMessage.fromResponseBody( - body as Record[], - options as CipherOptions, - client._MsgPack, - unpacked ? undefined : format - ); - }).get(params, callback); + new PaginatedResource( + client, + this.channel.client.rest.presenceMixin.basePath(this), + headers, + envelope, + async function (body, headers, unpacked) { + return await PresenceMessage.fromResponseBody( + body as Record[], + options as CipherOptions, + client._MsgPack, + unpacked ? undefined : format + ); + } + ).get(params, callback); } history( @@ -53,43 +54,7 @@ class RestPresence extends EventEmitter { callback: PaginatedResultCallback ): void | Promise> { Logger.logAction(Logger.LOG_MICRO, 'RestPresence.history()', 'channel = ' + this.channel.name); - return this._history(params, callback); - } - - _history( - params: any, - callback: PaginatedResultCallback - ): void | Promise> { - /* params and callback are optional; see if params contains the callback */ - if (callback === undefined) { - if (typeof params == 'function') { - callback = params; - params = null; - } else { - return Utils.promisify(this, '_history', [params]); - } - } - - const client = this.channel.client, - format = client.options.useBinaryProtocol ? Utils.Format.msgpack : Utils.Format.json, - envelope = this.channel.client.http.supportsLinkHeaders ? undefined : format, - headers = Defaults.defaultGetHeaders(client.options, { format }); - - Utils.mixin(headers, client.options.headers); - - const options = this.channel.channelOptions; - new PaginatedResource(client, this.basePath + '/history', headers, envelope, async function ( - body, - headers, - unpacked - ) { - return await PresenceMessage.fromResponseBody( - body as Record[], - options as CipherOptions, - client._MsgPack, - unpacked ? undefined : format - ); - }).get(params, callback); + return this.channel.client.rest.presenceMixin.history(this, params, callback); } } diff --git a/src/common/lib/client/restpresencemixin.ts b/src/common/lib/client/restpresencemixin.ts new file mode 100644 index 0000000000..9859150c3f --- /dev/null +++ b/src/common/lib/client/restpresencemixin.ts @@ -0,0 +1,51 @@ +import RestPresence from './restpresence'; +import RealtimePresence from './realtimepresence'; +import * as Utils from '../util/utils'; +import { PaginatedResultCallback } from '../../types/utils'; +import Defaults from '../util/defaults'; +import PaginatedResource, { PaginatedResult } from './paginatedresource'; +import PresenceMessage from '../types/presencemessage'; +import { CipherOptions } from '../types/message'; + +export class RestPresenceMixin { + static basePath(presence: RestPresence | RealtimePresence) { + return '/channels/' + encodeURIComponent(presence.channel.name); + } + + static history( + presence: RestPresence | RealtimePresence, + params: any, + callback: PaginatedResultCallback + ): void | Promise> { + /* params and callback are optional; see if params contains the callback */ + if (callback === undefined) { + if (typeof params == 'function') { + callback = params; + params = null; + } else { + return Utils.promisify(this, 'history', [presence, params]); + } + } + + const client = presence.channel.client, + format = client.options.useBinaryProtocol ? Utils.Format.msgpack : Utils.Format.json, + envelope = presence.channel.client.http.supportsLinkHeaders ? undefined : format, + headers = Defaults.defaultGetHeaders(client.options, { format }); + + Utils.mixin(headers, client.options.headers); + + const options = presence.channel.channelOptions; + new PaginatedResource(client, this.basePath(presence) + '/history', headers, envelope, async function ( + body, + headers, + unpacked + ) { + return await PresenceMessage.fromResponseBody( + body as Record[], + options as CipherOptions, + client._MsgPack, + unpacked ? undefined : format + ); + }).get(params, callback); + } +} diff --git a/src/common/lib/util/defaults.ts b/src/common/lib/util/defaults.ts index 9939c975c7..489d651307 100644 --- a/src/common/lib/util/defaults.ts +++ b/src/common/lib/util/defaults.ts @@ -6,6 +6,8 @@ import { version } from '../../../../package.json'; import ClientOptions, { InternalClientOptions, NormalisedClientOptions } from 'common/types/ClientOptions'; import IDefaults from '../../types/IDefaults'; import { MsgPack } from 'common/types/msgpack'; +import { IUntypedCryptoStatic } from 'common/types/ICryptoStatic'; +import { ChannelOptions } from 'common/types/channel'; let agent = 'ably-js/' + version; @@ -265,6 +267,22 @@ export function normaliseOptions(options: InternalClientOptions, MsgPack: MsgPac }; } +export function normaliseChannelOptions(Crypto: IUntypedCryptoStatic | null, options?: ChannelOptions) { + const channelOptions = options || {}; + if (channelOptions.cipher) { + if (!Crypto) Utils.throwMissingModuleError('Crypto'); + const cipher = Crypto.getCipher(channelOptions.cipher); + channelOptions.cipher = cipher.cipherParams; + channelOptions.channelCipher = cipher.cipher; + } else if ('cipher' in channelOptions) { + /* Don't deactivate an existing cipher unless options + * has a 'cipher' key that's falsey */ + channelOptions.cipher = undefined; + channelOptions.channelCipher = null; + } + return channelOptions; +} + const contentTypes = { json: 'application/json', xml: 'application/xml', diff --git a/test/browser/modules.test.js b/test/browser/modules.test.js index c726510fe9..f2ba4b77cb 100644 --- a/test/browser/modules.test.js +++ b/test/browser/modules.test.js @@ -85,6 +85,19 @@ describe('browser/modules', function () { }, action: (client) => client.auth.revokeTokens([{ type: 'clientId', value: 'foo' }]), }, + { + description: 'call channel’s `history()`', + action: (client) => client.channels.get('channel').history(), + }, + { + description: 'call channel’s `presence.history()`', + additionalRealtimeModules: { RealtimePresence }, + action: (client) => client.channels.get('channel').presence.history(), + }, + { + description: 'call channel’s `status()`', + action: (client) => client.channels.get('channel').status(), + }, ]; describe('BaseRest without explicit Rest', () => { @@ -111,6 +124,7 @@ describe('browser/modules', function () { WebSocketTransport, FetchRequest, Rest, + ...scenario.additionalRealtimeModules, }); let thrownError = null; @@ -145,13 +159,22 @@ describe('browser/modules', function () { }); for (const scenario of restScenarios) { - it(`throws an error when attempting to ${scenario.description}`, () => { + it(`throws an error when attempting to ${scenario.description}`, async () => { const client = new BaseRealtime(ablyClientOptions(scenario.getAdditionalClientOptions?.()), { WebSocketTransport, FetchRequest, + ...scenario.additionalRealtimeModules, }); - expect(() => scenario.action(client)).to.throw('Rest module not provided'); + let thrownError = null; + try { + await scenario.action(client); + } catch (error) { + thrownError = error; + } + + expect(thrownError).not.to.be.null; + expect(thrownError.message).to.equal('Rest module not provided'); }); } });