diff --git a/scripts/moduleReport.js b/scripts/moduleReport.js index 1937afc34d..577371359f 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 = ['Rest', 'Crypto']; +const moduleNames = ['Rest', 'Crypto', 'MsgPack']; // 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/lib/client/auth.ts b/src/common/lib/client/auth.ts index 1a47291c08..3ed509ed6d 100644 --- a/src/common/lib/client/auth.ts +++ b/src/common/lib/client/auth.ts @@ -1078,7 +1078,7 @@ class Auth { if (this.client.options.headers) Utils.mixin(headers, this.client.options.headers); - const requestBody = Utils.encodeBody(requestBodyDTO, format); + const requestBody = Utils.encodeBody(requestBodyDTO, this.client._MsgPack, format); Resource.post( this.client, `/keys/${keyName}/revokeTokens`, @@ -1092,7 +1092,9 @@ class Auth { return; } - const batchResult = (unpacked ? body : Utils.decodeBody(body, format)) as TokenRevocationResult; + const batchResult = ( + unpacked ? body : Utils.decodeBody(body, this.client._MsgPack, format) + ) as TokenRevocationResult; callback(null, batchResult); } diff --git a/src/common/lib/client/baseclient.ts b/src/common/lib/client/baseclient.ts index da5ee730e3..2c7dc089a0 100644 --- a/src/common/lib/client/baseclient.ts +++ b/src/common/lib/client/baseclient.ts @@ -15,6 +15,7 @@ import { ModulesMap } from './modulesmap'; import { Rest } from './rest'; import { IUntypedCryptoStatic } from 'common/types/ICryptoStatic'; import { throwMissingModuleError } from '../util/utils'; +import { MsgPack } from 'common/types/msgpack'; type BatchResult = API.Types.BatchResult; type BatchPublishSpec = API.Types.BatchPublishSpec; @@ -40,6 +41,7 @@ class BaseClient { private readonly _rest: Rest | null; readonly _Crypto: IUntypedCryptoStatic | null; + readonly _MsgPack: MsgPack | null; constructor(options: ClientOptions | string, modules: ModulesMap) { if (!options) { @@ -56,7 +58,8 @@ class BaseClient { 'initialized with clientOptions ' + Platform.Config.inspect(options) ); - const normalOptions = (this.options = Defaults.normaliseOptions(optionsObj)); + this._MsgPack = modules.MsgPack ?? null; + const normalOptions = (this.options = Defaults.normaliseOptions(optionsObj, this._MsgPack)); /* process options */ if (normalOptions.key) { diff --git a/src/common/lib/client/channel.ts b/src/common/lib/client/channel.ts index 370c32993e..ad4be4b767 100644 --- a/src/common/lib/client/channel.ts +++ b/src/common/lib/client/channel.ts @@ -99,7 +99,7 @@ class Channel extends EventEmitter { headers: Record, unpacked?: boolean ) { - return await Message.fromResponseBody(body, options, unpacked ? undefined : format); + return await Message.fromResponseBody(body, options, client._MsgPack, unpacked ? undefined : format); }).get(params as Record, callback); } @@ -177,7 +177,7 @@ class Channel extends EventEmitter { return; } - this._publish(Message.serialize(messages, format), headers, params, callback); + this._publish(Message.serialize(messages, client._MsgPack, format), headers, params, callback); }); } diff --git a/src/common/lib/client/defaultrealtime.ts b/src/common/lib/client/defaultrealtime.ts index e8b66f878e..8a905b449d 100644 --- a/src/common/lib/client/defaultrealtime.ts +++ b/src/common/lib/client/defaultrealtime.ts @@ -6,13 +6,19 @@ import ConnectionManager from '../transport/connectionmanager'; import ProtocolMessage from '../types/protocolmessage'; import Platform from 'common/platform'; import { DefaultMessage } from '../types/defaultmessage'; +import { MsgPack } from 'common/types/msgpack'; /** `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. */ export class DefaultRealtime extends BaseRealtime { constructor(options: ClientOptions) { - super(options, { ...allCommonModules, Crypto: DefaultRealtime.Crypto ?? undefined }); + const MsgPack = DefaultRealtime._MsgPack; + if (!MsgPack) { + throw new Error('Expected DefaultRealtime._MsgPack to have been set'); + } + + super(options, { ...allCommonModules, Crypto: DefaultRealtime.Crypto ?? undefined, MsgPack }); } static Utils = Utils; @@ -32,4 +38,6 @@ export class DefaultRealtime extends BaseRealtime { } static Message = DefaultMessage; + + static _MsgPack: MsgPack | null = null; } diff --git a/src/common/lib/client/defaultrest.ts b/src/common/lib/client/defaultrest.ts index 1b7df607a6..e70a9eecf6 100644 --- a/src/common/lib/client/defaultrest.ts +++ b/src/common/lib/client/defaultrest.ts @@ -3,13 +3,23 @@ import ClientOptions from '../../types/ClientOptions'; import { allCommonModules } from './modulesmap'; import Platform from 'common/platform'; import { DefaultMessage } from '../types/defaultmessage'; +import { MsgPack } from 'common/types/msgpack'; /** `DefaultRest` is the class that the non tree-shakable version of the SDK exports as `Rest`. It ensures that this version of the SDK includes all of the functionality which is optionally available in the tree-shakable version. */ export class DefaultRest extends BaseRest { constructor(options: ClientOptions | string) { - super(options, { ...allCommonModules, Crypto: DefaultRest.Crypto ?? undefined }); + const MsgPack = DefaultRest._MsgPack; + if (!MsgPack) { + throw new Error('Expected DefaultRest._MsgPack to have been set'); + } + + super(options, { + ...allCommonModules, + Crypto: DefaultRest.Crypto ?? undefined, + MsgPack: DefaultRest._MsgPack ?? undefined, + }); } private static _Crypto: typeof Platform.Crypto = null; @@ -25,4 +35,6 @@ export class DefaultRest extends BaseRest { } static Message = DefaultMessage; + + static _MsgPack: MsgPack | null = null; } diff --git a/src/common/lib/client/modulesmap.ts b/src/common/lib/client/modulesmap.ts index 4133bcc3a3..12ac9bc7f6 100644 --- a/src/common/lib/client/modulesmap.ts +++ b/src/common/lib/client/modulesmap.ts @@ -1,9 +1,11 @@ import { Rest } from './rest'; import { IUntypedCryptoStatic } from '../../types/ICryptoStatic'; +import { MsgPack } from 'common/types/msgpack'; export interface ModulesMap { Rest?: typeof Rest; Crypto?: IUntypedCryptoStatic; + MsgPack?: MsgPack; } export const allCommonModules: ModulesMap = { Rest }; diff --git a/src/common/lib/client/presence.ts b/src/common/lib/client/presence.ts index ea64ad3cd7..ac756ef9b8 100644 --- a/src/common/lib/client/presence.ts +++ b/src/common/lib/client/presence.ts @@ -43,7 +43,12 @@ class Presence extends EventEmitter { headers: Record, unpacked?: boolean ) { - return await PresenceMessage.fromResponseBody(body, options as CipherOptions, unpacked ? undefined : format); + return await PresenceMessage.fromResponseBody( + body, + options as CipherOptions, + client._MsgPack, + unpacked ? undefined : format + ); }).get(params, callback); } @@ -82,7 +87,12 @@ class Presence extends EventEmitter { headers: Record, unpacked?: boolean ) { - return await PresenceMessage.fromResponseBody(body, options as CipherOptions, unpacked ? undefined : format); + return await PresenceMessage.fromResponseBody( + body, + options as CipherOptions, + client._MsgPack, + unpacked ? undefined : format + ); }).get(params, callback); } } diff --git a/src/common/lib/client/push.ts b/src/common/lib/client/push.ts index 2cf3e01d19..33054fbd0c 100644 --- a/src/common/lib/client/push.ts +++ b/src/common/lib/client/push.ts @@ -44,7 +44,7 @@ class Admin { if (client.options.pushFullWait) Utils.mixin(params, { fullWait: 'true' }); - const requestBody = Utils.encodeBody(body, format); + const requestBody = Utils.encodeBody(body, client._MsgPack, format); Resource.post(client, '/push/publish', requestBody, headers, params, null, (err) => callback(err)); } } @@ -71,7 +71,7 @@ class DeviceRegistrations { if (client.options.pushFullWait) Utils.mixin(params, { fullWait: 'true' }); - const requestBody = Utils.encodeBody(body, format); + const requestBody = Utils.encodeBody(body, client._MsgPack, format); Resource.put( client, '/push/deviceRegistrations/' + encodeURIComponent(device.id), @@ -85,6 +85,7 @@ class DeviceRegistrations { !err ? (DeviceDetails.fromResponseBody( body as Record, + client._MsgPack, unpacked ? undefined : format ) as DeviceDetails) : undefined @@ -128,6 +129,7 @@ class DeviceRegistrations { !err ? (DeviceDetails.fromResponseBody( body as Record, + client._MsgPack, unpacked ? undefined : format ) as DeviceDetails) : undefined @@ -153,7 +155,7 @@ class DeviceRegistrations { headers: Record, unpacked?: boolean ) { - return DeviceDetails.fromResponseBody(body, unpacked ? undefined : format); + return DeviceDetails.fromResponseBody(body, client._MsgPack, unpacked ? undefined : format); }).get(params, callback); } @@ -232,7 +234,7 @@ class ChannelSubscriptions { if (client.options.pushFullWait) Utils.mixin(params, { fullWait: 'true' }); - const requestBody = Utils.encodeBody(body, format); + const requestBody = Utils.encodeBody(body, client._MsgPack, format); Resource.post( client, '/push/channelSubscriptions', @@ -243,7 +245,12 @@ class ChannelSubscriptions { function (err, body, headers, unpacked) { callback( err, - !err && PushChannelSubscription.fromResponseBody(body as Record, unpacked ? undefined : format) + !err && + PushChannelSubscription.fromResponseBody( + body as Record, + client._MsgPack, + unpacked ? undefined : format + ) ); } ); @@ -266,7 +273,7 @@ class ChannelSubscriptions { headers: Record, unpacked?: boolean ) { - return PushChannelSubscription.fromResponseBody(body, unpacked ? undefined : format); + return PushChannelSubscription.fromResponseBody(body, client._MsgPack, unpacked ? undefined : format); }).get(params, callback); } @@ -308,7 +315,9 @@ class ChannelSubscriptions { headers: Record, unpacked?: boolean ) { - const parsedBody = (!unpacked && format ? Utils.decodeBody(body, format) : body) as Array; + const parsedBody = ( + !unpacked && format ? Utils.decodeBody(body, client._MsgPack, format) : body + ) as Array; for (let i = 0; i < parsedBody.length; i++) { parsedBody[i] = String(parsedBody[i]); diff --git a/src/common/lib/client/resource.ts b/src/common/lib/client/resource.ts index 125069f214..7d506d369e 100644 --- a/src/common/lib/client/resource.ts +++ b/src/common/lib/client/resource.ts @@ -6,6 +6,7 @@ import HttpMethods from '../../constants/HttpMethods'; import ErrorInfo, { IPartialErrorInfo, PartialErrorInfo } from '../types/errorinfo'; import BaseClient from './baseclient'; import { ErrnoException } from '../../types/http'; +import { MsgPack } from 'common/types/msgpack'; function withAuthDetails( client: BaseClient, @@ -27,7 +28,11 @@ function withAuthDetails( } } -function unenvelope(callback: ResourceCallback, format: Utils.Format | null): ResourceCallback { +function unenvelope( + callback: ResourceCallback, + MsgPack: MsgPack | null, + format: Utils.Format | null +): ResourceCallback { return (err, body, outerHeaders, unpacked, outerStatusCode) => { if (err && !body) { callback(err); @@ -36,7 +41,7 @@ function unenvelope(callback: ResourceCallback, format: Utils.Format | nul if (!unpacked) { try { - body = Utils.decodeBody(body, format); + body = Utils.decodeBody(body, MsgPack, format); } catch (e) { if (Utils.isErrorInfoOrPartialErrorInfo(e)) { callback(e); @@ -204,7 +209,7 @@ class Resource { } if (envelope) { - callback = callback && unenvelope(callback, envelope); + callback = callback && unenvelope(callback, client._MsgPack, envelope); (params = params || {})['envelope'] = envelope; } @@ -221,7 +226,10 @@ class Resource { let decodedBody = body; if (headers['content-type']?.indexOf('msgpack') > 0) { try { - decodedBody = Platform.Config.msgpack.decode(body as Buffer); + if (!client._MsgPack) { + Utils.throwMissingModuleError('MsgPack'); + } + decodedBody = client._MsgPack.decode(body as Buffer); } catch (decodeErr) { Logger.logAction( Logger.LOG_MICRO, diff --git a/src/common/lib/client/rest.ts b/src/common/lib/client/rest.ts index a9d11b3c3b..f58ef1b493 100644 --- a/src/common/lib/client/rest.ts +++ b/src/common/lib/client/rest.ts @@ -124,11 +124,17 @@ export class Rest { 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; + const [encoder, decoder, format] = (() => { + if (this.client.options.useBinaryProtocol) { + if (!this.client._MsgPack) { + Utils.throwMissingModuleError('MsgPack'); + } + return [this.client._MsgPack.encode, this.client._MsgPack.decode, Utils.Format.msgpack]; + } else { + return [JSON.stringify, JSON.parse, Utils.Format.json]; + } + })(); + const envelope = this.client.http.supportsLinkHeaders ? undefined : format; params = params || {}; const _method = method.toLowerCase() as HttpMethods; const headers = @@ -200,7 +206,7 @@ export class Rest { if (this.client.options.headers) Utils.mixin(headers, this.client.options.headers); - const requestBody = Utils.encodeBody(requestBodyDTO, format); + const requestBody = Utils.encodeBody(requestBodyDTO, this.client._MsgPack, format); Resource.post( this.client, '/messages', @@ -214,7 +220,9 @@ export class Rest { return; } - const batchResults = (unpacked ? body : Utils.decodeBody(body, format)) as BatchPublishResult[]; + const batchResults = ( + unpacked ? body : Utils.decodeBody(body, this.client._MsgPack, format) + ) as BatchPublishResult[]; // I don't love the below type assertions for `callback` but not sure how to avoid them if (singleSpecMode) { @@ -254,7 +262,9 @@ export class Rest { return; } - const batchResult = (unpacked ? body : Utils.decodeBody(body, format)) as BatchPresenceResult; + const batchResult = ( + unpacked ? body : Utils.decodeBody(body, this.client._MsgPack, format) + ) as BatchPresenceResult; callback(null, batchResult); } diff --git a/src/common/lib/transport/websockettransport.ts b/src/common/lib/transport/websockettransport.ts index ed805197cc..624c23dff5 100644 --- a/src/common/lib/transport/websockettransport.ts +++ b/src/common/lib/transport/websockettransport.ts @@ -102,7 +102,9 @@ class WebSocketTransport extends Transport { return; } try { - (wsConnection as NodeWebSocket).send(ProtocolMessage.serialize(message, this.params.format)); + (wsConnection as NodeWebSocket).send( + ProtocolMessage.serialize(message, this.connectionManager.realtime._MsgPack, this.params.format) + ); } catch (e) { const msg = 'Exception from ws connection when trying to send: ' + Utils.inspectError(e); Logger.logAction(Logger.LOG_ERROR, 'WebSocketTransport.send()', msg); @@ -119,7 +121,7 @@ class WebSocketTransport extends Transport { 'data received; length = ' + data.length + '; type = ' + typeof data ); try { - this.onProtocolMessage(ProtocolMessage.deserialize(data, this.format)); + this.onProtocolMessage(ProtocolMessage.deserialize(data, this.connectionManager.realtime._MsgPack, this.format)); } catch (e) { Logger.logAction( Logger.LOG_ERROR, diff --git a/src/common/lib/types/devicedetails.ts b/src/common/lib/types/devicedetails.ts index 59cc80b514..8ad7e59a0b 100644 --- a/src/common/lib/types/devicedetails.ts +++ b/src/common/lib/types/devicedetails.ts @@ -1,3 +1,4 @@ +import { MsgPack } from 'common/types/msgpack'; import * as Utils from '../util/utils'; import ErrorInfo, { IConvertibleToErrorInfo } from './errorinfo'; @@ -74,10 +75,11 @@ class DeviceDetails { static fromResponseBody( body: Array> | Record, + MsgPack: MsgPack | null, format?: Utils.Format ): DeviceDetails | DeviceDetails[] { if (format) { - body = Utils.decodeBody(body, format); + body = Utils.decodeBody(body, MsgPack, format); } if (Utils.isArray(body)) { diff --git a/src/common/lib/types/message.ts b/src/common/lib/types/message.ts index 692c5e069f..48285adae3 100644 --- a/src/common/lib/types/message.ts +++ b/src/common/lib/types/message.ts @@ -7,6 +7,7 @@ import * as Utils from '../util/utils'; import { Bufferlike as BrowserBufferlike } from '../../../platform/web/lib/util/bufferutils'; import * as API from '../../../../ably'; import { IUntypedCryptoStatic } from 'common/types/ICryptoStatic'; +import { MsgPack } from 'common/types/msgpack'; export type CipherOptions = { channelCipher: { @@ -335,10 +336,11 @@ class Message { static async fromResponseBody( body: Array, options: ChannelOptions | EncodingDecodingContext, + MsgPack: MsgPack | null, format?: Utils.Format ): Promise { if (format) { - body = Utils.decodeBody(body, format); + body = Utils.decodeBody(body, MsgPack, format); } for (let i = 0; i < body.length; i++) { diff --git a/src/common/lib/types/presencemessage.ts b/src/common/lib/types/presencemessage.ts index 2b9c04e766..da6f3cc5c5 100644 --- a/src/common/lib/types/presencemessage.ts +++ b/src/common/lib/types/presencemessage.ts @@ -3,6 +3,7 @@ import Platform from 'common/platform'; import Message, { CipherOptions } from './message'; import * as Utils from '../util/utils'; import * as API from '../../../../ably'; +import { MsgPack } from 'common/types/msgpack'; function toActionValue(actionString: string) { return PresenceMessage.Actions.indexOf(actionString); @@ -111,11 +112,12 @@ class PresenceMessage { static async fromResponseBody( body: Record[], options: CipherOptions, + MsgPack: MsgPack | null, format?: Utils.Format ): Promise { const messages: PresenceMessage[] = []; if (format) { - body = Utils.decodeBody(body, format); + body = Utils.decodeBody(body, MsgPack, format); } for (let i = 0; i < body.length; i++) { diff --git a/src/common/lib/types/protocolmessage.ts b/src/common/lib/types/protocolmessage.ts index 20acbdf380..46641b5c1c 100644 --- a/src/common/lib/types/protocolmessage.ts +++ b/src/common/lib/types/protocolmessage.ts @@ -1,3 +1,4 @@ +import { MsgPack } from 'common/types/msgpack'; import { Types } from '../../../../ably'; import * as Utils from '../util/utils'; import ErrorInfo from './errorinfo'; @@ -109,8 +110,8 @@ class ProtocolMessage { static serialize = Utils.encodeBody; - static deserialize = function (serialized: unknown, format?: Utils.Format): ProtocolMessage { - const deserialized = Utils.decodeBody>(serialized, format); + static deserialize = function (serialized: unknown, MsgPack: MsgPack, format?: Utils.Format): ProtocolMessage { + const deserialized = Utils.decodeBody>(serialized, MsgPack, format); return ProtocolMessage.fromDeserialized(deserialized); }; diff --git a/src/common/lib/types/pushchannelsubscription.ts b/src/common/lib/types/pushchannelsubscription.ts index b4056c006c..48190f25af 100644 --- a/src/common/lib/types/pushchannelsubscription.ts +++ b/src/common/lib/types/pushchannelsubscription.ts @@ -1,3 +1,4 @@ +import { MsgPack } from 'common/types/msgpack'; import * as Utils from '../util/utils'; type PushChannelSubscriptionObject = { @@ -36,10 +37,11 @@ class PushChannelSubscription { static fromResponseBody( body: Array> | Record, + MsgPack: MsgPack | null, format?: Utils.Format ): PushChannelSubscription | PushChannelSubscription[] { if (format) { - body = Utils.decodeBody(body, format) as Record; + body = Utils.decodeBody(body, MsgPack, format) as Record; } if (Utils.isArray(body)) { diff --git a/src/common/lib/util/defaults.ts b/src/common/lib/util/defaults.ts index 23d73aee98..9939c975c7 100644 --- a/src/common/lib/util/defaults.ts +++ b/src/common/lib/util/defaults.ts @@ -5,6 +5,7 @@ import ErrorInfo from 'common/lib/types/errorinfo'; import { version } from '../../../../package.json'; import ClientOptions, { InternalClientOptions, NormalisedClientOptions } from 'common/types/ClientOptions'; import IDefaults from '../../types/IDefaults'; +import { MsgPack } from 'common/types/msgpack'; let agent = 'ably-js/' + version; @@ -41,7 +42,7 @@ type CompleteDefaults = IDefaults & { checkHost(host: string): void; getRealtimeHost(options: ClientOptions, production: boolean, environment: string): string; objectifyOptions(options: ClientOptions | string): ClientOptions; - normaliseOptions(options: InternalClientOptions): NormalisedClientOptions; + normaliseOptions(options: InternalClientOptions, MsgPack: MsgPack | null): NormalisedClientOptions; defaultGetHeaders(options: NormalisedClientOptions, headersOptions?: HeadersOptions): Record; defaultPostHeaders(options: NormalisedClientOptions, headersOptions?: HeadersOptions): Record; }; @@ -185,7 +186,7 @@ export function objectifyOptions(options: ClientOptions | string): ClientOptions return options; } -export function normaliseOptions(options: InternalClientOptions): NormalisedClientOptions { +export function normaliseOptions(options: InternalClientOptions, MsgPack: MsgPack | null): NormalisedClientOptions { if (typeof options.recover === 'function' && options.closeOnUnload === true) { Logger.logAction( Logger.LOG_ERROR, @@ -222,10 +223,14 @@ export function normaliseOptions(options: InternalClientOptions): NormalisedClie const timeouts = getTimeouts(options); - if ('useBinaryProtocol' in options) { - options.useBinaryProtocol = Platform.Config.supportsBinary && options.useBinaryProtocol; + if (MsgPack) { + if ('useBinaryProtocol' in options) { + options.useBinaryProtocol = Platform.Config.supportsBinary && options.useBinaryProtocol; + } else { + options.useBinaryProtocol = Platform.Config.preferBinary; + } } else { - options.useBinaryProtocol = Platform.Config.preferBinary; + options.useBinaryProtocol = false; } const headers: Record = {}; @@ -250,10 +255,6 @@ export function normaliseOptions(options: InternalClientOptions): NormalisedClie return { ...options, - useBinaryProtocol: - 'useBinaryProtocol' in options - ? Platform.Config.supportsBinary && options.useBinaryProtocol - : Platform.Config.preferBinary, realtimeHost, restHost, maxMessageSize: options.internal?.maxMessageSize || Defaults.maxMessageSize, diff --git a/src/common/lib/util/utils.ts b/src/common/lib/util/utils.ts index 788f54d502..ba8fa6f370 100644 --- a/src/common/lib/util/utils.ts +++ b/src/common/lib/util/utils.ts @@ -1,6 +1,7 @@ import Platform from 'common/platform'; import ErrorInfo, { PartialErrorInfo } from 'common/lib/types/errorinfo'; import { ModulesMap } from '../client/modulesmap'; +import { MsgPack } from 'common/types/msgpack'; function randomPosn(arrOrStr: Array | string) { return Math.floor(Math.random() * arrOrStr.length); @@ -451,12 +452,26 @@ export function promisify(ob: Record, fnName: string, args: IArg }); } -export function decodeBody(body: unknown, format?: Format | null): T { - return format == 'msgpack' ? Platform.Config.msgpack.decode(body as Buffer) : JSON.parse(String(body)); +export function decodeBody(body: unknown, MsgPack: MsgPack | null, format?: Format | null): T { + if (format == 'msgpack') { + if (!MsgPack) { + throwMissingModuleError('MsgPack'); + } + return MsgPack.decode(body as Buffer); + } + + return JSON.parse(String(body)); } -export function encodeBody(body: unknown, format?: Format): string | Buffer { - return format == 'msgpack' ? (Platform.Config.msgpack.encode(body, true) as Buffer) : JSON.stringify(body); +export function encodeBody(body: unknown, MsgPack: MsgPack | null, format?: Format): string | Buffer { + if (format == 'msgpack') { + if (!MsgPack) { + throwMissingModuleError('MsgPack'); + } + return MsgPack.encode(body, true) as Buffer; + } + + return JSON.stringify(body); } export function allToLowerCase(arr: Array): Array { diff --git a/src/common/types/IPlatformConfig.d.ts b/src/common/types/IPlatformConfig.d.ts index ecb0e4868d..9d678f23bb 100644 --- a/src/common/types/IPlatformConfig.d.ts +++ b/src/common/types/IPlatformConfig.d.ts @@ -1,15 +1,9 @@ -interface MsgPack { - encode(value: any, sparse?: boolean): Buffer | ArrayBuffer | undefined; - decode(buffer: Buffer): any; -} - export interface IPlatformConfig { agent: string; logTimestamps: boolean; binaryType: BinaryType; WebSocket: typeof WebSocket | typeof import('ws'); useProtocolHeartbeats: boolean; - msgpack: MsgPack; supportsBinary: boolean; preferBinary: boolean; nextTick: process.nextTick; diff --git a/src/common/types/msgpack.ts b/src/common/types/msgpack.ts new file mode 100644 index 0000000000..0667def08a --- /dev/null +++ b/src/common/types/msgpack.ts @@ -0,0 +1,4 @@ +export interface MsgPack { + encode(value: any, sparse?: boolean): Buffer | ArrayBuffer | undefined; + decode(buffer: Buffer): any; +} diff --git a/src/platform/nativescript/config.js b/src/platform/nativescript/config.js index 31090e0240..ccdece4dd5 100644 --- a/src/platform/nativescript/config.js +++ b/src/platform/nativescript/config.js @@ -1,5 +1,4 @@ /* eslint-disable no-undef */ -import msgpack from '../web/lib/util/msgpack'; require('nativescript-websockets'); var randomBytes; @@ -28,7 +27,6 @@ var Config = { allowComet: true, streamingSupported: false, useProtocolHeartbeats: true, - msgpack: msgpack, supportsBinary: typeof TextDecoder !== 'undefined' && TextDecoder, preferBinary: false, ArrayBuffer: ArrayBuffer, diff --git a/src/platform/nativescript/index.ts b/src/platform/nativescript/index.ts index bdb55c8a34..f1ea2e7285 100644 --- a/src/platform/nativescript/index.ts +++ b/src/platform/nativescript/index.ts @@ -31,6 +31,7 @@ Platform.WebStorage = WebStorage; for (const clientClass of [DefaultRest, DefaultRealtime]) { clientClass.Crypto = Crypto; + clientClass._MsgPack = msgpack; } Logger.initLogHandlers(); diff --git a/src/platform/nodejs/config.ts b/src/platform/nodejs/config.ts index 716be3b12a..4947994239 100644 --- a/src/platform/nodejs/config.ts +++ b/src/platform/nodejs/config.ts @@ -10,7 +10,6 @@ const Config: IPlatformConfig = { binaryType: 'nodebuffer' as BinaryType, WebSocket, useProtocolHeartbeats: false, - msgpack: require('@ably/msgpack-js'), supportsBinary: true, preferBinary: true, nextTick: process.nextTick, diff --git a/src/platform/nodejs/index.ts b/src/platform/nodejs/index.ts index ba683f9bad..b066225b6b 100644 --- a/src/platform/nodejs/index.ts +++ b/src/platform/nodejs/index.ts @@ -15,6 +15,7 @@ import Transports from './lib/transport'; import Logger from '../../common/lib/util/logger'; import { getDefaults } from '../../common/lib/util/defaults'; import PlatformDefaults from './lib/util/defaults'; +import msgpack = require('@ably/msgpack-js'); const Crypto = createCryptoClass(BufferUtils); @@ -27,6 +28,7 @@ Platform.WebStorage = null; for (const clientClass of [DefaultRest, DefaultRealtime]) { clientClass.Crypto = Crypto; + clientClass._MsgPack = msgpack; } Logger.initLogHandlers(); diff --git a/src/platform/nodejs/lib/util/http.ts b/src/platform/nodejs/lib/util/http.ts index 322f2b6b80..7f53e13638 100644 --- a/src/platform/nodejs/lib/util/http.ts +++ b/src/platform/nodejs/lib/util/http.ts @@ -10,7 +10,7 @@ import BaseClient from 'common/lib/client/baseclient'; import BaseRealtime from 'common/lib/client/baserealtime'; import { NormalisedClientOptions, RestAgentOptions } from 'common/types/ClientOptions'; import { isSuccessCode } from 'common/constants/HttpStatusCodes'; -import { shallowEquals } from 'common/lib/util/utils'; +import { shallowEquals, throwMissingModuleError } from 'common/lib/util/utils'; /*************************************************** * @@ -28,7 +28,7 @@ import { shallowEquals } from 'common/lib/util/utils'; const globalAgentPool: Array<{ options: RestAgentOptions; agents: Agents }> = []; -const handler = function (uri: string, params: unknown, callback?: RequestCallback) { +const handler = function (uri: string, params: unknown, client: BaseClient | null, callback?: RequestCallback) { return function (err: ErrnoException | null, response?: Response, body?: unknown) { if (err) { callback?.(err); @@ -42,7 +42,10 @@ const handler = function (uri: string, params: unknown, callback?: RequestCallba body = JSON.parse(body as string); break; case 'application/x-msgpack': - body = Platform.Config.msgpack.decode(body as Buffer); + if (!client?._MsgPack) { + throwMissingModuleError('MsgPack'); + } + body = client._MsgPack.decode(body as Buffer); } const error = (body as { error: ErrorInfo }).error ? ErrorInfo.fromValues((body as { error: ErrorInfo }).error) @@ -230,14 +233,14 @@ const Http: typeof IHttp = class { (got[method](doOptions) as CancelableRequest) .then((res: Response) => { - handler(uri, params, callback)(null, res, res.body); + handler(uri, params, client, callback)(null, res, res.body); }) .catch((err: ErrnoException) => { if (err instanceof got.HTTPError) { - handler(uri, params, callback)(null, err.response, err.response.body); + handler(uri, params, client, callback)(null, err.response, err.response.body); return; } - handler(uri, params, callback)(err); + handler(uri, params, client, callback)(err); }); } diff --git a/src/platform/react-native/config.ts b/src/platform/react-native/config.ts index f6e6f29a2e..6a2f0107b8 100644 --- a/src/platform/react-native/config.ts +++ b/src/platform/react-native/config.ts @@ -1,4 +1,3 @@ -import msgpack from '../web/lib/util/msgpack'; import { IPlatformConfig } from '../../common/types/IPlatformConfig'; import BufferUtils from '../web/lib/util/bufferutils'; @@ -13,7 +12,6 @@ export default function (bufferUtils: typeof BufferUtils): IPlatformConfig { allowComet: true, streamingSupported: true, useProtocolHeartbeats: true, - msgpack: msgpack, supportsBinary: !!(typeof TextDecoder !== 'undefined' && TextDecoder), preferBinary: false, ArrayBuffer: typeof ArrayBuffer !== 'undefined' && ArrayBuffer, diff --git a/src/platform/react-native/index.ts b/src/platform/react-native/index.ts index 478ee9664d..3b7d6debeb 100644 --- a/src/platform/react-native/index.ts +++ b/src/platform/react-native/index.ts @@ -31,6 +31,7 @@ Platform.WebStorage = WebStorage; for (const clientClass of [DefaultRest, DefaultRealtime]) { clientClass.Crypto = Crypto; + clientClass._MsgPack = msgpack; } Logger.initLogHandlers(); diff --git a/src/platform/web-noencryption/index.ts b/src/platform/web-noencryption/index.ts index 80748e6b9e..4b8d3ed9de 100644 --- a/src/platform/web-noencryption/index.ts +++ b/src/platform/web-noencryption/index.ts @@ -24,6 +24,10 @@ Platform.Config = Config; Platform.Transports = Transports; Platform.WebStorage = WebStorage; +for (const clientClass of [DefaultRest, DefaultRealtime]) { + clientClass._MsgPack = msgpack; +} + Logger.initLogHandlers(); Platform.Defaults = getDefaults(PlatformDefaults); diff --git a/src/platform/web/config.ts b/src/platform/web/config.ts index 87536361ad..f3aac12c93 100644 --- a/src/platform/web/config.ts +++ b/src/platform/web/config.ts @@ -1,4 +1,3 @@ -import msgpack from './lib/util/msgpack'; import { IPlatformConfig } from '../../common/types/IPlatformConfig'; import * as Utils from 'common/lib/util/utils'; @@ -37,7 +36,6 @@ const Config: IPlatformConfig = { allowComet: allowComet(), streamingSupported: true, useProtocolHeartbeats: true, - msgpack: msgpack, supportsBinary: !!globalObject.TextDecoder, preferBinary: false, ArrayBuffer: globalObject.ArrayBuffer, diff --git a/src/platform/web/index.ts b/src/platform/web/index.ts index 9fa12989e4..d4566814d5 100644 --- a/src/platform/web/index.ts +++ b/src/platform/web/index.ts @@ -29,6 +29,7 @@ Platform.WebStorage = WebStorage; for (const clientClass of [DefaultRest, DefaultRealtime]) { clientClass.Crypto = Crypto; + clientClass._MsgPack = msgpack; } Logger.initLogHandlers(); diff --git a/src/platform/web/modules.ts b/src/platform/web/modules.ts index ed91109bfe..516e220c84 100644 --- a/src/platform/web/modules.ts +++ b/src/platform/web/modules.ts @@ -41,5 +41,6 @@ if (Platform.Config.noUpgrade) { export * from './modules/crypto'; export * from './modules/message'; +export * from './modules/msgpack'; export { Rest } from '../../common/lib/client/rest'; export { BaseRest, BaseRealtime, ErrorInfo }; diff --git a/src/platform/web/modules/msgpack.ts b/src/platform/web/modules/msgpack.ts new file mode 100644 index 0000000000..698b09e34c --- /dev/null +++ b/src/platform/web/modules/msgpack.ts @@ -0,0 +1 @@ +export { default as MsgPack } from '../lib/util/msgpack'; diff --git a/test/browser/modules.test.js b/test/browser/modules.test.js index 72aabe2c8e..6536f25f6e 100644 --- a/test/browser/modules.test.js +++ b/test/browser/modules.test.js @@ -9,6 +9,7 @@ import { decodeMessages, decodeEncryptedMessages, Crypto, + MsgPack, } from '../../build/modules/index.js'; describe('browser/modules', function () { @@ -246,4 +247,74 @@ describe('browser/modules', function () { } }); }); + + describe('MsgPack', () => { + async function testRestUsesContentType(rest, expectedContentType) { + const channelName = 'channel'; + const channel = rest.channels.get(channelName); + const contentTypeUsedForPublishPromise = new Promise((resolve, reject) => { + rest.http.do = (method, client, path, headers, body, params, callback) => { + if (!(method == 'post' && path == `/channels/${channelName}/messages`)) { + return; + } + resolve(headers['content-type']); + callback(null); + }; + }); + + await channel.publish('message', 'body'); + + const contentTypeUsedForPublish = await contentTypeUsedForPublishPromise; + expect(contentTypeUsedForPublish).to.equal(expectedContentType); + } + + async function testRealtimeUsesFormat(realtime, expectedFormat) { + const formatUsedForConnectionPromise = new Promise((resolve, reject) => { + realtime.connection.connectionManager.connectImpl = (transportParams) => { + resolve(transportParams.format); + }; + }); + realtime.connect(); + + const formatUsedForConnection = await formatUsedForConnectionPromise; + expect(formatUsedForConnection).to.equal(expectedFormat); + } + + // TODO once https://github.com/ably/ably-js/issues/1424 is fixed, this should also test the case where the useBinaryProtocol option is not specified + describe('with useBinaryProtocol client option', () => { + describe('without MsgPack', () => { + describe('BaseRest', () => { + it('uses JSON', async () => { + const client = new BaseRest(ablyClientOptions({ useBinaryProtocol: true }), {}); + await testRestUsesContentType(client, 'application/json'); + }); + }); + + describe('BaseRealtime', () => { + it('uses JSON', async () => { + const client = new BaseRealtime(ablyClientOptions({ useBinaryProtocol: true, autoConnect: false }), {}); + await testRealtimeUsesFormat(client, 'json'); + }); + }); + }); + + describe('with MsgPack', () => { + describe('BaseRest', () => { + it('uses MessagePack', async () => { + const client = new BaseRest(ablyClientOptions({ useBinaryProtocol: true }), { MsgPack }); + await testRestUsesContentType(client, 'application/x-msgpack'); + }); + }); + + describe('BaseRealtime', () => { + it('uses MessagePack', async () => { + const client = new BaseRealtime(ablyClientOptions({ useBinaryProtocol: true, autoConnect: false }), { + MsgPack, + }); + await testRealtimeUsesFormat(client, 'msgpack'); + }); + }); + }); + }); + }); }); diff --git a/test/rest/defaults.test.js b/test/rest/defaults.test.js index 40be2e7213..173dfd684e 100644 --- a/test/rest/defaults.test.js +++ b/test/rest/defaults.test.js @@ -135,6 +135,28 @@ define(['ably', 'chai'], function (Ably, chai) { Defaults.ENVIRONMENT = ''; }); + // TODO once https://github.com/ably/ably-js/issues/1424 is fixed, this should also test the case where the useBinaryProtocol option is not specified + describe('normaliseOptions with useBinaryProtocol == true', () => { + if (Ably.Realtime.Platform.Config.supportsBinary) { + describe('given MsgPack implementation', () => { + it('maintains useBinaryProtocol as true', () => { + const StubMsgPack = {}; + var normalisedOptions = Defaults.normaliseOptions({ useBinaryProtocol: true }, StubMsgPack); + + expect(normalisedOptions.useBinaryProtocol).to.be.true; + }); + }); + } + + describe('given no MsgPack implementation', () => { + it('changes useBinaryProtocol to false', () => { + var normalisedOptions = Defaults.normaliseOptions({ useBinaryProtocol: true }, null); + + expect(normalisedOptions.useBinaryProtocol).to.be.false; + }); + }); + }); + it('closeOnUnload', function () { var options;