Skip to content

Commit

Permalink
Split REST functionality into a tree-shakable module
Browse files Browse the repository at this point in the history
This moves the following functionality from BaseRest into a new module
named Rest:

- methods that wrap REST endpoints (e.g. `stats`)
- the `request` method
- all functionality accessed via `BaseRest.channels` and `BaseRest.push`

This allows us to now construct a BaseRealtime instance that doesn’t
have REST functionality.

Note that the BaseRest class always includes the Rest module. This comes
after discussions in which we decided that it would be quite useless
without it.

Resolves #1374.

Co-authored-by: Owen Pearson <owen.pearson@ably.com>
  • Loading branch information
lawrence-forooghian and owenpearson committed Aug 15, 2023
1 parent f3245d5 commit a420d0e
Show file tree
Hide file tree
Showing 9 changed files with 272 additions and 175 deletions.
2 changes: 1 addition & 1 deletion scripts/moduleReport.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
191 changes: 27 additions & 164 deletions src/common/lib/client/baseclient.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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);
Expand Down Expand Up @@ -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) {
Expand All @@ -98,78 +103,11 @@ class BaseClient {
params: RequestParams,
callback: StandardCallback<PaginatedResult<Stats>>
): Promise<PaginatedResult<Stats>> | 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<PaginatedResult<Stats>>;
}
}
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<string, string>,
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<string, string>, callback);
return this.rest.stats(params, callback);
}

time(params?: RequestParams | StandardCallback<number>, callback?: StandardCallback<number>): Promise<number> | 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<number>;
}
}

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<string, string>,
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(
Expand All @@ -181,54 +119,7 @@ class BaseClient {
customHeaders: Record<string, string>,
callback: StandardCallback<HttpPaginatedResponse<unknown>>
): Promise<HttpPaginatedResponse<unknown>> | 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<unknown>
>;
}

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<string, string>, 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<unknown>);
} else {
paginatedResource[_method as HttpMethods.Get | HttpMethods.Delete](
params,
callback as PaginatedResultCallback<unknown>
);
}
return this.rest.request(method, path, version, params, body, customHeaders, callback);
}

setLog(logOptions: LoggerOptions): void {
Expand All @@ -241,32 +132,4 @@ class BaseClient {
static PresenceMessage = PresenceMessage;
}

class Channels {
client: BaseClient;
all: Record<string, Channel>;

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;
8 changes: 6 additions & 2 deletions src/common/lib/client/baserealtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
11 changes: 10 additions & 1 deletion src/common/lib/client/baserest.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
}
8 changes: 6 additions & 2 deletions src/common/lib/client/modulesmap.ts
Original file line number Diff line number Diff line change
@@ -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 };
Loading

0 comments on commit a420d0e

Please sign in to comment.