diff --git a/scripts/moduleReport.js b/scripts/moduleReport.js index 84d6bac99b..ee5cadf0a8 100644 --- a/scripts/moduleReport.js +++ b/scripts/moduleReport.js @@ -1,7 +1,15 @@ const esbuild = require('esbuild'); // List of all modules accepted in ModulesMap -const moduleNames = ['Rest', 'Crypto', 'MsgPack', 'RealtimePresence']; +const moduleNames = [ + 'Rest', + 'Crypto', + 'MsgPack', + 'RealtimePresence', + 'XHRPolling', + 'XHRStreaming', + 'WebSocketTransport', +]; // List of all free-standing functions exported by the library along with the // ModulesMap entries that we expect them to transitively import diff --git a/src/common/constants/TransportName.ts b/src/common/constants/TransportName.ts new file mode 100644 index 0000000000..29137c7f4c --- /dev/null +++ b/src/common/constants/TransportName.ts @@ -0,0 +1,14 @@ +export namespace TransportNames { + export const WebSocket = 'web_socket' as const; + export const Comet = 'comet' as const; + export const XhrStreaming = 'xhr_streaming' as const; + export const XhrPolling = 'xhr_polling' as const; +} + +type TransportName = + | typeof TransportNames.WebSocket + | typeof TransportNames.Comet + | typeof TransportNames.XhrStreaming + | typeof TransportNames.XhrPolling; + +export default TransportName; diff --git a/src/common/constants/TransportNames.ts b/src/common/constants/TransportNames.ts deleted file mode 100644 index 9cda74c614..0000000000 --- a/src/common/constants/TransportNames.ts +++ /dev/null @@ -1,8 +0,0 @@ -enum TransportNames { - WebSocket = 'web_socket', - Comet = 'comet', - XhrStreaming = 'xhr_streaming', - XhrPolling = 'xhr_polling', -} - -export default TransportNames; diff --git a/src/common/lib/client/baserealtime.ts b/src/common/lib/client/baserealtime.ts index ff51a2a773..0fe9558313 100644 --- a/src/common/lib/client/baserealtime.ts +++ b/src/common/lib/client/baserealtime.ts @@ -11,24 +11,45 @@ import ClientOptions from '../../types/ClientOptions'; import * as API from '../../../../ably'; import { ModulesMap } from './modulesmap'; import RealtimePresence from './realtimepresence'; +import { TransportNames } from 'common/constants/TransportName'; +import { TransportImplementations } from 'common/platform'; /** `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 { readonly _RealtimePresence: typeof RealtimePresence | null; + // Extra transport implementations available to this client, in addition to those in Platform.Transports.bundledImplementations + readonly _additionalTransportImplementations: TransportImplementations; _channels: any; connection: Connection; constructor(options: ClientOptions, modules: ModulesMap) { super(options, modules); Logger.logAction(Logger.LOG_MINOR, 'Realtime()', ''); + this._additionalTransportImplementations = BaseRealtime.transportImplementationsFromModules(modules); this._RealtimePresence = modules.RealtimePresence ?? null; this.connection = new Connection(this, this.options); this._channels = new Channels(this); if (options.autoConnect !== false) this.connect(); } + private static transportImplementationsFromModules(modules: ModulesMap) { + const transports: TransportImplementations = {}; + + if (modules.WebSocketTransport) { + transports[TransportNames.WebSocket] = modules.WebSocketTransport; + } + if (modules.XHRStreaming) { + transports[TransportNames.XhrStreaming] = modules.XHRStreaming; + } + if (modules.XHRPolling) { + transports[TransportNames.XhrPolling] = modules.XHRPolling; + } + + return transports; + } + get channels() { return this._channels; } diff --git a/src/common/lib/client/defaultrealtime.ts b/src/common/lib/client/defaultrealtime.ts index 242e612288..2204ab71ac 100644 --- a/src/common/lib/client/defaultrealtime.ts +++ b/src/common/lib/client/defaultrealtime.ts @@ -9,6 +9,7 @@ import { DefaultMessage } from '../types/defaultmessage'; import { MsgPack } from 'common/types/msgpack'; import RealtimePresence from './realtimepresence'; import { DefaultPresenceMessage } from '../types/defaultpresencemessage'; +import initialiseWebSocketTransport from '../transport/websockettransport'; /** `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. @@ -20,7 +21,13 @@ export class DefaultRealtime extends BaseRealtime { throw new Error('Expected DefaultRealtime._MsgPack to have been set'); } - super(options, { ...allCommonModules, Crypto: DefaultRealtime.Crypto ?? undefined, MsgPack, RealtimePresence }); + super(options, { + ...allCommonModules, + Crypto: DefaultRealtime.Crypto ?? undefined, + MsgPack, + RealtimePresence, + WebSocketTransport: initialiseWebSocketTransport, + }); } static Utils = Utils; diff --git a/src/common/lib/client/modulesmap.ts b/src/common/lib/client/modulesmap.ts index a4de0a0d51..ada7de44ee 100644 --- a/src/common/lib/client/modulesmap.ts +++ b/src/common/lib/client/modulesmap.ts @@ -2,12 +2,16 @@ import { Rest } from './rest'; import { IUntypedCryptoStatic } from '../../types/ICryptoStatic'; import { MsgPack } from 'common/types/msgpack'; import RealtimePresence from './realtimepresence'; +import { TransportInitialiser } from '../transport/connectionmanager'; export interface ModulesMap { Rest?: typeof Rest; Crypto?: IUntypedCryptoStatic; MsgPack?: MsgPack; RealtimePresence?: typeof RealtimePresence; + WebSocketTransport?: TransportInitialiser; + XHRPolling?: TransportInitialiser; + XHRStreaming?: TransportInitialiser; } export const allCommonModules: ModulesMap = { Rest }; diff --git a/src/common/lib/transport/connectionmanager.ts b/src/common/lib/transport/connectionmanager.ts index 992fe239b6..335e2a01d4 100644 --- a/src/common/lib/transport/connectionmanager.ts +++ b/src/common/lib/transport/connectionmanager.ts @@ -2,7 +2,7 @@ import ProtocolMessage from 'common/lib/types/protocolmessage'; import * as Utils from 'common/lib/util/utils'; import Protocol, { PendingMessage } from './protocol'; import Defaults, { getAgentString } from 'common/lib/util/defaults'; -import Platform from 'common/platform'; +import Platform, { TransportImplementations } from 'common/platform'; import EventEmitter from '../util/eventemitter'; import MessageQueue from './messagequeue'; import Logger from '../util/logger'; @@ -12,14 +12,13 @@ import ErrorInfo, { IPartialErrorInfo, PartialErrorInfo } from 'common/lib/types import Auth from 'common/lib/client/auth'; import Message from 'common/lib/types/message'; import Multicaster, { MulticasterInstance } from 'common/lib/util/multicaster'; -import WebSocketTransport from './websockettransport'; import Transport, { TransportCtor } from './transport'; import * as API from '../../../../ably'; import { ErrCallback } from 'common/types/utils'; import HttpStatusCodes from 'common/constants/HttpStatusCodes'; - -type Realtime = any; -type ClientOptions = any; +import BaseRealtime from '../client/baserealtime'; +import { NormalisedClientOptions } from 'common/types/ClientOptions'; +import TransportName, { TransportNames } from 'common/constants/TransportName'; let globalObject = typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : self; @@ -91,18 +90,16 @@ type RecoveryContext = { channelSerials: { [name: string]: string }; }; -function decodeRecoveryKey(recoveryKey: string): RecoveryContext | null { +function decodeRecoveryKey(recoveryKey: NormalisedClientOptions['recover']): RecoveryContext | null { try { - return JSON.parse(recoveryKey); + return JSON.parse(recoveryKey as string); } catch (e) { return null; } } -const supportedTransports: Record = {}; - export class TransportParams { - options: ClientOptions; + options: NormalisedClientOptions; host: string | null; mode: string; format?: Utils.Format; @@ -110,7 +107,7 @@ export class TransportParams { stream?: any; heartbeats?: boolean; - constructor(options: ClientOptions, host: string | null, mode: string, connectionKey?: string) { + constructor(options: NormalisedClientOptions, host: string | null, mode: string, connectionKey?: string) { this.options = options; this.host = host; this.mode = mode; @@ -190,8 +187,9 @@ type ConnectionState = { }; class ConnectionManager extends EventEmitter { - realtime: Realtime; - options: ClientOptions; + supportedTransports: Partial> = {}; + realtime: BaseRealtime; + options: NormalisedClientOptions; states: Record; state: ConnectionState; errorReason: IPartialErrorInfo | string | null; @@ -202,9 +200,9 @@ class ConnectionManager extends EventEmitter { connectionKey?: string; connectionStateTtl: number; maxIdleInterval: number | null; - transports: string[]; - baseTransport: string; - upgradeTransports: string[]; + transports: TransportName[]; + baseTransport: TransportName; + upgradeTransports: TransportName[]; transportPreference: string | null; httpHosts: string[]; activeProtocol: null | Protocol; @@ -226,10 +224,10 @@ class ConnectionManager extends EventEmitter { queue: { message: ProtocolMessage; transport: Transport }[]; } = { isProcessing: false, queue: [] }; - constructor(realtime: Realtime, options: ClientOptions) { + constructor(realtime: BaseRealtime, options: NormalisedClientOptions) { super(); - ConnectionManager.initTransports(); this.realtime = realtime; + this.initTransports(); this.options = options; const timeouts = options.timeouts; /* connectingTimeout: leave preferenceConnectTimeout (~6s) to try the @@ -305,10 +303,7 @@ class ConnectionManager extends EventEmitter { this.connectionStateTtl = timeouts.connectionStateTtl; this.maxIdleInterval = null; - this.transports = Utils.intersect( - options.transports || Defaults.defaultTransports, - ConnectionManager.supportedTransports - ); + this.transports = Utils.intersect(options.transports || Defaults.defaultTransports, this.supportedTransports); /* baseTransports selects the leftmost transport in the Defaults.baseTransportOrder list * that's both requested and supported. */ this.baseTransport = Utils.intersect(Defaults.baseTransportOrder, this.transports)[0]; @@ -404,17 +399,32 @@ class ConnectionManager extends EventEmitter { * transport management *********************/ - static get supportedTransports() { - return supportedTransports; + // Used by tests + static supportedTransports(additionalImplementations: TransportImplementations) { + const storage: TransportStorage = { supportedTransports: {} }; + this.initTransports(additionalImplementations, storage); + return storage.supportedTransports; } - static initTransports() { - WebSocketTransport(ConnectionManager); - Utils.arrForEach(Platform.Transports, function (initFn) { - initFn(ConnectionManager); + private static initTransports(additionalImplementations: TransportImplementations, storage: TransportStorage) { + const implementations = { ...Platform.Transports.bundledImplementations, ...additionalImplementations }; + + const initialiseWebSocketTransport = implementations[TransportNames.WebSocket]; + if (initialiseWebSocketTransport) { + initialiseWebSocketTransport(storage); + } + Utils.arrForEach(Platform.Transports.order, function (transportName) { + const initFn = implementations[transportName]; + if (initFn) { + initFn(storage); + } }); } + initTransports() { + ConnectionManager.initTransports(this.realtime._additionalTransportImplementations, this); + } + createTransportParams(host: string | null, mode: string): TransportParams { return new TransportParams(this.options, host, mode, this.connectionKey); } @@ -481,11 +491,11 @@ class ConnectionManager extends EventEmitter { * @param candidate, the transport to try * @param callback */ - tryATransport(transportParams: TransportParams, candidate: string, callback: Function): void { + tryATransport(transportParams: TransportParams, candidate: TransportName, callback: Function): void { Logger.logAction(Logger.LOG_MICRO, 'ConnectionManager.tryATransport()', 'trying ' + candidate); Transport.tryConnect( - ConnectionManager.supportedTransports[candidate], + this.supportedTransports[candidate]!, this, this.realtime.auth, transportParams, @@ -600,7 +610,7 @@ class ConnectionManager extends EventEmitter { if (mode === 'recover' && this.options.recover) { /* After a successful recovery, we unpersist, as a recovery key cannot * be used more than once */ - this.options.recover = null; + delete this.options.recover; this.unpersistConnection(); } }); @@ -1191,7 +1201,8 @@ class ConnectionManager extends EventEmitter { const newState = (this.state = this.states[stateChange.current as string]); if (stateChange.reason) { this.errorReason = stateChange.reason; - this.realtime.connection.errorReason = stateChange.reason; + // TODO remove this type assertion after fixing https://github.com/ably/ably-js/issues/1405 + this.realtime.connection.errorReason = stateChange.reason as ErrorInfo; } if (newState.terminal || newState.state === 'suspended') { /* suspended is nonterminal, but once in the suspended state, realtime @@ -1673,13 +1684,13 @@ class ConnectionManager extends EventEmitter { this.tryATransport(transportParams, this.baseTransport, hostAttemptCb); } - getUpgradePossibilities(): string[] { + getUpgradePossibilities(): TransportName[] { /* returns the subset of upgradeTransports to the right of the current * transport in upgradeTransports (if it's in there - if not, currentSerial * will be -1, so return upgradeTransports.slice(0) == upgradeTransports */ const current = (this.activeProtocol as Protocol).getTransport().shortName; const currentSerial = Utils.arrIndexOf(this.upgradeTransports, current); - return this.upgradeTransports.slice(currentSerial + 1) as string[]; + return this.upgradeTransports.slice(currentSerial + 1); } upgradeIfNeeded(transportParams: Record): void { @@ -1694,7 +1705,7 @@ class ConnectionManager extends EventEmitter { return; } - Utils.arrForEach(upgradePossibilities, (upgradeTransport: string) => { + Utils.arrForEach(upgradePossibilities, (upgradeTransport: TransportName) => { /* Note: the transport may mutate the params, so give each transport a fresh one */ const upgradeTransportParams = this.createTransportParams(transportParams.host, 'upgrade'); this.tryATransport(upgradeTransportParams, upgradeTransport, noop); @@ -2097,7 +2108,7 @@ class ConnectionManager extends EventEmitter { this.proposedTransports.push(transport); } - getTransportPreference(): string { + getTransportPreference(): TransportName { return this.transportPreference || (haveWebStorage() && Platform.WebStorage?.get?.(transportPreferenceName)); } @@ -2166,3 +2177,9 @@ class ConnectionManager extends EventEmitter { } export default ConnectionManager; + +export interface TransportStorage { + supportedTransports: Partial>; +} + +export type TransportInitialiser = (transportStorage: TransportStorage) => typeof Transport; diff --git a/src/common/lib/transport/websockettransport.ts b/src/common/lib/transport/websockettransport.ts index 624c23dff5..60f2cc061a 100644 --- a/src/common/lib/transport/websockettransport.ts +++ b/src/common/lib/transport/websockettransport.ts @@ -6,10 +6,11 @@ import Logger from '../util/logger'; import ProtocolMessage from '../types/protocolmessage'; import ErrorInfo from '../types/errorinfo'; import NodeWebSocket from 'ws'; -import ConnectionManager, { TransportParams } from './connectionmanager'; +import ConnectionManager, { TransportParams, TransportStorage } from './connectionmanager'; import Auth from '../client/auth'; +import { TransportNames } from 'common/constants/TransportName'; -const shortName = 'web_socket'; +const shortName = TransportNames.WebSocket; function isNodeWebSocket(ws: WebSocket | NodeWebSocket): ws is NodeWebSocket { return !!(ws as NodeWebSocket).on; @@ -195,8 +196,8 @@ class WebSocketTransport extends Transport { } } -function initialiseTransport(connectionManager: any): typeof WebSocketTransport { - if (WebSocketTransport.isAvailable()) connectionManager.supportedTransports[shortName] = WebSocketTransport; +function initialiseTransport(transportStorage: TransportStorage): typeof WebSocketTransport { + if (WebSocketTransport.isAvailable()) transportStorage.supportedTransports[shortName] = WebSocketTransport; return WebSocketTransport; } diff --git a/src/common/lib/types/protocolmessage.ts b/src/common/lib/types/protocolmessage.ts index fde1978f9a..b721fe9e10 100644 --- a/src/common/lib/types/protocolmessage.ts +++ b/src/common/lib/types/protocolmessage.ts @@ -110,7 +110,7 @@ class ProtocolMessage { static serialize = Utils.encodeBody; - static deserialize = function (serialized: unknown, MsgPack: MsgPack, format?: Utils.Format): ProtocolMessage { + static deserialize = function (serialized: unknown, MsgPack: MsgPack | null, format?: Utils.Format): ProtocolMessage { const deserialized = Utils.decodeBody>(serialized, MsgPack, format); return ProtocolMessage.fromDeserialized(deserialized); }; diff --git a/src/common/lib/util/utils.ts b/src/common/lib/util/utils.ts index ba8fa6f370..494a4b6a3e 100644 --- a/src/common/lib/util/utils.ts +++ b/src/common/lib/util/utils.ts @@ -156,7 +156,7 @@ export function containsValue(ob: Record, val: unknown): boolea return false; } -export function intersect(arr: Array, ob: string[] | Record): string[] { +export function intersect(arr: Array, ob: K[] | Partial>): K[] { return isArray(ob) ? arrIntersect(arr, ob) : arrIntersectOb(arr, ob); } @@ -169,7 +169,7 @@ export function arrIntersect(arr1: Array, arr2: Array): Array { return result; } -export function arrIntersectOb(arr: Array, ob: Record): string[] { +export function arrIntersectOb(arr: Array, ob: Partial>): K[] { const result = []; for (let i = 0; i < arr.length; i++) { const member = arr[i]; diff --git a/src/common/platform.ts b/src/common/platform.ts index b55e625a26..609b232687 100644 --- a/src/common/platform.ts +++ b/src/common/platform.ts @@ -1,18 +1,20 @@ import { IPlatformConfig } from './types/IPlatformConfig'; import { IHttp } from './types/http'; -import ConnectionManager from './lib/transport/connectionmanager'; +import { TransportInitialiser } from './lib/transport/connectionmanager'; import IDefaults from './types/IDefaults'; import IWebStorage from './types/IWebStorage'; import IBufferUtils from './types/IBufferUtils'; -import Transport from './lib/transport/transport'; import * as WebBufferUtils from '../platform/web/lib/util/bufferutils'; import * as NodeBufferUtils from '../platform/nodejs/lib/util/bufferutils'; import { IUntypedCryptoStatic } from '../common/types/ICryptoStatic'; +import TransportName from './constants/TransportName'; type Bufferlike = WebBufferUtils.Bufferlike | NodeBufferUtils.Bufferlike; type BufferUtilsOutput = WebBufferUtils.Output | NodeBufferUtils.Output; type ToBufferOutput = WebBufferUtils.ToBufferOutput | NodeBufferUtils.ToBufferOutput; +export type TransportImplementations = Partial>; + export default class Platform { static Config: IPlatformConfig; /* @@ -30,7 +32,11 @@ export default class Platform { */ static Crypto: IUntypedCryptoStatic | null; static Http: typeof IHttp; - static Transports: Array<(connectionManager: typeof ConnectionManager) => Transport>; + static Transports: { + order: TransportName[]; + // Transport implementations that always come with this platform + bundledImplementations: TransportImplementations; + }; static Defaults: IDefaults; static WebStorage: IWebStorage | null; } diff --git a/src/common/types/IDefaults.d.ts b/src/common/types/IDefaults.d.ts index 3fb342cbc8..1fd02bf5da 100644 --- a/src/common/types/IDefaults.d.ts +++ b/src/common/types/IDefaults.d.ts @@ -1,11 +1,11 @@ -import TransportNames from '../constants/TransportNames'; +import TransportName from '../constants/TransportName'; import { RestAgentOptions } from './ClientOptions'; export default interface IDefaults { connectivityCheckUrl: string; - defaultTransports: TransportNames[]; - baseTransportOrder: TransportNames[]; - transportPreferenceOrder: TransportNames[]; - upgradeTransports: TransportNames[]; + defaultTransports: TransportName[]; + baseTransportOrder: TransportName[]; + transportPreferenceOrder: TransportName[]; + upgradeTransports: TransportName[]; restAgentOptions?: RestAgentOptions; } diff --git a/src/platform/nodejs/lib/transport/index.js b/src/platform/nodejs/lib/transport/index.js deleted file mode 100644 index 2d9be41828..0000000000 --- a/src/platform/nodejs/lib/transport/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import NodeCometTransport from './nodecomettransport'; - -export default [NodeCometTransport]; diff --git a/src/platform/nodejs/lib/transport/index.ts b/src/platform/nodejs/lib/transport/index.ts new file mode 100644 index 0000000000..80ab1d0115 --- /dev/null +++ b/src/platform/nodejs/lib/transport/index.ts @@ -0,0 +1,11 @@ +import { TransportNames } from 'common/constants/TransportName'; +import initialiseNodeCometTransport from './nodecomettransport'; +import { default as initialiseWebSocketTransport } from '../../../../common/lib/transport/websockettransport'; + +export default { + order: [TransportNames.Comet], + bundledImplementations: { + [TransportNames.WebSocket]: initialiseWebSocketTransport, + [TransportNames.Comet]: initialiseNodeCometTransport, + }, +}; diff --git a/src/platform/nodejs/lib/transport/nodecomettransport.d.ts b/src/platform/nodejs/lib/transport/nodecomettransport.d.ts new file mode 100644 index 0000000000..f90fa468b0 --- /dev/null +++ b/src/platform/nodejs/lib/transport/nodecomettransport.d.ts @@ -0,0 +1,5 @@ +import { TransportStorage } from '../../../../common/lib/transport/connectionmanager'; +import Transport from '../../../../common/lib/transport/transport'; + +declare function initialiseNodeCometTransport(transportStorage: TransportStorage): typeof Transport; +export default initialiseNodeCometTransport; diff --git a/src/platform/nodejs/lib/transport/nodecomettransport.js b/src/platform/nodejs/lib/transport/nodecomettransport.js index 7ba730eb10..9a1c29411d 100644 --- a/src/platform/nodejs/lib/transport/nodecomettransport.js +++ b/src/platform/nodejs/lib/transport/nodecomettransport.js @@ -10,10 +10,11 @@ import http from 'http'; import https from 'https'; import url from 'url'; import util from 'util'; +import { TransportNames } from '../../../../common/constants/TransportName'; -var NodeCometTransport = function (connectionManager) { +var NodeCometTransport = function (transportStorage) { var noop = function () {}; - var shortName = 'comet'; + var shortName = TransportNames.Comet; /* * A transport to use with nodejs @@ -31,7 +32,7 @@ var NodeCometTransport = function (connectionManager) { NodeCometTransport.isAvailable = function () { return true; }; - connectionManager.supportedTransports[shortName] = NodeCometTransport; + transportStorage.supportedTransports[shortName] = NodeCometTransport; NodeCometTransport.prototype.toString = function () { return ( diff --git a/src/platform/nodejs/lib/util/defaults.ts b/src/platform/nodejs/lib/util/defaults.ts index adaf8a5083..8b29953cbd 100644 --- a/src/platform/nodejs/lib/util/defaults.ts +++ b/src/platform/nodejs/lib/util/defaults.ts @@ -1,5 +1,5 @@ import IDefaults from '../../../../common/types/IDefaults'; -import TransportNames from '../../../../common/constants/TransportNames'; +import { TransportNames } from '../../../../common/constants/TransportName'; const Defaults: IDefaults = { connectivityCheckUrl: 'https://internet-up.ably-realtime.com/is-the-internet-up.txt', diff --git a/src/platform/web/lib/transport/index.js b/src/platform/web/lib/transport/index.js deleted file mode 100644 index 8fd7afb26a..0000000000 --- a/src/platform/web/lib/transport/index.js +++ /dev/null @@ -1,4 +0,0 @@ -import XHRPollingTransport from './xhrpollingtransport'; -import XHRStreamingTransport from './xhrstreamingtransport'; - -export default [XHRPollingTransport, XHRStreamingTransport]; diff --git a/src/platform/web/lib/transport/index.ts b/src/platform/web/lib/transport/index.ts new file mode 100644 index 0000000000..ced6dc4eef --- /dev/null +++ b/src/platform/web/lib/transport/index.ts @@ -0,0 +1,25 @@ +import TransportName from 'common/constants/TransportName'; +import Platform from 'common/platform'; +import initialiseXHRPollingTransport from './xhrpollingtransport'; +import initialiseXHRStreamingTransport from './xhrstreamingtransport'; +import { default as initialiseWebSocketTransport } from '../../../../common/lib/transport/websockettransport'; + +// For reasons that I don’t understand, if we use [TransportNames.XhrStreaming] and [TransportNames.XhrPolling] for the keys in defaultTransports’s, then defaultTransports does not get tree-shaken. Hence using literals instead. They’re still correctly type-checked. + +const order: TransportName[] = ['xhr_polling', 'xhr_streaming']; + +const defaultTransports: (typeof Platform)['Transports'] = { + order, + bundledImplementations: { + web_socket: initialiseWebSocketTransport, + xhr_polling: initialiseXHRPollingTransport, + xhr_streaming: initialiseXHRStreamingTransport, + }, +}; + +export default defaultTransports; + +export const ModulesTransports: (typeof Platform)['Transports'] = { + order, + bundledImplementations: {}, +}; diff --git a/src/platform/web/lib/transport/xhrpollingtransport.ts b/src/platform/web/lib/transport/xhrpollingtransport.ts index 212af4682f..4d3b2110d3 100644 --- a/src/platform/web/lib/transport/xhrpollingtransport.ts +++ b/src/platform/web/lib/transport/xhrpollingtransport.ts @@ -1,11 +1,12 @@ import Platform from '../../../../common/platform'; import CometTransport from '../../../../common/lib/transport/comettransport'; import XHRRequest from './xhrrequest'; -import ConnectionManager, { TransportParams } from 'common/lib/transport/connectionmanager'; +import ConnectionManager, { TransportParams, TransportStorage } from 'common/lib/transport/connectionmanager'; import Auth from 'common/lib/client/auth'; import { RequestParams } from 'common/types/http'; +import { TransportNames } from 'common/constants/TransportName'; -var shortName = 'xhr_polling'; +var shortName = TransportNames.XhrPolling; class XHRPollingTransport extends CometTransport { shortName = shortName; constructor(connectionManager: ConnectionManager, auth: Auth, params: TransportParams) { @@ -33,8 +34,8 @@ class XHRPollingTransport extends CometTransport { } } -function initialiseTransport(connectionManager: any): typeof XHRPollingTransport { - if (XHRPollingTransport.isAvailable()) connectionManager.supportedTransports[shortName] = XHRPollingTransport; +function initialiseTransport(transportStorage: TransportStorage): typeof XHRPollingTransport { + if (XHRPollingTransport.isAvailable()) transportStorage.supportedTransports[shortName] = XHRPollingTransport; return XHRPollingTransport; } diff --git a/src/platform/web/lib/transport/xhrstreamingtransport.ts b/src/platform/web/lib/transport/xhrstreamingtransport.ts index d2b816a650..9ef8631fec 100644 --- a/src/platform/web/lib/transport/xhrstreamingtransport.ts +++ b/src/platform/web/lib/transport/xhrstreamingtransport.ts @@ -1,11 +1,12 @@ import CometTransport from '../../../../common/lib/transport/comettransport'; import Platform from '../../../../common/platform'; import XHRRequest from './xhrrequest'; -import ConnectionManager, { TransportParams } from 'common/lib/transport/connectionmanager'; +import ConnectionManager, { TransportParams, TransportStorage } from 'common/lib/transport/connectionmanager'; import Auth from 'common/lib/client/auth'; import { RequestParams } from 'common/types/http'; +import { TransportNames } from 'common/constants/TransportName'; -const shortName = 'xhr_streaming'; +const shortName = TransportNames.XhrStreaming; class XHRStreamingTransport extends CometTransport { shortName = shortName; constructor(connectionManager: ConnectionManager, auth: Auth, params: TransportParams) { @@ -31,8 +32,8 @@ class XHRStreamingTransport extends CometTransport { } } -function initialiseTransport(connectionManager: any): typeof XHRStreamingTransport { - if (XHRStreamingTransport.isAvailable()) connectionManager.supportedTransports[shortName] = XHRStreamingTransport; +function initialiseTransport(transportStorage: TransportStorage): typeof XHRStreamingTransport { + if (XHRStreamingTransport.isAvailable()) transportStorage.supportedTransports[shortName] = XHRStreamingTransport; return XHRStreamingTransport; } diff --git a/src/platform/web/lib/util/defaults.ts b/src/platform/web/lib/util/defaults.ts index 7366ec75a4..3dbd549733 100644 --- a/src/platform/web/lib/util/defaults.ts +++ b/src/platform/web/lib/util/defaults.ts @@ -1,5 +1,5 @@ import IDefaults from 'common/types/IDefaults'; -import TransportNames from 'common/constants/TransportNames'; +import { TransportNames } from 'common/constants/TransportName'; const Defaults: IDefaults = { connectivityCheckUrl: 'https://internet-up.ably-realtime.com/is-the-internet-up.txt', diff --git a/src/platform/web/modules.ts b/src/platform/web/modules.ts index 0488f6bdf3..a3bf018afe 100644 --- a/src/platform/web/modules.ts +++ b/src/platform/web/modules.ts @@ -10,7 +10,7 @@ import BufferUtils from './lib/util/bufferutils'; import Http from './lib/util/http'; import Config from './config'; // @ts-ignore -import Transports from './lib/transport'; +import { ModulesTransports } from './lib/transport'; import Logger from '../../common/lib/util/logger'; import { getDefaults } from '../../common/lib/util/defaults'; import WebStorage from './lib/util/webstorage'; @@ -19,7 +19,7 @@ import PlatformDefaults from './lib/util/defaults'; Platform.BufferUtils = BufferUtils; Platform.Http = Http; Platform.Config = Config; -Platform.Transports = Transports; +Platform.Transports = ModulesTransports; Platform.WebStorage = WebStorage; Logger.initLogHandlers(); @@ -44,5 +44,6 @@ export * from './modules/message'; export * from './modules/presencemessage'; export * from './modules/msgpack'; export * from './modules/realtimepresence'; +export * from './modules/transports'; export { Rest } from '../../common/lib/client/rest'; export { BaseRest, BaseRealtime, ErrorInfo }; diff --git a/src/platform/web/modules/transports.ts b/src/platform/web/modules/transports.ts new file mode 100644 index 0000000000..37c0a09cf8 --- /dev/null +++ b/src/platform/web/modules/transports.ts @@ -0,0 +1,3 @@ +export { default as XHRPolling } from '../lib/transport/xhrpollingtransport'; +export { default as XHRStreaming } from '../lib/transport/xhrstreamingtransport'; +export { default as WebSocketTransport } from '../../../common/lib/transport/websockettransport'; diff --git a/test/browser/modules.test.js b/test/browser/modules.test.js index 0e0acd38a7..501bd09f9d 100644 --- a/test/browser/modules.test.js +++ b/test/browser/modules.test.js @@ -14,6 +14,9 @@ import { decodePresenceMessage, decodePresenceMessages, constructPresenceMessage, + XHRPolling, + XHRStreaming, + WebSocketTransport, } from '../../build/modules/index.js'; describe('browser/modules', function () { @@ -42,13 +45,17 @@ describe('browser/modules', function () { }); describe('without any modules', () => { - for (const clientClass of [BaseRest, BaseRealtime]) { - describe(clientClass.name, () => { - it('can be constructed', async () => { - expect(() => new clientClass(ablyClientOptions(), {})).not.to.throw(); - }); + describe('BaseRest', () => { + it('can be constructed', () => { + expect(() => new BaseRest(ablyClientOptions(), {})).not.to.throw(); }); - } + }); + + describe('BaseRealtime', () => { + it('throws an error due to absence of a transport module', () => { + expect(() => new BaseRealtime(ablyClientOptions(), {})).to.throw('no requested transports available'); + }); + }); }); describe('Rest', () => { @@ -62,7 +69,7 @@ describe('browser/modules', function () { describe('BaseRealtime with Rest', () => { it('offers REST functionality', async () => { - const client = new BaseRealtime(ablyClientOptions(), { Rest }); + const client = new BaseRealtime(ablyClientOptions(), { WebSocketTransport, Rest }); const time = await client.time(); expect(time).to.be.a('number'); }); @@ -70,7 +77,7 @@ describe('browser/modules', function () { describe('BaseRealtime without Rest', () => { it('throws an error when attempting to use REST functionality', async () => { - const client = new BaseRealtime(ablyClientOptions(), {}); + const client = new BaseRealtime(ablyClientOptions(), { WebSocketTransport }); expect(() => client.time()).to.throw('Rest module not provided'); }); }); @@ -206,48 +213,68 @@ describe('browser/modules', function () { describe('Crypto', () => { describe('without Crypto', () => { - for (const clientClass of [BaseRest, BaseRealtime]) { - describe(clientClass.name, () => { + async function testThrowsAnErrorWhenGivenChannelOptionsWithACipher(clientClassConfig) { + const client = new clientClassConfig.clientClass( + ablyClientOptions(), + clientClassConfig.additionalModules ?? {} + ); + const key = await generateRandomKey(); + expect(() => client.channels.get('channel', { cipher: { key } })).to.throw('Crypto module not provided'); + } + + for (const clientClassConfig of [ + { clientClass: BaseRest }, + { clientClass: BaseRealtime, additionalModules: { WebSocketTransport } }, + ]) { + describe(clientClassConfig.clientClass.name, () => { it('throws an error when given channel options with a cipher', async () => { - const client = new clientClass(ablyClientOptions(), {}); - const key = await generateRandomKey(); - expect(() => client.channels.get('channel', { cipher: { key } })).to.throw('Crypto module not provided'); + await testThrowsAnErrorWhenGivenChannelOptionsWithACipher(clientClassConfig); }); }); } }); describe('with Crypto', () => { - for (const clientClass of [BaseRest, BaseRealtime]) { - describe(clientClass.name, () => { - it('is able to publish encrypted messages', async () => { - const clientOptions = ablyClientOptions(); + async function testIsAbleToPublishEncryptedMessages(clientClassConfig) { + const clientOptions = ablyClientOptions(); - const key = await generateRandomKey(); + const key = await generateRandomKey(); - // Publish the message on a channel configured to use encryption, and receive it on one not configured to use encryption + // Publish the message on a channel configured to use encryption, and receive it on one not configured to use encryption - const rxClient = new BaseRealtime(clientOptions, {}); - const rxChannel = rxClient.channels.get('channel'); - await rxChannel.attach(); + const rxClient = new BaseRealtime(clientOptions, { WebSocketTransport }); + const rxChannel = rxClient.channels.get('channel'); + await rxChannel.attach(); - const rxMessagePromise = new Promise((resolve, _) => rxChannel.subscribe((message) => resolve(message))); + const rxMessagePromise = new Promise((resolve, _) => rxChannel.subscribe((message) => resolve(message))); - const encryptionChannelOptions = { cipher: { key } }; + const encryptionChannelOptions = { cipher: { key } }; - const txMessage = { name: 'message', data: 'data' }; - const txClient = new clientClass(clientOptions, { Crypto }); - const txChannel = txClient.channels.get('channel', encryptionChannelOptions); - await txChannel.publish(txMessage); + const txMessage = { name: 'message', data: 'data' }; + const txClient = new clientClassConfig.clientClass(clientOptions, { + ...(clientClassConfig.additionalModules ?? {}), + Crypto, + }); + const txChannel = txClient.channels.get('channel', encryptionChannelOptions); + await txChannel.publish(txMessage); + + const rxMessage = await rxMessagePromise; - const rxMessage = await rxMessagePromise; + // Verify that the message was published with encryption + expect(rxMessage.encoding).to.equal('utf-8/cipher+aes-256-cbc'); - // Verify that the message was published with encryption - expect(rxMessage.encoding).to.equal('utf-8/cipher+aes-256-cbc'); + // Verify that the message was correctly encrypted + const rxMessageDecrypted = await decodeEncryptedMessage(rxMessage, encryptionChannelOptions); + testMessageEquality(rxMessageDecrypted, txMessage); + } - // Verify that the message was correctly encrypted - const rxMessageDecrypted = await decodeEncryptedMessage(rxMessage, encryptionChannelOptions); - testMessageEquality(rxMessageDecrypted, txMessage); + for (const clientClassConfig of [ + { clientClass: BaseRest }, + { clientClass: BaseRealtime, additionalModules: { WebSocketTransport } }, + ]) { + describe(clientClassConfig.clientClass.name, () => { + it('is able to publish encrypted messages', async () => { + await testIsAbleToPublishEncryptedMessages(clientClassConfig); }); }); } @@ -298,7 +325,9 @@ describe('browser/modules', function () { describe('BaseRealtime', () => { it('uses JSON', async () => { - const client = new BaseRealtime(ablyClientOptions({ useBinaryProtocol: true, autoConnect: false }), {}); + const client = new BaseRealtime(ablyClientOptions({ useBinaryProtocol: true, autoConnect: false }), { + WebSocketTransport, + }); await testRealtimeUsesFormat(client, 'json'); }); }); @@ -307,7 +336,9 @@ describe('browser/modules', function () { describe('with MsgPack', () => { describe('BaseRest', () => { it('uses MessagePack', async () => { - const client = new BaseRest(ablyClientOptions({ useBinaryProtocol: true }), { MsgPack }); + const client = new BaseRest(ablyClientOptions({ useBinaryProtocol: true }), { + MsgPack, + }); await testRestUsesContentType(client, 'application/x-msgpack'); }); }); @@ -315,6 +346,7 @@ describe('browser/modules', function () { describe('BaseRealtime', () => { it('uses MessagePack', async () => { const client = new BaseRealtime(ablyClientOptions({ useBinaryProtocol: true, autoConnect: false }), { + WebSocketTransport, MsgPack, }); await testRealtimeUsesFormat(client, 'msgpack'); @@ -327,7 +359,7 @@ describe('browser/modules', function () { describe('RealtimePresence', () => { describe('BaseRealtime without RealtimePresence', () => { it('throws an error when attempting to access the `presence` property', () => { - const client = new BaseRealtime(ablyClientOptions(), {}); + const client = new BaseRealtime(ablyClientOptions(), { WebSocketTransport }); const channel = client.channels.get('channel'); expect(() => channel.presence).to.throw('RealtimePresence module not provided'); @@ -336,9 +368,12 @@ describe('browser/modules', function () { describe('BaseRealtime with RealtimePresence', () => { it('offers realtime presence functionality', async () => { - const rxChannel = new BaseRealtime(ablyClientOptions(), { RealtimePresence }).channels.get('channel'); + const rxChannel = new BaseRealtime(ablyClientOptions(), { WebSocketTransport, RealtimePresence }).channels.get( + 'channel' + ); const txClientId = randomString(); const txChannel = new BaseRealtime(ablyClientOptions({ clientId: txClientId }), { + WebSocketTransport, RealtimePresence, }).channels.get('channel'); @@ -397,4 +432,40 @@ describe('browser/modules', function () { }); }); }); + + describe('Transports', () => { + describe('BaseRealtime', () => { + for (const scenario of [ + { moduleMapKey: 'WebSocketTransport', transportModule: WebSocketTransport, transportName: 'web_socket' }, + { moduleMapKey: 'XHRPolling', transportModule: XHRPolling, transportName: 'xhr_polling' }, + { moduleMapKey: 'XHRStreaming', transportModule: XHRStreaming, transportName: 'xhr_streaming' }, + ]) { + describe(`with the ${scenario.moduleMapKey} module`, () => { + it(`is able to use the ${scenario.transportName} transport`, async () => { + const realtime = new BaseRealtime( + ablyClientOptions({ autoConnect: false, transports: [scenario.transportName] }), + { + [scenario.moduleMapKey]: scenario.transportModule, + } + ); + + let firstTransportCandidate; + const connectionManager = realtime.connection.connectionManager; + const originalTryATransport = connectionManager.tryATransport; + realtime.connection.connectionManager.tryATransport = (transportParams, candidate, callback) => { + if (!firstTransportCandidate) { + firstTransportCandidate = candidate; + } + originalTryATransport.bind(connectionManager)(transportParams, candidate, callback); + }; + + realtime.connect(); + + await realtime.connection.once('connected'); + expect(firstTransportCandidate).to.equal(scenario.transportName); + }); + }); + } + }); + }); }); diff --git a/test/browser/simple.test.js b/test/browser/simple.test.js index bfc22014d7..23e2667e17 100644 --- a/test/browser/simple.test.js +++ b/test/browser/simple.test.js @@ -17,7 +17,7 @@ define(['ably', 'shared_helper', 'chai'], function (Ably, helper, chai) { }); function isTransportAvailable(transport) { - return transport in Ably.Realtime.ConnectionManager.supportedTransports; + return transport in Ably.Realtime.ConnectionManager.supportedTransports(Ably.Realtime._transports); } function realtimeConnection(transports) { diff --git a/test/common/modules/shared_helper.js b/test/common/modules/shared_helper.js index 4d8581ab22..465ef5a74f 100644 --- a/test/common/modules/shared_helper.js +++ b/test/common/modules/shared_helper.js @@ -14,8 +14,9 @@ define([ var platform = clientModule.Ably.Realtime.Platform; var BufferUtils = platform.BufferUtils; var expect = chai.expect; - clientModule.Ably.Realtime.ConnectionManager.initTransports(); - var availableTransports = utils.keysArray(clientModule.Ably.Realtime.ConnectionManager.supportedTransports), + var availableTransports = utils.keysArray( + clientModule.Ably.Realtime.ConnectionManager.supportedTransports(clientModule.Ably.Realtime._transports) + ), bestTransport = availableTransports[0], /* IANA reserved; requests to it will hang forever */ unroutableHost = '10.255.255.1',