Skip to content

Commit

Permalink
Create a tree-shakable module for realtime publishing
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
lawrence-forooghian committed Nov 16, 2023
1 parent 4ae8580 commit d3fd4cb
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 78 deletions.
19 changes: 19 additions & 0 deletions modules.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,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.
*/
Expand Down Expand Up @@ -213,6 +227,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;
}

/**
Expand Down
1 change: 1 addition & 0 deletions scripts/moduleReport.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const moduleNames = [
'XHRRequest',
'FetchRequest',
'MessageInteractions',
'RealtimePublishing',
];

// List of all free-standing functions exported by the library along with the
Expand Down
10 changes: 10 additions & 0 deletions src/common/lib/client/baserealtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand All @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions src/common/lib/client/defaultrealtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -29,6 +30,7 @@ export class DefaultRealtime extends BaseRealtime {
RealtimePresence,
WebSocketTransport: initialiseWebSocketTransport,
MessageInteractions: FilteredSubscriptions,
RealtimePublishing: RealtimePublishing,
});
}

Expand Down
2 changes: 2 additions & 0 deletions src/common/lib/client/modulesmap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,6 +19,7 @@ export interface ModulesMap {
XHRRequest?: typeof XHRRequest;
FetchRequest?: typeof fetchRequest;
MessageInteractions?: typeof FilteredSubscriptions;
RealtimePublishing?: typeof RealtimePublishing;
}

export const allCommonModules: ModulesMap = { Rest };
75 changes: 3 additions & 72 deletions src/common/lib/client/realtimechannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down Expand Up @@ -232,73 +225,11 @@ class RealtimeChannel extends EventEmitter {
}

publish(...args: any[]): void | Promise<void> {
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<Message>, 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<any>): void {
Expand Down
85 changes: 85 additions & 0 deletions src/common/lib/client/realtimepublishing.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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<Message>, 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;
}
}
}
}
1 change: 1 addition & 0 deletions src/platform/web/modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
47 changes: 43 additions & 4 deletions test/browser/modules.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
FetchRequest,
XHRRequest,
MessageInteractions,
RealtimePublishing,
} from '../../build/modules/index.js';

describe('browser/modules', function () {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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();

Expand All @@ -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;
Expand All @@ -639,6 +648,7 @@ describe('browser/modules', function () {
const realtime = new BaseRealtime(ablyClientOptions(), {
WebSocketTransport,
FetchRequest,
RealtimePublishing,
MessageInteractions,
});
const channel = realtime.channels.get('channel');
Expand Down Expand Up @@ -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' });
});
});
});
});
});
Loading

0 comments on commit d3fd4cb

Please sign in to comment.