diff --git a/scripts/moduleReport.js b/scripts/moduleReport.js index 19d1071672..61bdff99e3 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 = []; +const moduleNames = ['Rest']; function formatBytes(bytes) { const kibibytes = bytes / 1024; @@ -40,7 +40,7 @@ const errors = []; const size = getImportSize([baseClient, moduleName]); console.log(`${baseClient} + ${moduleName}: ${formatBytes(size)}`); - if (!(baseClientSize < size)) { + if (!(baseClientSize < size) && !(baseClient === 'BaseRest' && moduleName === 'Rest')) { // Emit an error if adding the module does not increase the bundle size // (this means that the module is not being tree-shaken correctly). errors.push(new Error(`Adding ${moduleName} to ${baseClient} does not increase the bundle size.`)); diff --git a/src/common/lib/client/baseclient.ts b/src/common/lib/client/baseclient.ts index 1486df07b5..aeac034d62 100644 --- a/src/common/lib/client/baseclient.ts +++ b/src/common/lib/client/baseclient.ts @@ -1,24 +1,19 @@ -import * as Utils from '../util/utils'; import Logger, { LoggerOptions } from '../util/logger'; import Defaults from '../util/defaults'; import Auth from './auth'; -import Push from './push'; -import PaginatedResource, { HttpPaginatedResponse, PaginatedResult } from './paginatedresource'; -import Channel from './channel'; +import { HttpPaginatedResponse, PaginatedResult } from './paginatedresource'; import ErrorInfo from '../types/errorinfo'; import Stats from '../types/stats'; -import HttpMethods from '../../constants/HttpMethods'; -import { ChannelOptions } from '../../types/channel'; -import { PaginatedResultCallback, StandardCallback } from '../../types/utils'; -import { ErrnoException, IHttp, RequestParams } from '../../types/http'; +import { StandardCallback } from '../../types/utils'; +import { IHttp, RequestParams } from '../../types/http'; import ClientOptions, { NormalisedClientOptions } from '../../types/ClientOptions'; import * as API from '../../../../ably'; import Platform from '../../platform'; import Message from '../types/message'; import PresenceMessage from '../types/presencemessage'; -import Resource from './resource'; import { ModulesMap } from './modulesmap'; +import { Rest } from './rest'; type BatchResult = API.Types.BatchResult; type BatchPublishSpec = API.Types.BatchPublishSpec; @@ -29,8 +24,6 @@ type BatchPresenceSuccessResult = API.Types.BatchPresenceSuccessResult; type BatchPresenceFailureResult = API.Types.BatchPresenceFailureResult; type BatchPresenceResult = BatchResult; -const noop = function () {}; - /** `BaseClient` acts as the base class for all of the client classes exported by the SDK. It is an implementation detail and this class is not advertised publicly. */ @@ -43,14 +36,10 @@ class BaseClient { serverTimeOffset: number | null; http: IHttp; auth: Auth; - channels: Channels; - push: Push; - constructor( - options: ClientOptions | string, - // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars - modules: ModulesMap - ) { + private readonly _rest: Rest | null; + + constructor(options: ClientOptions | string, modules: ModulesMap) { if (!options) { const msg = 'no options provided'; Logger.logAction(Logger.LOG_ERROR, 'BaseClient()', msg); @@ -97,8 +86,23 @@ class BaseClient { this.serverTimeOffset = null; this.http = new Platform.Http(normalOptions); this.auth = new Auth(this, normalOptions); - this.channels = new Channels(this); - this.push = new Push(this); + + this._rest = modules.Rest ? new modules.Rest(this) : null; + } + + private get rest(): Rest { + if (!this._rest) { + throw new ErrorInfo('Rest module not provided', 400, 40000); + } + return this._rest; + } + + get channels() { + return this.rest.channels; + } + + get push() { + return this.rest.push; } baseUri(host: string) { @@ -109,78 +113,11 @@ class BaseClient { params: RequestParams, callback: StandardCallback> ): Promise> | void { - /* 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, 'stats', [params]) as Promise>; - } - } - const headers = Defaults.defaultGetHeaders(this.options), - format = this.options.useBinaryProtocol ? Utils.Format.msgpack : Utils.Format.json, - envelope = this.http.supportsLinkHeaders ? undefined : format; - - Utils.mixin(headers, this.options.headers); - - new PaginatedResource(this, '/stats', headers, envelope, function ( - body: unknown, - headers: Record, - unpacked?: boolean - ) { - const statsValues = unpacked ? body : JSON.parse(body as string); - for (let i = 0; i < statsValues.length; i++) statsValues[i] = Stats.fromValues(statsValues[i]); - return statsValues; - }).get(params as Record, callback); + return this.rest.stats(params, callback); } time(params?: RequestParams | StandardCallback, callback?: StandardCallback): Promise | void { - /* 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, 'time', [params]) as Promise; - } - } - - const _callback = callback || noop; - - const headers = Defaults.defaultGetHeaders(this.options); - if (this.options.headers) Utils.mixin(headers, this.options.headers); - const timeUri = (host: string) => { - return this.baseUri(host) + '/time'; - }; - this.http.do( - HttpMethods.Get, - this, - timeUri, - headers, - null, - params as RequestParams, - ( - err?: ErrorInfo | ErrnoException | null, - res?: unknown, - headers?: Record, - unpacked?: boolean - ) => { - if (err) { - _callback(err); - return; - } - if (!unpacked) res = JSON.parse(res as string); - const time = (res as number[])[0]; - if (!time) { - _callback(new ErrorInfo('Internal error (unexpected result type from GET /time)', 50000, 500)); - return; - } - /* calculate time offset only once for this device by adding to the prototype */ - this.serverTimeOffset = time - Utils.now(); - _callback(null, time); - } - ); + return this.rest.time(params, callback); } request( @@ -192,141 +129,17 @@ class BaseClient { customHeaders: Record, callback: StandardCallback> ): Promise> | void { - const useBinary = this.options.useBinaryProtocol, - encoder = useBinary ? Platform.Config.msgpack.encode : JSON.stringify, - decoder = useBinary ? Platform.Config.msgpack.decode : JSON.parse, - format = useBinary ? Utils.Format.msgpack : Utils.Format.json, - envelope = this.http.supportsLinkHeaders ? undefined : format; - params = params || {}; - const _method = method.toLowerCase() as HttpMethods; - const headers = - _method == 'get' - ? Defaults.defaultGetHeaders(this.options, { format, protocolVersion: version }) - : Defaults.defaultPostHeaders(this.options, { format, protocolVersion: version }); - - if (callback === undefined) { - return Utils.promisify(this, 'request', [method, path, version, params, body, customHeaders]) as Promise< - HttpPaginatedResponse - >; - } - - if (typeof body !== 'string') { - body = encoder(body); - } - Utils.mixin(headers, this.options.headers); - if (customHeaders) { - Utils.mixin(headers, customHeaders); - } - const paginatedResource = new PaginatedResource( - this, - path, - headers, - envelope, - async function (resbody: unknown, headers: Record, unpacked?: boolean) { - return Utils.ensureArray(unpacked ? resbody : decoder(resbody as string & Buffer)); - }, - /* useHttpPaginatedResponse: */ true - ); - - if (!Utils.arrIn(Platform.Http.methods, _method)) { - throw new ErrorInfo('Unsupported method ' + _method, 40500, 405); - } - - if (Utils.arrIn(Platform.Http.methodsWithBody, _method)) { - paginatedResource[_method as HttpMethods.Post](params, body, callback as PaginatedResultCallback); - } else { - paginatedResource[_method as HttpMethods.Get | HttpMethods.Delete]( - params, - callback as PaginatedResultCallback - ); - } + return this.rest.request(method, path, version, params, body, customHeaders, callback); } batchPublish( specOrSpecs: T - ): Promise; - batchPublish( - specOrSpecs: T, - callback?: StandardCallback - ): void | Promise { - if (callback === undefined) { - return Utils.promisify(this, 'batchPublish', [specOrSpecs]); - } - - let requestBodyDTO: BatchPublishSpec[]; - let singleSpecMode: boolean; - if (Utils.isArray(specOrSpecs)) { - requestBodyDTO = specOrSpecs; - singleSpecMode = false; - } else { - requestBodyDTO = [specOrSpecs]; - singleSpecMode = true; - } - - const format = this.options.useBinaryProtocol ? Utils.Format.msgpack : Utils.Format.json, - headers = Defaults.defaultPostHeaders(this.options, { format }); - - if (this.options.headers) Utils.mixin(headers, this.options.headers); - - const requestBody = Utils.encodeBody(requestBodyDTO, format); - Resource.post( - this, - '/messages', - requestBody, - headers, - { newBatchResponse: 'true' }, - null, - (err, body, headers, unpacked) => { - if (err) { - callback(err); - return; - } - - const batchResults = (unpacked ? body : Utils.decodeBody(body, format)) as BatchPublishResult[]; - - // I don't love the below type assertions for `callback` but not sure how to avoid them - if (singleSpecMode) { - (callback as StandardCallback)(null, batchResults[0]); - } else { - (callback as StandardCallback)(null, batchResults); - } - } - ); + ): Promise { + return this.rest.batchPublish(specOrSpecs); } - batchPresence(channels: string[]): Promise; - batchPresence( - channels: string[], - callback?: StandardCallback - ): void | Promise { - if (callback === undefined) { - return Utils.promisify(this, 'batchPresence', [channels]); - } - - const format = this.options.useBinaryProtocol ? Utils.Format.msgpack : Utils.Format.json, - headers = Defaults.defaultPostHeaders(this.options, { format }); - - if (this.options.headers) Utils.mixin(headers, this.options.headers); - - const channelsParam = channels.join(','); - - Resource.get( - this, - '/presence', - headers, - { newBatchResponse: 'true', channels: channelsParam }, - null, - (err, body, headers, unpacked) => { - if (err) { - callback(err); - return; - } - - const batchResult = (unpacked ? body : Utils.decodeBody(body, format)) as BatchPresenceResult; - - callback(null, batchResult); - } - ); + batchPresence(channels: string[]): Promise { + return this.rest.batchPresence(channels); } setLog(logOptions: LoggerOptions): void { @@ -339,32 +152,4 @@ class BaseClient { static PresenceMessage = PresenceMessage; } -class Channels { - client: BaseClient; - all: Record; - - constructor(client: BaseClient) { - this.client = client; - this.all = Object.create(null); - } - - get(name: string, channelOptions?: ChannelOptions) { - name = String(name); - let channel = this.all[name]; - if (!channel) { - this.all[name] = channel = new Channel(this.client, name, channelOptions); - } else if (channelOptions) { - channel.setOptions(channelOptions); - } - - return channel; - } - - /* Included to support certain niche use-cases; most users should ignore this. - * Please do not use this unless you know what you're doing */ - release(name: string) { - delete this.all[String(name)]; - } -} - export default BaseClient; diff --git a/src/common/lib/client/baserealtime.ts b/src/common/lib/client/baserealtime.ts index 74057b9a58..fb7e888bb0 100644 --- a/src/common/lib/client/baserealtime.ts +++ b/src/common/lib/client/baserealtime.ts @@ -18,17 +18,21 @@ import { ModulesMap } from './modulesmap'; `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 { - channels: any; + _channels: any; connection: Connection; constructor(options: ClientOptions, modules: ModulesMap) { super(options, modules); Logger.logAction(Logger.LOG_MINOR, 'Realtime()', ''); this.connection = new Connection(this, this.options); - this.channels = new Channels(this); + this._channels = new Channels(this); if (options.autoConnect !== false) this.connect(); } + get channels() { + return this._channels; + } + connect(): void { Logger.logAction(Logger.LOG_MINOR, 'Realtime.connect()', ''); this.connection.connect(); diff --git a/src/common/lib/client/baserest.ts b/src/common/lib/client/baserest.ts index 6073aafd00..247145f989 100644 --- a/src/common/lib/client/baserest.ts +++ b/src/common/lib/client/baserest.ts @@ -1,6 +1,15 @@ import BaseClient from './baseclient'; +import ClientOptions from '../../types/ClientOptions'; +import { ModulesMap } from './modulesmap'; +import { Rest } from './rest'; /** `BaseRest` is an export of the tree-shakable version of the SDK, and acts as the base class for the `DefaultRest` class exported by the non tree-shakable version. + + It always includes the `Rest` module. */ -export class BaseRest extends BaseClient {} +export class BaseRest extends BaseClient { + constructor(options: ClientOptions | string, modules: ModulesMap) { + super(options, { Rest, ...modules }); + } +} diff --git a/src/common/lib/client/modulesmap.ts b/src/common/lib/client/modulesmap.ts index 13b207d8c5..c56c2d98e7 100644 --- a/src/common/lib/client/modulesmap.ts +++ b/src/common/lib/client/modulesmap.ts @@ -1,3 +1,7 @@ -export interface ModulesMap {} +import { Rest } from './rest'; -export const allCommonModules: ModulesMap = {}; +export interface ModulesMap { + Rest?: typeof Rest; +} + +export const allCommonModules: ModulesMap = { Rest }; diff --git a/src/common/lib/client/rest.ts b/src/common/lib/client/rest.ts new file mode 100644 index 0000000000..a9d11b3c3b --- /dev/null +++ b/src/common/lib/client/rest.ts @@ -0,0 +1,295 @@ +import * as Utils from '../util/utils'; +import Logger, { LoggerOptions } from '../util/logger'; +import Defaults from '../util/defaults'; +import Push from './push'; +import PaginatedResource, { HttpPaginatedResponse, PaginatedResult } from './paginatedresource'; +import Channel from './channel'; +import ErrorInfo from '../types/errorinfo'; +import Stats from '../types/stats'; +import HttpMethods from '../../constants/HttpMethods'; +import { ChannelOptions } from '../../types/channel'; +import { PaginatedResultCallback, StandardCallback } from '../../types/utils'; +import { ErrnoException, RequestParams } from '../../types/http'; +import * as API from '../../../../ably'; +import Resource from './resource'; + +import Platform from '../../platform'; +import BaseClient from './baseclient'; + +type BatchResult = API.Types.BatchResult; +type BatchPublishSpec = API.Types.BatchPublishSpec; +type BatchPublishSuccessResult = API.Types.BatchPublishSuccessResult; +type BatchPublishFailureResult = API.Types.BatchPublishFailureResult; +type BatchPublishResult = BatchResult; +type BatchPresenceSuccessResult = API.Types.BatchPresenceSuccessResult; +type BatchPresenceFailureResult = API.Types.BatchPresenceFailureResult; +type BatchPresenceResult = BatchResult; + +const noop = function () {}; +export class Rest { + private readonly client: BaseClient; + readonly channels: Channels; + readonly push: Push; + + constructor(client: BaseClient) { + this.client = client; + this.channels = new Channels(this.client); + this.push = new Push(this.client); + } + + stats( + params: RequestParams, + callback: StandardCallback> + ): Promise> | void { + /* 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, 'stats', [params]) as Promise>; + } + } + const headers = Defaults.defaultGetHeaders(this.client.options), + format = this.client.options.useBinaryProtocol ? Utils.Format.msgpack : Utils.Format.json, + envelope = this.client.http.supportsLinkHeaders ? undefined : format; + + Utils.mixin(headers, this.client.options.headers); + + new PaginatedResource(this.client, '/stats', headers, envelope, function ( + body: unknown, + headers: Record, + unpacked?: boolean + ) { + const statsValues = unpacked ? body : JSON.parse(body as string); + for (let i = 0; i < statsValues.length; i++) statsValues[i] = Stats.fromValues(statsValues[i]); + return statsValues; + }).get(params as Record, callback); + } + + time(params?: RequestParams | StandardCallback, callback?: StandardCallback): Promise | void { + /* 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, 'time', [params]) as Promise; + } + } + + const _callback = callback || noop; + + const headers = Defaults.defaultGetHeaders(this.client.options); + if (this.client.options.headers) Utils.mixin(headers, this.client.options.headers); + const timeUri = (host: string) => { + return this.client.baseUri(host) + '/time'; + }; + this.client.http.do( + HttpMethods.Get, + this.client, + timeUri, + headers, + null, + params as RequestParams, + ( + err?: ErrorInfo | ErrnoException | null, + res?: unknown, + headers?: Record, + unpacked?: boolean + ) => { + if (err) { + _callback(err); + return; + } + if (!unpacked) res = JSON.parse(res as string); + const time = (res as number[])[0]; + if (!time) { + _callback(new ErrorInfo('Internal error (unexpected result type from GET /time)', 50000, 500)); + return; + } + /* calculate time offset only once for this device by adding to the prototype */ + this.client.serverTimeOffset = time - Utils.now(); + _callback(null, time); + } + ); + } + + request( + method: string, + path: string, + version: number, + params: RequestParams, + body: unknown, + customHeaders: Record, + callback: StandardCallback> + ): Promise> | void { + const useBinary = this.client.options.useBinaryProtocol, + encoder = useBinary ? Platform.Config.msgpack.encode : JSON.stringify, + decoder = useBinary ? Platform.Config.msgpack.decode : JSON.parse, + format = useBinary ? Utils.Format.msgpack : Utils.Format.json, + envelope = this.client.http.supportsLinkHeaders ? undefined : format; + params = params || {}; + const _method = method.toLowerCase() as HttpMethods; + const headers = + _method == 'get' + ? Defaults.defaultGetHeaders(this.client.options, { format, protocolVersion: version }) + : Defaults.defaultPostHeaders(this.client.options, { format, protocolVersion: version }); + + if (callback === undefined) { + return Utils.promisify(this, 'request', [method, path, version, params, body, customHeaders]) as Promise< + HttpPaginatedResponse + >; + } + + if (typeof body !== 'string') { + body = encoder(body); + } + Utils.mixin(headers, this.client.options.headers); + if (customHeaders) { + Utils.mixin(headers, customHeaders); + } + const paginatedResource = new PaginatedResource( + this.client, + path, + headers, + envelope, + async function (resbody: unknown, headers: Record, unpacked?: boolean) { + return Utils.ensureArray(unpacked ? resbody : decoder(resbody as string & Buffer)); + }, + /* useHttpPaginatedResponse: */ true + ); + + if (!Utils.arrIn(Platform.Http.methods, _method)) { + throw new ErrorInfo('Unsupported method ' + _method, 40500, 405); + } + + if (Utils.arrIn(Platform.Http.methodsWithBody, _method)) { + paginatedResource[_method as HttpMethods.Post](params, body, callback as PaginatedResultCallback); + } else { + paginatedResource[_method as HttpMethods.Get | HttpMethods.Delete]( + params, + callback as PaginatedResultCallback + ); + } + } + + batchPublish( + specOrSpecs: T + ): Promise; + batchPublish( + specOrSpecs: T, + callback?: StandardCallback + ): void | Promise { + if (callback === undefined) { + return Utils.promisify(this, 'batchPublish', [specOrSpecs]); + } + + let requestBodyDTO: BatchPublishSpec[]; + let singleSpecMode: boolean; + if (Utils.isArray(specOrSpecs)) { + requestBodyDTO = specOrSpecs; + singleSpecMode = false; + } else { + requestBodyDTO = [specOrSpecs]; + singleSpecMode = true; + } + + const format = this.client.options.useBinaryProtocol ? Utils.Format.msgpack : Utils.Format.json, + headers = Defaults.defaultPostHeaders(this.client.options, { format }); + + if (this.client.options.headers) Utils.mixin(headers, this.client.options.headers); + + const requestBody = Utils.encodeBody(requestBodyDTO, format); + Resource.post( + this.client, + '/messages', + requestBody, + headers, + { newBatchResponse: 'true' }, + null, + (err, body, headers, unpacked) => { + if (err) { + callback(err); + return; + } + + const batchResults = (unpacked ? body : Utils.decodeBody(body, format)) as BatchPublishResult[]; + + // I don't love the below type assertions for `callback` but not sure how to avoid them + if (singleSpecMode) { + (callback as StandardCallback)(null, batchResults[0]); + } else { + (callback as StandardCallback)(null, batchResults); + } + } + ); + } + + batchPresence(channels: string[]): Promise; + batchPresence( + channels: string[], + callback?: StandardCallback + ): void | Promise { + if (callback === undefined) { + return Utils.promisify(this, 'batchPresence', [channels]); + } + + const format = this.client.options.useBinaryProtocol ? Utils.Format.msgpack : Utils.Format.json, + headers = Defaults.defaultPostHeaders(this.client.options, { format }); + + if (this.client.options.headers) Utils.mixin(headers, this.client.options.headers); + + const channelsParam = channels.join(','); + + Resource.get( + this.client, + '/presence', + headers, + { newBatchResponse: 'true', channels: channelsParam }, + null, + (err, body, headers, unpacked) => { + if (err) { + callback(err); + return; + } + + const batchResult = (unpacked ? body : Utils.decodeBody(body, format)) as BatchPresenceResult; + + callback(null, batchResult); + } + ); + } + + setLog(logOptions: LoggerOptions): void { + Logger.setLog(logOptions.level, logOptions.handler); + } +} + +class Channels { + client: BaseClient; + all: Record; + + constructor(client: BaseClient) { + this.client = client; + this.all = Object.create(null); + } + + get(name: string, channelOptions?: ChannelOptions) { + name = String(name); + let channel = this.all[name]; + if (!channel) { + this.all[name] = channel = new Channel(this.client, name, channelOptions); + } else if (channelOptions) { + channel.setOptions(channelOptions); + } + + return channel; + } + + /* Included to support certain niche use-cases; most users should ignore this. + * Please do not use this unless you know what you're doing */ + release(name: string) { + delete this.all[String(name)]; + } +} diff --git a/src/common/types/http.d.ts b/src/common/types/http.d.ts index 16be6249cd..0727978b52 100644 --- a/src/common/types/http.d.ts +++ b/src/common/types/http.d.ts @@ -1,5 +1,5 @@ import HttpMethods from '../constants/HttpMethods'; -import BaseClient from '../lib/client/baseclient'; +import { BaseClient } from '../lib/client/baseclient'; import ErrorInfo from '../lib/types/errorinfo'; import { Agents } from 'got'; diff --git a/src/platform/web/modules.ts b/src/platform/web/modules.ts index e1031a9c17..8054981daf 100644 --- a/src/platform/web/modules.ts +++ b/src/platform/web/modules.ts @@ -47,4 +47,5 @@ if (Platform.Config.noUpgrade) { Platform.Defaults.upgradeTransports = []; } +export { Rest } from '../../common/lib/client/rest'; export { BaseRest, BaseRealtime, ErrorInfo }; diff --git a/test/browser/modules.test.js b/test/browser/modules.test.js index 755d70063f..6ccbf64bbc 100644 --- a/test/browser/modules.test.js +++ b/test/browser/modules.test.js @@ -1,4 +1,4 @@ -import { BaseRest, BaseRealtime } from '../../build/modules/index.js'; +import { BaseRest, BaseRealtime, Rest } from '../../build/modules/index.js'; describe('browser/modules', function () { this.timeout(10 * 1000); @@ -12,11 +12,36 @@ describe('browser/modules', function () { describe('without any modules', () => { for (const clientClass of [BaseRest, BaseRealtime]) { - it(clientClass.name, async () => { - const client = new clientClass(ablyClientOptions()); + describe(clientClass.name, () => { + it('can be constructed', async () => { + expect(() => new clientClass(ablyClientOptions(), {})).not.to.throw(); + }); + }); + } + }); + + describe('Rest', () => { + describe('BaseRest without explicit Rest', () => { + it('offers REST functionality', async () => { + const client = new BaseRest(ablyClientOptions(), {}); const time = await client.time(); expect(time).to.be.a('number'); }); - } + }); + + describe('BaseRealtime with Rest', () => { + it('offers REST functionality', async () => { + const client = new BaseRealtime(ablyClientOptions(), { Rest }); + const time = await client.time(); + expect(time).to.be.a('number'); + }); + }); + + describe('BaseRealtime without Rest', () => { + it('throws an error when attempting to use REST functionality', async () => { + const client = new BaseRealtime(ablyClientOptions(), {}); + expect(() => client.time()).to.throw('Rest module not provided'); + }); + }); }); });