From f5a273e300520b432bdfeec925a93e11a01c753f Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 15 Nov 2023 10:03:12 -0300 Subject: [PATCH] Create a tree-shakable module for realtime publishing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owen mentioned that we have many browser use cases which only require subscriptions, and no publishing. He suggested that we create a separate tree-shakable module for this functionality. This commit introduces the API, but the bundle size savings are minimal since it only pulls out the very low-hanging fruit. I think that we could return to this at some point to see what further size savings we could achieve, but I didn’t want to spend too much time on this now. Resolves #1491. --- modules.d.ts | 19 +++++ scripts/moduleReport.js | 1 + src/common/lib/client/baserealtime.ts | 10 +++ src/common/lib/client/defaultrealtime.ts | 2 + src/common/lib/client/modulesmap.ts | 2 + src/common/lib/client/realtimechannel.ts | 75 +--------------- src/common/lib/client/realtimepublishing.ts | 85 +++++++++++++++++++ src/platform/web/modules.ts | 1 + test/browser/modules.test.js | 47 +++++++++- .../browser/template/src/index-modules.ts | 14 ++- 10 files changed, 178 insertions(+), 78 deletions(-) create mode 100644 src/common/lib/client/realtimepublishing.ts diff --git a/modules.d.ts b/modules.d.ts index 0ea246a164..e56c0b6563 100644 --- a/modules.d.ts +++ b/modules.d.ts @@ -162,6 +162,20 @@ export declare const FetchRequest: unknown; */ export declare const MessageInteractions: unknown; +/** + * Provides a {@link BaseRealtime} instance with the ability to publish messages. + * + * To create a client that includes this module, include it in the `ModulesMap` that you pass to the {@link BaseRealtime.constructor}: + * + * ```javascript + * import { BaseRealtime, WebSocketTransport, FetchRequest, RealtimePublishing } from 'ably/modules'; + * const realtime = new BaseRealtime(options, { WebSocketTransport, FetchRequest, RealtimePublishing }); + * ``` + * + * If this module is not provided, then calling {@link Types.RealtimeChannel.publish} on a channel will cause a runtime error. + */ +export declare const RealtimePublishing: unknown; + /** * Pass a `ModulesMap` to { @link BaseRest.constructor | the constructor of BaseRest } or {@link BaseRealtime.constructor | that of BaseRealtime} to specify which functionality should be made available to that client. */ @@ -215,6 +229,11 @@ export interface ModulesMap { * See {@link MessageInteractions | documentation for the `MessageInteractions` module}. */ MessageInteractions?: typeof MessageInteractions; + + /** + * See {@link RealtimePublishing | documentation for the `RealtimePublishing` module}. + */ + RealtimePublishing?: typeof RealtimePublishing; } /** diff --git a/scripts/moduleReport.js b/scripts/moduleReport.js index dd1ec550f5..31e1b35a5f 100644 --- a/scripts/moduleReport.js +++ b/scripts/moduleReport.js @@ -12,6 +12,7 @@ const moduleNames = [ 'XHRRequest', 'FetchRequest', 'MessageInteractions', + 'RealtimePublishing', ]; // List of all free-standing functions exported by the library along with the diff --git a/src/common/lib/client/baserealtime.ts b/src/common/lib/client/baserealtime.ts index 0fe9558313..47e1c54d4a 100644 --- a/src/common/lib/client/baserealtime.ts +++ b/src/common/lib/client/baserealtime.ts @@ -13,12 +13,14 @@ import { ModulesMap } from './modulesmap'; import RealtimePresence from './realtimepresence'; import { TransportNames } from 'common/constants/TransportName'; import { TransportImplementations } from 'common/platform'; +import { RealtimePublishing } from './realtimepublishing'; /** `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; + readonly __RealtimePublishing: typeof RealtimePublishing | null; // Extra transport implementations available to this client, in addition to those in Platform.Transports.bundledImplementations readonly _additionalTransportImplementations: TransportImplementations; _channels: any; @@ -29,6 +31,7 @@ class BaseRealtime extends BaseClient { Logger.logAction(Logger.LOG_MINOR, 'Realtime()', ''); this._additionalTransportImplementations = BaseRealtime.transportImplementationsFromModules(modules); this._RealtimePresence = modules.RealtimePresence ?? null; + this.__RealtimePublishing = modules.RealtimePublishing ?? null; this.connection = new Connection(this, this.options); this._channels = new Channels(this); if (options.autoConnect !== false) this.connect(); @@ -54,6 +57,13 @@ class BaseRealtime extends BaseClient { return this._channels; } + get _RealtimePublishing(): typeof RealtimePublishing { + if (!this.__RealtimePublishing) { + Utils.throwMissingModuleError('RealtimePublishing'); + } + return this.__RealtimePublishing; + } + connect(): void { Logger.logAction(Logger.LOG_MINOR, 'Realtime.connect()', ''); this.connection.connect(); diff --git a/src/common/lib/client/defaultrealtime.ts b/src/common/lib/client/defaultrealtime.ts index a4ea6c2965..235e585cb7 100644 --- a/src/common/lib/client/defaultrealtime.ts +++ b/src/common/lib/client/defaultrealtime.ts @@ -11,6 +11,7 @@ import RealtimePresence from './realtimepresence'; import { DefaultPresenceMessage } from '../types/defaultpresencemessage'; import initialiseWebSocketTransport from '../transport/websockettransport'; import { FilteredSubscriptions } from './filteredsubscriptions'; +import { RealtimePublishing } from './realtimepublishing'; /** `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. @@ -29,6 +30,7 @@ export class DefaultRealtime extends BaseRealtime { RealtimePresence, WebSocketTransport: initialiseWebSocketTransport, MessageInteractions: FilteredSubscriptions, + RealtimePublishing: RealtimePublishing, }); } diff --git a/src/common/lib/client/modulesmap.ts b/src/common/lib/client/modulesmap.ts index dab5c8b718..26aff02166 100644 --- a/src/common/lib/client/modulesmap.ts +++ b/src/common/lib/client/modulesmap.ts @@ -6,6 +6,7 @@ import { TransportInitialiser } from '../transport/connectionmanager'; import XHRRequest from 'platform/web/lib/http/request/xhrrequest'; import fetchRequest from 'platform/web/lib/http/request/fetchrequest'; import { FilteredSubscriptions } from './filteredsubscriptions'; +import { RealtimePublishing } from './realtimepublishing'; export interface ModulesMap { Rest?: typeof Rest; @@ -18,6 +19,7 @@ export interface ModulesMap { XHRRequest?: typeof XHRRequest; FetchRequest?: typeof fetchRequest; MessageInteractions?: typeof FilteredSubscriptions; + RealtimePublishing?: typeof RealtimePublishing; } export const allCommonModules: ModulesMap = { Rest }; diff --git a/src/common/lib/client/realtimechannel.ts b/src/common/lib/client/realtimechannel.ts index 6a4694690d..33cd1173c9 100644 --- a/src/common/lib/client/realtimechannel.ts +++ b/src/common/lib/client/realtimechannel.ts @@ -7,14 +7,7 @@ import EventEmitter from '../util/eventemitter'; import * as Utils from '../util/utils'; import Logger from '../util/logger'; import RealtimePresence from './realtimepresence'; -import Message, { - fromValues as messageFromValues, - fromValuesArray as messagesFromValuesArray, - encodeArray as encodeMessagesArray, - decode as decodeMessage, - getMessagesSize, - CipherOptions, -} from '../types/message'; +import Message, { decode as decodeMessage } from '../types/message'; import ChannelStateChange from './channelstatechange'; import ErrorInfo, { IPartialErrorInfo, PartialErrorInfo } from '../types/errorinfo'; import PresenceMessage, { @@ -232,73 +225,11 @@ class RealtimeChannel extends EventEmitter { } publish(...args: any[]): void | Promise { - let messages = args[0]; - let argCount = args.length; - let callback = args[argCount - 1]; - - if (typeof callback !== 'function') { - return Utils.promisify(this, 'publish', arguments); - } - if (!this.connectionManager.activeState()) { - callback(this.connectionManager.getError()); - return; - } - if (argCount == 2) { - if (Utils.isObject(messages)) messages = [messageFromValues(messages)]; - else if (Utils.isArray(messages)) messages = messagesFromValuesArray(messages); - else - throw new ErrorInfo( - 'The single-argument form of publish() expects a message object or an array of message objects', - 40013, - 400 - ); - } else { - messages = [messageFromValues({ name: args[0], data: args[1] })]; - } - const maxMessageSize = this.client.options.maxMessageSize; - encodeMessagesArray(messages, this.channelOptions as CipherOptions, (err: Error | null) => { - if (err) { - callback(err); - return; - } - /* RSL1i */ - const size = getMessagesSize(messages); - if (size > maxMessageSize) { - callback( - new ErrorInfo( - 'Maximum size of messages that can be published at once exceeded ( was ' + - size + - ' bytes; limit is ' + - maxMessageSize + - ' bytes)', - 40009, - 400 - ) - ); - return; - } - this._publish(messages, callback); - }); + return this.client._RealtimePublishing.publish(this, ...args); } _publish(messages: Array, callback: ErrCallback) { - Logger.logAction(Logger.LOG_MICRO, 'RealtimeChannel.publish()', 'message count = ' + messages.length); - const state = this.state; - switch (state) { - case 'failed': - case 'suspended': - callback(ErrorInfo.fromValues(this.invalidStateError())); - break; - default: { - Logger.logAction(Logger.LOG_MICRO, 'RealtimeChannel.publish()', 'sending message; channel state is ' + state); - const msg = new ProtocolMessage(); - msg.action = actions.MESSAGE; - msg.channel = this.name; - msg.messages = messages; - this.sendMessage(msg, callback); - break; - } - } + this.client._RealtimePublishing._publish(this, messages, callback); } onEvent(messages: Array): void { diff --git a/src/common/lib/client/realtimepublishing.ts b/src/common/lib/client/realtimepublishing.ts new file mode 100644 index 0000000000..3ba04cb410 --- /dev/null +++ b/src/common/lib/client/realtimepublishing.ts @@ -0,0 +1,85 @@ +import RealtimeChannel from './realtimechannel'; +import ProtocolMessage, { actions } from '../types/protocolmessage'; +import * as Utils from '../util/utils'; +import Logger from '../util/logger'; +import Message, { + fromValues as messageFromValues, + fromValuesArray as messagesFromValuesArray, + encodeArray as encodeMessagesArray, + getMessagesSize, + CipherOptions, +} from '../types/message'; +import ErrorInfo from '../types/errorinfo'; +import { ErrCallback } from '../../types/utils'; + +export class RealtimePublishing { + static publish(channel: RealtimeChannel, ...args: any[]): void | Promise { + let messages = args[0]; + let argCount = args.length; + let callback = args[argCount - 1]; + + if (typeof callback !== 'function') { + return Utils.promisify(this, 'publish', arguments); + } + if (!channel.connectionManager.activeState()) { + callback(channel.connectionManager.getError()); + return; + } + if (argCount == 2) { + if (Utils.isObject(messages)) messages = [messageFromValues(messages)]; + else if (Utils.isArray(messages)) messages = messagesFromValuesArray(messages); + else + throw new ErrorInfo( + 'The single-argument form of publish() expects a message object or an array of message objects', + 40013, + 400 + ); + } else { + messages = [messageFromValues({ name: args[0], data: args[1] })]; + } + const maxMessageSize = channel.client.options.maxMessageSize; + encodeMessagesArray(messages, channel.channelOptions as CipherOptions, (err: Error | null) => { + if (err) { + callback(err); + return; + } + /* RSL1i */ + const size = getMessagesSize(messages); + if (size > maxMessageSize) { + callback( + new ErrorInfo( + 'Maximum size of messages that can be published at once exceeded ( was ' + + size + + ' bytes; limit is ' + + maxMessageSize + + ' bytes)', + 40009, + 400 + ) + ); + return; + } + this._publish(channel, messages, callback); + }); + } + + static _publish(channel: RealtimeChannel, messages: Array, callback: ErrCallback) { + Logger.logAction(Logger.LOG_MICRO, 'RealtimeChannel.publish()', 'message count = ' + messages.length); + const state = channel.state; + switch (state) { + case 'failed': + case 'suspended': + callback(ErrorInfo.fromValues(channel.invalidStateError())); + break; + default: { + Logger.logAction(Logger.LOG_MICRO, 'RealtimeChannel.publish()', 'sending message; channel state is ' + state); + const msg = new ProtocolMessage(); + msg.action = actions.MESSAGE; + msg.channel = channel.name; + msg.messages = messages; + channel.sendMessage(msg, callback); + break; + } + } + } +} diff --git a/src/platform/web/modules.ts b/src/platform/web/modules.ts index 0465405a90..683dace27d 100644 --- a/src/platform/web/modules.ts +++ b/src/platform/web/modules.ts @@ -51,4 +51,5 @@ export * from './modules/transports'; export * from './modules/http'; export { Rest } from '../../common/lib/client/rest'; export { FilteredSubscriptions as MessageInteractions } from '../../common/lib/client/filteredsubscriptions'; +export { RealtimePublishing } from '../../common/lib/client/realtimepublishing'; export { BaseRest, BaseRealtime, ErrorInfo }; diff --git a/test/browser/modules.test.js b/test/browser/modules.test.js index f2ba4b77cb..9f001a234e 100644 --- a/test/browser/modules.test.js +++ b/test/browser/modules.test.js @@ -20,6 +20,7 @@ import { FetchRequest, XHRRequest, MessageInteractions, + RealtimePublishing, } from '../../build/modules/index.js'; describe('browser/modules', function () { @@ -141,7 +142,7 @@ describe('browser/modules', function () { describe('BaseRealtime without Rest', () => { it('still allows publishing and subscribing', async () => { - const client = new BaseRealtime(ablyClientOptions(), { WebSocketTransport, FetchRequest }); + const client = new BaseRealtime(ablyClientOptions(), { WebSocketTransport, FetchRequest, RealtimePublishing }); const channel = client.channels.get('channel'); await channel.attach(); @@ -368,7 +369,7 @@ describe('browser/modules', function () { for (const clientClassConfig of [ { clientClass: BaseRest }, - { clientClass: BaseRealtime, additionalModules: { WebSocketTransport } }, + { clientClass: BaseRealtime, additionalModules: { WebSocketTransport, RealtimePublishing } }, ]) { describe(clientClassConfig.clientClass.name, () => { it('is able to publish encrypted messages', async () => { @@ -606,7 +607,11 @@ describe('browser/modules', function () { describe('BaseRealtime', () => { describe('without MessageInteractions', () => { it('is able to subscribe to and unsubscribe from channel events, as long as a MessageFilter isn’t passed', async () => { - const realtime = new BaseRealtime(ablyClientOptions(), { WebSocketTransport, FetchRequest }); + const realtime = new BaseRealtime(ablyClientOptions(), { + WebSocketTransport, + FetchRequest, + RealtimePublishing, + }); const channel = realtime.channels.get('channel'); await channel.attach(); @@ -619,7 +624,11 @@ describe('browser/modules', function () { }); it('throws an error when attempting to subscribe to channel events using a MessageFilter', async () => { - const realtime = new BaseRealtime(ablyClientOptions(), { WebSocketTransport, FetchRequest }); + const realtime = new BaseRealtime(ablyClientOptions(), { + WebSocketTransport, + FetchRequest, + RealtimePublishing, + }); const channel = realtime.channels.get('channel'); let thrownError = null; @@ -639,6 +648,7 @@ describe('browser/modules', function () { const realtime = new BaseRealtime(ablyClientOptions(), { WebSocketTransport, FetchRequest, + RealtimePublishing, MessageInteractions, }); const channel = realtime.channels.get('channel'); @@ -689,4 +699,33 @@ describe('browser/modules', function () { }); }); }); + + describe('RealtimePublishing', () => { + describe('BaseRealtime', () => { + describe('without RealtimePublishing', () => { + it('throws an error when attempting to publish a message', async () => { + const realtime = new BaseRealtime(ablyClientOptions(), { + WebSocketTransport, + FetchRequest, + }); + + const channel = realtime.channels.get('channel'); + expect(() => channel.publish('message', { foo: 'bar' })).to.throw('RealtimePublishing module not provided'); + }); + }); + + describe('with RealtimePublishing', () => { + it('can publish a message', async () => { + const realtime = new BaseRealtime(ablyClientOptions(), { + WebSocketTransport, + FetchRequest, + RealtimePublishing, + }); + + const channel = realtime.channels.get('channel'); + await channel.publish('message', { foo: 'bar' }); + }); + }); + }); + }); }); diff --git a/test/package/browser/template/src/index-modules.ts b/test/package/browser/template/src/index-modules.ts index 07a7751e3d..9b28f5606f 100644 --- a/test/package/browser/template/src/index-modules.ts +++ b/test/package/browser/template/src/index-modules.ts @@ -1,4 +1,11 @@ -import { BaseRealtime, Types, WebSocketTransport, FetchRequest, generateRandomKey } from 'ably/modules'; +import { + BaseRealtime, + Types, + WebSocketTransport, + FetchRequest, + RealtimePublishing, + generateRandomKey, +} from 'ably/modules'; import { createSandboxAblyAPIKey } from './sandbox'; // This function exists to check that we can import the Types namespace and refer to its types. @@ -17,7 +24,10 @@ async function checkStandaloneFunction() { globalThis.testAblyPackage = async function () { const key = await createSandboxAblyAPIKey(); - const realtime = new BaseRealtime({ key, environment: 'sandbox' }, { WebSocketTransport, FetchRequest }); + const realtime = new BaseRealtime( + { key, environment: 'sandbox' }, + { WebSocketTransport, FetchRequest, RealtimePublishing } + ); const channel = realtime.channels.get('channel'); await attachChannel(channel);