Skip to content

Commit

Permalink
Merge pull request #1880 from ably/DTP-947/liveobjects-plugin-base
Browse files Browse the repository at this point in the history
[DTP-947] Expose LiveObjects as a plugin
  • Loading branch information
VeskeR authored Oct 8, 2024
2 parents 458c1ca + fac8a64 commit ac0ac6c
Show file tree
Hide file tree
Showing 18 changed files with 429 additions and 237 deletions.
26 changes: 25 additions & 1 deletion Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']);

Expand Down Expand Up @@ -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',
]);
Expand Down
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<script src="https://cdn.ably.com/lib/liveobjects.umd.min-2.js"></script>
```
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.
Expand Down
14 changes: 14 additions & 0 deletions ably.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -2010,6 +2015,11 @@ export declare interface PushChannel {
listSubscriptions(params?: Record<string, string>): Promise<PaginatedResult<PushChannelSubscription>>;
}

/**
* 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.
*/
Expand Down Expand Up @@ -2139,6 +2149,10 @@ export declare interface RealtimeChannel extends EventEmitter<channelEventCallba
* A {@link RealtimePresence} object.
*/
presence: RealtimePresence;
/**
* A {@link LiveObjects} object.
*/
liveObjects: LiveObjects;
/**
* Attach to this channel ensuring the channel is created in the Ably system and all messages published on the channel are received by any channel listeners registered using {@link RealtimeChannel.subscribe | `subscribe()`}. Any resulting channel state change will be emitted to any listeners registered using the {@link EventEmitter.on | `on()`} or {@link EventEmitter.once | `once()`} methods. As a convenience, `attach()` is called implicitly if {@link RealtimeChannel.subscribe | `subscribe()`} for the channel is called, or {@link RealtimePresence.enter | `enter()`} or {@link RealtimePresence.subscribe | `subscribe()`} are called on the {@link RealtimePresence} object for this channel.
*
Expand Down
25 changes: 25 additions & 0 deletions grunt/esbuild/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,28 @@ const minifiedPushPluginCdnConfig = {
minify: true,
};

const liveObjectsPluginConfig = {
...createBaseConfig(),
entryPoints: ['src/plugins/liveobjects/index.ts'],
plugins: [umdWrapper.default({ libraryName: 'AblyLiveObjectsPlugin', amdNamedModule: false })],
outfile: 'build/liveobjects.js',
};

const liveObjectsPluginCdnConfig = {
...createBaseConfig(),
entryPoints: ['src/plugins/liveobjects/index.ts'],
plugins: [umdWrapper.default({ libraryName: 'AblyLiveObjectsPlugin', amdNamedModule: false })],
outfile: 'build/liveobjects.umd.js',
};

const minifiedLiveObjectsPluginCdnConfig = {
...createBaseConfig(),
entryPoints: ['src/plugins/liveobjects/index.ts'],
plugins: [umdWrapper.default({ libraryName: 'AblyLiveObjectsPlugin', amdNamedModule: false })],
outfile: 'build/liveobjects.umd.min.js',
minify: true,
};

module.exports = {
webConfig,
minifiedWebConfig,
Expand All @@ -85,4 +107,7 @@ module.exports = {
pushPluginConfig,
pushPluginCdnConfig,
minifiedPushPluginCdnConfig,
liveObjectsPluginConfig,
liveObjectsPluginCdnConfig,
minifiedLiveObjectsPluginCdnConfig,
};
28 changes: 28 additions & 0 deletions liveobjects.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// The ESLint warning is triggered because we only use these types in a documentation comment.
/* eslint-disable no-unused-vars, @typescript-eslint/no-unused-vars */
import { RealtimeClient } from './ably';
import { BaseRealtime } from './modular';
/* eslint-enable no-unused-vars, @typescript-eslint/no-unused-vars */

/**
* Provides a {@link RealtimeClient} instance with the ability to use LiveObjects functionality.
*
* To create a client that includes this plugin, include it in the client options that you pass to the {@link RealtimeClient.constructor}:
*
* ```javascript
* import { Realtime } from 'ably';
* import LiveObjects from 'ably/liveobjects';
* const realtime = new Realtime({ ...options, plugins: { LiveObjects } });
* ```
*
* The LiveObjects plugin can also be used with a {@link BaseRealtime} client
*
* ```javascript
* import { BaseRealtime, WebSocketTransport, FetchRequest } from 'ably/modular';
* import LiveObjects from 'ably/liveobjects';
* const realtime = new BaseRealtime({ ...options, plugins: { WebSocketTransport, FetchRequest, LiveObjects } });
* ```
*/
declare const LiveObjects: any;

export = LiveObjects;
10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,18 @@
"./push": {
"types": "./push.d.ts",
"import": "./build/push.js"
},
"./liveobjects": {
"types": "./liveobjects.d.ts",
"import": "./build/liveobjects.js"
}
},
"files": [
"build/**",
"ably.d.ts",
"push.d.ts",
"liveobjects.d.ts",
"modular.d.ts",
"push.d.ts",
"resources/**",
"src/**",
"react/**"
Expand Down Expand Up @@ -138,7 +143,7 @@
"start:react": "npx vite serve",
"grunt": "grunt",
"test": "npm run test:node",
"test:node": "npm run build:node && npm run build:push && mocha",
"test:node": "npm run build:node && npm run build:push && npm run build:liveobjects && mocha",
"test:node:skip-build": "mocha",
"test:webserver": "grunt test:webserver",
"test:playwright": "node test/support/runPlaywrightTests.js",
Expand All @@ -152,6 +157,7 @@
"build:react:mjs": "tsc --project src/platform/react-hooks/tsconfig.mjs.json && cp src/platform/react-hooks/res/package.mjs.json react/mjs/package.json",
"build:react:cjs": "tsc --project src/platform/react-hooks/tsconfig.cjs.json && cp src/platform/react-hooks/res/package.cjs.json react/cjs/package.json",
"build:push": "grunt build:push",
"build:liveobjects": "grunt build:liveobjects",
"requirejs": "grunt requirejs",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
Expand Down
2 changes: 1 addition & 1 deletion scripts/cdn_deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ async function run() {
// Comma separated directories (relative to `path`) to exclude from upload
excludeDirs: 'node_modules,.git',
// Regex to match files against for upload
fileRegex: '^(ably|push\\.umd)?(\\.min)?\\.js$',
fileRegex: '^(ably|push\\.umd|liveobjects\\.umd)?(\\.min)?\\.js$',
...argv,
};

Expand Down
31 changes: 25 additions & 6 deletions scripts/moduleReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { gzip } from 'zlib';
import Table from 'cli-table';

// The maximum size we allow for a minimal useful Realtime bundle (i.e. one that can subscribe to a channel)
const minimalUsefulRealtimeBundleSizeThresholdsKiB = { raw: 98, gzip: 30 };
const minimalUsefulRealtimeBundleSizeThresholdsKiB = { raw: 99, gzip: 30 };

const baseClientNames = ['BaseRest', 'BaseRealtime'];

Expand Down Expand Up @@ -183,22 +183,30 @@ async function calculateAndCheckFunctionSizes(): Promise<Output> {
return output;
}

async function calculatePushPluginSize(): Promise<Output> {
async function calculatePluginSize(options: { path: string; description: string }): Promise<Output> {
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<Output> {
return calculatePluginSize({ path: './build/push.js', description: 'Push' });
}

async function calculateLiveObjectsPluginSize(): Promise<Output> {
return calculatePluginSize({ path: './build/liveobjects.js', description: 'LiveObjects' });
}

async function calculateAndCheckMinimalUsefulRealtimeBundleSize(): Promise<Output> {
const output: Output = { tableRows: [], errors: [] };

Expand Down Expand Up @@ -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<string>, thresholdBytes: number) {
const exploreResult = await runSourceMapExplorer(bundleInfo);

Expand Down Expand Up @@ -347,6 +364,7 @@ async function checkBundleFiles(bundleInfo: BundleInfo, allowedFiles: Set<string
calculateAndCheckExportSizes(),
calculateAndCheckFunctionSizes(),
calculatePushPluginSize(),
calculateLiveObjectsPluginSize(),
])
).reduce((accum, current) => ({
tableRows: [...accum.tableRows, ...current.tableRows],
Expand All @@ -355,6 +373,7 @@ async function checkBundleFiles(bundleInfo: BundleInfo, allowedFiles: Set<string

output.errors.push(...(await checkBaseRealtimeFiles()));
output.errors.push(...(await checkPushPluginFiles()));
output.errors.push(...(await checkLiveObjectsPluginFiles()));

const table = new Table({
style: { head: ['green'] },
Expand Down
2 changes: 2 additions & 0 deletions src/common/lib/client/modularplugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '../types/presencemessage';
import { TransportCtor } from '../transport/transport';
import * as PushPlugin from 'plugins/push';
import * as LiveObjectsPlugin from 'plugins/liveobjects';

export interface PresenceMessagePlugin {
presenceMessageFromValues: typeof presenceMessageFromValues;
Expand All @@ -32,6 +33,7 @@ export interface ModularPlugins {
FetchRequest?: typeof fetchRequest;
MessageInteractions?: typeof FilteredSubscriptions;
Push?: typeof PushPlugin;
LiveObjects?: typeof LiveObjectsPlugin;
}

export const allCommonModularPlugins: ModularPlugins = { Rest };
13 changes: 13 additions & 0 deletions src/common/lib/client/realtimechannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { ChannelOptions } from '../../types/channel';
import { normaliseChannelOptions } from '../util/defaults';
import { PaginatedResult } from './paginatedresource';
import type { PushChannel } from 'plugins/push';
import type { LiveObjects } from 'plugins/liveobjects';

interface RealtimeHistoryParams {
start?: number;
Expand Down Expand Up @@ -99,6 +100,7 @@ class RealtimeChannel extends EventEmitter {
retryTimer?: number | NodeJS.Timeout | null;
retryCount: number = 0;
_push?: PushChannel;
_liveObjects?: LiveObjects;

constructor(client: BaseRealtime, name: string, options?: API.ChannelOptions) {
super(client.logger);
Expand Down Expand Up @@ -137,6 +139,10 @@ class RealtimeChannel extends EventEmitter {
if (client.options.plugins?.Push) {
this._push = new client.options.plugins.Push.PushChannel(this);
}

if (client.options.plugins?.LiveObjects) {
this._liveObjects = new client.options.plugins.LiveObjects.LiveObjects(this);
}
}

get push() {
Expand All @@ -146,6 +152,13 @@ class RealtimeChannel extends EventEmitter {
return this._push;
}

get liveObjects() {
if (!this._liveObjects) {
Utils.throwMissingPluginError('LiveObjects');
}
return this._liveObjects;
}

invalidStateError(): ErrorInfo {
return new ErrorInfo(
'Channel operation failed as channel state is ' + this.state,
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import LiveObjects from './liveobjects';
import Push from './push';

export interface StandardPlugins {
LiveObjects?: typeof LiveObjects;
Push?: typeof Push;
}
7 changes: 7 additions & 0 deletions src/plugins/liveobjects/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { LiveObjects } from './liveobjects';

export { LiveObjects };

export default {
LiveObjects,
};
12 changes: 12 additions & 0 deletions src/plugins/liveobjects/liveobjects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type BaseClient from 'common/lib/client/baseclient';
import type RealtimeChannel from 'common/lib/client/realtimechannel';

export class LiveObjects {
private _client: BaseClient;
private _channel: RealtimeChannel;

constructor(channel: RealtimeChannel) {
this._channel = channel;
this._client = channel.client;
}
}
Loading

0 comments on commit ac0ac6c

Please sign in to comment.