Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DTP-947] Expose LiveObjects as a plugin #1880

Merged
merged 2 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
* 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 @@
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 @@
* 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 Expand Up @@ -2281,7 +2295,7 @@
* and {@link ChannelOptions}, or returns the existing channel object.
*
* @experimental This is a preview feature and may change in a future non-major release.
* This experimental method allows you to create custom realtime data feeds by selectively subscribing

Check warning on line 2298 in ably.d.ts

View workflow job for this annotation

GitHub Actions / lint

Expected no lines between tags
* to receive only part of the data from the channel.
* See the [announcement post](https://pages.ably.com/subscription-filters-preview) for more information.
*
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 })],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is confusing to me, we have an artefact called liveobjects.js and another called liveobjects.umd.js but they're exactly the same. i assume this one was meant to be esm

Suggested change
plugins: [umdWrapper.default({ libraryName: 'AblyLiveObjectsPlugin', amdNamedModule: false })],

Copy link
Contributor Author

@VeskeR VeskeR Oct 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the second liveObjectsPluginCdnConfig (and third minifiedLiveObjectsPluginCdnConfig) configs are for CDN LiveObjects bundles.
the implementation is based on our CDN for Push plugin #1861.
Push plugin used *.umd.js naming convention for its CDN plugin, so I've used the same here for LiveObjects CDN plugin.
you're right that cdn and regular configs for LiveObjects are the same right now - as LiveObjects plugin doesn't have any external dependencies at this moment, but 1. that can easily change in the future, 2. we wouldn't want to change a name for a CDN script for LiveObjects in the future

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;
VeskeR marked this conversation as resolved.
Show resolved Hide resolved

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,
VeskeR marked this conversation as resolved.
Show resolved Hide resolved
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);
}
VeskeR marked this conversation as resolved.
Show resolved Hide resolved

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
Loading