-
Notifications
You must be signed in to change notification settings - Fork 56
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create a tree-shakable module for realtime publishing
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
1 parent
c183041
commit 71eedc2
Showing
11 changed files
with
287 additions
and
78 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
const esbuild = require('esbuild'); | ||
|
||
// List of all modules accepted in ModulesMap | ||
const moduleNames = [ | ||
'Rest', | ||
'Crypto', | ||
'MsgPack', | ||
'RealtimePresence', | ||
'XHRPolling', | ||
'XHRStreaming', | ||
'WebSocketTransport', | ||
'XHRRequest', | ||
'FetchRequest', | ||
'MessageInteractions', | ||
'RealtimePublishing', | ||
]; | ||
|
||
// List of all free-standing functions exported by the library along with the | ||
// ModulesMap entries that we expect them to transitively import | ||
const functions = [ | ||
{ name: 'generateRandomKey', transitiveImports: ['Crypto'] }, | ||
{ name: 'getDefaultCryptoParams', transitiveImports: ['Crypto'] }, | ||
{ name: 'decodeMessage', transitiveImports: [] }, | ||
{ name: 'decodeEncryptedMessage', transitiveImports: ['Crypto'] }, | ||
{ name: 'decodeMessages', transitiveImports: [] }, | ||
{ name: 'decodeEncryptedMessages', transitiveImports: ['Crypto'] }, | ||
{ name: 'decodePresenceMessage', transitiveImports: [] }, | ||
{ name: 'decodePresenceMessages', transitiveImports: [] }, | ||
{ name: 'constructPresenceMessage', transitiveImports: [] }, | ||
]; | ||
|
||
function formatBytes(bytes) { | ||
const kibibytes = bytes / 1024; | ||
const formatted = kibibytes.toFixed(2); | ||
return `${formatted} KiB`; | ||
} | ||
|
||
// Gets the bundled size in bytes of an array of named exports from 'ably/modules' | ||
function getImportSize(modules) { | ||
const outfile = modules.join(''); | ||
const result = esbuild.buildSync({ | ||
stdin: { | ||
contents: `export { ${modules.join(', ')} } from './build/modules'`, | ||
resolveDir: '.', | ||
}, | ||
metafile: true, | ||
minify: true, | ||
bundle: true, | ||
outfile, | ||
write: false, | ||
}); | ||
|
||
return result.metafile.outputs[outfile].bytes; | ||
} | ||
|
||
const errors = []; | ||
|
||
['BaseRest', 'BaseRealtime'].forEach((baseClient) => { | ||
const baseClientSize = getImportSize([baseClient]); | ||
|
||
// First display the size of the base client | ||
console.log(`${baseClient}: ${formatBytes(baseClientSize)}`); | ||
|
||
// Then display the size of each export together with the base client | ||
[...moduleNames, ...Object.values(functions).map((functionData) => functionData.name)].forEach((exportName) => { | ||
const size = getImportSize([baseClient, exportName]); | ||
console.log(`${baseClient} + ${exportName}: ${formatBytes(size)}`); | ||
|
||
if (!(baseClientSize < size) && !(baseClient === 'BaseRest' && exportName === 'Rest')) { | ||
// Emit an error if adding the module does not increase the bundle size | ||
// (this means that the module is not being tree-shaken correctly). | ||
errors.push(new Error(`Adding ${exportName} to ${baseClient} does not increase the bundle size.`)); | ||
} | ||
}); | ||
}); | ||
|
||
for (const functionData of functions) { | ||
const { name: functionName, transitiveImports } = functionData; | ||
|
||
// First display the size of the function | ||
const standaloneSize = getImportSize([functionName]); | ||
console.log(`${functionName}: ${formatBytes(standaloneSize)}`); | ||
|
||
// Then display the size of the function together with the modules we expect | ||
// it to transitively import | ||
if (transitiveImports.length > 0) { | ||
const withTransitiveImportsSize = getImportSize([functionName, ...transitiveImports]); | ||
console.log(`${functionName} + ${transitiveImports.join(' + ')}: ${formatBytes(withTransitiveImportsSize)}`); | ||
|
||
if (withTransitiveImportsSize > standaloneSize) { | ||
// Emit an error if the bundle size is increased by adding the modules | ||
// that we expect this function to have transitively imported anyway. | ||
// This seemed like a useful sense check, but it might need tweaking in | ||
// the future if we make future optimisations that mean that the | ||
// standalone functions don’t necessarily import the whole module. | ||
errors.push( | ||
new Error(`Adding ${transitiveImports.join(' + ')} to ${functionName} unexpectedly increases the bundle size.`) | ||
); | ||
} | ||
} | ||
} | ||
|
||
if (errors.length > 0) { | ||
for (const error of errors) { | ||
console.log(error.message); | ||
} | ||
process.exit(1); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.