diff --git a/scripts/moduleReport.js b/scripts/moduleReport.js index b0eb1f4445..a14657d888 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; diff --git a/src/common/lib/client/baseclient.ts b/src/common/lib/client/baseclient.ts index 922a9f214b..52b225a34b 100644 --- a/src/common/lib/client/baseclient.ts +++ b/src/common/lib/client/baseclient.ts @@ -1,24 +1,18 @@ -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 Platform from '../../platform'; import Message from '../types/message'; import PresenceMessage from '../types/presencemessage'; import { ModulesMap } from './modulesmap'; - -const noop = function () {}; +import { Rest } from './rest'; /** `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. @@ -32,14 +26,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); @@ -86,8 +76,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) { @@ -98,78 +103,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( @@ -181,54 +119,7 @@ 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); } setLog(logOptions: LoggerOptions): void { @@ -241,32 +132,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 b4740efca4..4ff45345bc 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 `BaseRealtime` 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 5bf441170a..412e636c2a 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 `BaseRest` 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..505bc05edc --- /dev/null +++ b/src/common/lib/client/rest.ts @@ -0,0 +1,197 @@ +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 Platform from '../../platform'; +import BaseClient from './baseclient'; + +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 + ); + } + } + + 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 1cf703c540..d1931cb59c 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 182e276098..abebae04fa 100644 --- a/src/platform/web/modules.ts +++ b/src/platform/web/modules.ts @@ -46,4 +46,5 @@ if (Platform.Config.noUpgrade) { Platform.Defaults.upgradeTransports = []; } +export { Rest } from '../../common/lib/client/rest'; export { BaseRest, BaseRealtime }; diff --git a/test/browser/modules.test.js b/test/browser/modules.test.js index 16725b20ea..3cf7845fd7 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); @@ -11,13 +11,32 @@ describe('browser/modules', function () { }); }); - [BaseRest, BaseRealtime].forEach((clientClass) => { - it(clientClass.name, async function () { - const client = new clientClass({ key }, {}); + describe('Rest', function () { + it('BaseRest without explicit Rest', async function () { + const client = new BaseRest({ key }, { Rest }); const time = await client.time(); if (typeof time !== 'number') { throw new Error('client.time returned wrong type'); } }); + + it('BaseRealtime with Rest', async function () { + const client = new BaseRealtime({ key }, { Rest }); + const time = await client.time(); + if (typeof time !== 'number') { + throw new Error('client.time returned wrong type'); + } + }); + + it('BaseRealtime without Rest', function (done) { + const client = new BaseRealtime({ key }, {}); + try { + client.time(); + } catch (err) { + done(); + return; + } + done(new Error('Expected client.time to throw')); + }); }); });