From 5d666522d632ddbf16ce5e13ccb49ea297a3a1bb Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Tue, 1 Oct 2024 06:15:05 +0100 Subject: [PATCH] Expose LiveObjects as a plugin Base code, tests and build setup for new LiveObjects plugin. Adds a new `.liveObjects` property for RealtimeChannel. Plugin setup is based on Web Push plugin PR [1], and CDN setup for Push plugin PR [2]. Resolves DTP-947 [1] https://github.com/ably/ably-js/pull/1775 [2] https://github.com/ably/ably-js/pull/1861 --- Gruntfile.js | 26 +++++++- README.md | 35 +++++++++++ ably.d.ts | 14 +++++ grunt/esbuild/build.js | 25 ++++++++ liveobjects.d.ts | 28 +++++++++ package.json | 10 ++- scripts/cdn_deploy.js | 2 +- scripts/moduleReport.ts | 31 ++++++++-- src/common/lib/client/modularplugins.ts | 2 + src/common/lib/client/realtimechannel.ts | 13 ++++ src/plugins/index.d.ts | 2 + src/plugins/liveobjects/index.ts | 7 +++ src/plugins/liveobjects/liveobjects.ts | 12 ++++ test/common/globals/named_dependencies.js | 4 ++ test/realtime/live_objects.test.js | 75 +++++++++++++++++++++++ test/support/browser_file_list.js | 1 + 16 files changed, 277 insertions(+), 10 deletions(-) create mode 100644 liveobjects.d.ts create mode 100644 src/plugins/liveobjects/index.ts create mode 100644 src/plugins/liveobjects/liveobjects.ts create mode 100644 test/realtime/live_objects.test.js diff --git a/Gruntfile.js b/Gruntfile.js index 3bd1b0c23..fdace117d 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -73,7 +73,14 @@ module.exports = function (grunt) { }); }); - grunt.registerTask('build', ['checkGitSubmodules', 'webpack:all', 'build:browser', 'build:node', 'build:push']); + grunt.registerTask('build', [ + 'checkGitSubmodules', + 'webpack:all', + 'build:browser', + 'build:node', + 'build:push', + 'build:liveobjects', + ]); grunt.registerTask('all', ['build', 'requirejs']); @@ -138,9 +145,26 @@ module.exports = function (grunt) { }); }); + grunt.registerTask('build:liveobjects', function () { + var done = this.async(); + + Promise.all([ + esbuild.build(esbuildConfig.liveObjectsPluginConfig), + esbuild.build(esbuildConfig.liveObjectsPluginCdnConfig), + esbuild.build(esbuildConfig.minifiedLiveObjectsPluginCdnConfig), + ]) + .then(() => { + done(true); + }) + .catch((err) => { + done(err); + }); + }); + grunt.registerTask('test:webserver', 'Launch the Mocha test web server on http://localhost:3000/', [ 'build:browser', 'build:push', + 'build:liveobjects', 'checkGitSubmodules', 'mocha:webserver', ]); diff --git a/README.md b/README.md index 0f7c27796..78a2b81a2 100644 --- a/README.md +++ b/README.md @@ -586,6 +586,41 @@ The Push plugin is developed as part of the Ably client library, so it is availa For more information on publishing push notifcations over Ably, see the [Ably push documentation](https://ably.com/docs/push). +### Live Objects functionality + +Live Objects functionality is supported for Realtime clients via the LiveObjects plugin. In order to use Live Objects, you must pass in the plugin via client options. + +```javascript +import * as Ably from 'ably'; +import LiveObjects from 'ably/liveobjects'; + +const client = new Ably.Realtime({ + ...options, + plugins: { LiveObjects }, +}); +``` + +LiveObjects plugin also works with the [Modular variant](#modular-tree-shakable-variant) of the library. + +Alternatively, you can load the LiveObjects plugin directly in your HTML using `script` tag (in case you can't use a package manager): + +```html + +``` + +When loaded this way, the LiveObjects plugin will be available on the global object via the `AblyLiveObjectsPlugin` property, so you will need to pass it to the Ably instance as follows: + +```javascript +const client = new Ably.Realtime({ + ...options, + plugins: { LiveObjects: AblyLiveObjectsPlugin }, +}); +``` + +The LiveObjects plugin is developed as part of the Ably client library, so it is available for the same versions as the Ably client library itself. It also means that it follows the same semantic versioning rules as they were defined for [the Ably client library](#for-browsers). For example, to lock into a major or minor version of the LiveObjects plugin, you can specify a specific version number such as https://cdn.ably.com/lib/liveobjects.umd.min-2.js for all v2._ versions, or https://cdn.ably.com/lib/liveobjects.umd.min-2.4.js for all v2.4._ versions, or you can lock into a single release with https://cdn.ably.com/lib/liveobjects.umd.min-2.4.0.js. Note you can load the non-minified version by omitting `.min` from the URL such as https://cdn.ably.com/lib/liveobjects.umd-2.js. + +For more information about Live Objects product, see the [Ably Live Objects documentation](https://ably.com/docs/products/liveobjects). + ## Delta Plugin From version 1.2 this client library supports subscription to a stream of Vcdiff formatted delta messages from the Ably service. For certain applications this can bring significant data efficiency savings. diff --git a/ably.d.ts b/ably.d.ts index b8e85c6a4..18b7ee720 100644 --- a/ably.d.ts +++ b/ably.d.ts @@ -623,6 +623,11 @@ export interface CorePlugins { * A plugin which allows the client to be the target of push notifications. */ Push?: unknown; + + /** + * A plugin which allows the client to use LiveObjects functionality at {@link RealtimeChannel.liveObjects}. + */ + LiveObjects?: unknown; } /** @@ -2010,6 +2015,11 @@ export declare interface PushChannel { listSubscriptions(params?: Record): Promise>; } +/** + * Enables the LiveObjects state to be subscribed to for a channel. + */ +export declare interface LiveObjects {} + /** * Enables messages to be published and historic messages to be retrieved for a channel. */ @@ -2139,6 +2149,10 @@ export declare interface RealtimeChannel extends EventEmitter { return output; } -async function calculatePushPluginSize(): Promise { +async function calculatePluginSize(options: { path: string; description: string }): Promise { const output: Output = { tableRows: [], errors: [] }; - const pushPluginBundleInfo = getBundleInfo('./build/push.js'); + const pluginBundleInfo = getBundleInfo(options.path); const sizes = { - rawByteSize: pushPluginBundleInfo.byteSize, - gzipEncodedByteSize: (await promisify(gzip)(pushPluginBundleInfo.code)).byteLength, + rawByteSize: pluginBundleInfo.byteSize, + gzipEncodedByteSize: (await promisify(gzip)(pluginBundleInfo.code)).byteLength, }; output.tableRows.push({ - description: 'Push', + description: options.description, sizes: sizes, }); return output; } +async function calculatePushPluginSize(): Promise { + return calculatePluginSize({ path: './build/push.js', description: 'Push' }); +} + +async function calculateLiveObjectsPluginSize(): Promise { + return calculatePluginSize({ path: './build/liveobjects.js', description: 'LiveObjects' }); +} + async function calculateAndCheckMinimalUsefulRealtimeBundleSize(): Promise { const output: Output = { tableRows: [], errors: [] }; @@ -296,6 +304,15 @@ async function checkPushPluginFiles() { return checkBundleFiles(pushPluginBundleInfo, allowedFiles, 100); } +async function checkLiveObjectsPluginFiles() { + const pluginBundleInfo = getBundleInfo('./build/liveobjects.js'); + + // These are the files that are allowed to contribute >= `threshold` bytes to the LiveObjects bundle. + const allowedFiles = new Set(['src/plugins/liveobjects/index.ts']); + + return checkBundleFiles(pluginBundleInfo, allowedFiles, 100); +} + async function checkBundleFiles(bundleInfo: BundleInfo, allowedFiles: Set, thresholdBytes: number) { const exploreResult = await runSourceMapExplorer(bundleInfo); @@ -347,6 +364,7 @@ async function checkBundleFiles(bundleInfo: BundleInfo, allowedFiles: Set ({ tableRows: [...accum.tableRows, ...current.tableRows], @@ -355,6 +373,7 @@ async function checkBundleFiles(bundleInfo: BundleInfo, allowedFiles: Set { + /** @nospec */ + it("throws an error when attempting to access the channel's `liveObjects` property", async function () { + const helper = this.test.helper; + const client = helper.AblyRealtime(); + + await monitorConnectionThenCloseAndFinish( + helper, + async () => { + const channel = client.channels.get('channel'); + + expect(() => channel.liveObjects).to.throw('LiveObjects plugin not provided'); + }, + client, + ); + }); + }); + + describe('Realtime with LiveObjects plugin', () => { + it("returns LiveObjects instance when accessing channel's `liveObjects` property", async function () { + const helper = this.test.helper; + const client = LiveObjectsRealtime(helper); + + await monitorConnectionThenCloseAndFinish( + helper, + async () => { + const channel = client.channels.get('channel'); + + expect(channel.liveObjects.constructor.name).to.equal('LiveObjects'); + }, + client, + ); + }); + }); + }); +}); diff --git a/test/support/browser_file_list.js b/test/support/browser_file_list.js index 80d5d8d8b..d49cbee9e 100644 --- a/test/support/browser_file_list.js +++ b/test/support/browser_file_list.js @@ -39,6 +39,7 @@ window.__testFiles__.files = { 'test/realtime/failure.test.js': true, 'test/realtime/history.test.js': true, 'test/realtime/init.test.js': true, + 'test/realtime/live_objects.test.js': true, 'test/realtime/message.test.js': true, 'test/realtime/presence.test.js': true, 'test/realtime/reauth.test.js': true,