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

[REA-1702] feat: web push #1775

Merged
merged 7 commits into from
Jul 2, 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
16 changes: 15 additions & 1 deletion Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ module.exports = function (grunt) {
});
});

grunt.registerTask('build', ['checkGitSubmodules', 'webpack:all', 'build:browser', 'build:node']);
grunt.registerTask('build', ['checkGitSubmodules', 'webpack:all', 'build:browser', 'build:node', 'build:push']);

grunt.registerTask('all', ['build', 'requirejs']);

Expand Down Expand Up @@ -122,8 +122,22 @@ module.exports = function (grunt) {
});
});

grunt.registerTask('build:push', function () {
var done = this.async();

esbuild
.build(esbuildConfig.pushPluginConfig)
.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',
'checkGitSubmodules',
'mocha:webserver',
]);
Expand Down
58 changes: 54 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,60 @@ const nextPage = await statsPage.next(); // retrieves the next page as Pa
const time = await client.time(); // time is in ms since epoch
```

### Push activation

Push activation is supported for browser clients, via the Push plugin. In order to use push activation, you must pass in the plugin via client options.

You also need to provide a path to a service worker which will be registered when the client is activated, and will handle receipt of push notifications.

```javascript
import * as Ably from 'ably';
import Push from 'ably/push';

const client = new Ably.Rest({
...options,
pushServiceWorkerUrl: '/my_service_worker.js',
plugins: { Push }
});
```

Example service worker:

```javascript
// my_service_worker.js
self.addEventListener("push", async (event) => {
const { notification } = event.data.json();
self.registration.showNotification(notification.title, notification);
});
```

To register the device to receive push notifications, you must call the `activate` method:

```javascript
await client.push.activate();
```

Once the client is activated, you can subscribe to receive push notifcations on a channel:

```javascript
const channel = client.channels.get('my_push_channel');

// Subscribe the device to receive push notifcations for a channel...
await channel.push.subscribeDevice();

// ...or subscribe all devices associated with the client's cliendId to receive notifcations from the channel
await channel.push.subscribeClient();

// When you no longer need to be subscribed to push notifcations, you can remove the subscription:
await channel.push.unsubscribeDevice();
// Or:
await channel.push.unsubscribeClient();
```

Push activation works with the [Modular variant](#modular-tree-shakable-variant) of the library, but requires you to be using the Rest plugin.

For more information on publishing push notifcations over Ably, see the [Ably push documentation](https://ably.com/docs/push).

## 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 All @@ -503,10 +557,6 @@ You can also view the [community reported Github issues](https://github.com/ably

To see what has changed in recent versions, see the [CHANGELOG](CHANGELOG.md).

## Known Limitations

This library currently does not support being the [target of a push notification](https://www.ably.com/docs/general/push/activate-subscribe) (i.e. web push).

#### Browser-specific issues

- ["Unable to parse request body" error when publishing large messages from old versions of Internet Explorer](https://support.ably.com/solution/articles/3000062360-ably-js-unable-to-parse-request-body-error-when-publishing-large-messages-from-old-browsers).
Expand Down
93 changes: 93 additions & 0 deletions ably.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,11 @@
* @defaultValue 65536
*/
maxMessageSize?: number;

/**
* A URL pointing to a service worker script which is used as the target for web push notifications.
*/
pushServiceWorkerUrl?: string;
}

/**
Expand All @@ -604,6 +609,11 @@
* A plugin capable of decoding `vcdiff`-encoded messages. For more information on how to configure a channel to use delta encoding, see the [documentation for the `@ably-forks/vcdiff-decoder` package](https://github.com/ably-forks/vcdiff-decoder#usage).
*/
vcdiff?: any;

/**
* A plugin which allows the client to be the target of push notifications.
*/
Push?: unknown;
}

/**
Expand Down Expand Up @@ -1494,6 +1504,37 @@
callback: recoverConnectionCompletionCallback,
) => void;

/**
* A standard callback format which is invoked upon completion of a task.
*
* @param err - An error object if the task failed.
* @param result - The result of the task, if any.
*/
type StandardCallback<T> = (err: ErrorInfo | null, result?: T) => void;

/**
* A function passed to {@link Push.activate} in order to override the default implementation to register a device for push activation.
*
* @param device - A DeviceDetails object representing the local device
* @param callback - A callback to be invoked when the registration is complete
*/
export type RegisterCallback = (device: DeviceDetails, callback: StandardCallback<DeviceDetails>) => void;

/**
* A function passed to {@link Push.activate} in order to override the default implementation to deregister a device for push activation.
*
* @param device - A DeviceDetails object representing the local device
* @param callback - A callback to be invoked when the deregistration is complete
*/
export type DeregisterCallback = (device: DeviceDetails, callback: StandardCallback<string>) => void;

/**
* A callback which returns only an error, or null, when complete.
*
* @param error - The error if the task failed, or null not.
*/
export type ErrorCallback = (error: ErrorInfo | null) => void;

// Internal Interfaces

// To allow a uniform (callback) interface between on and once even in the
Expand Down Expand Up @@ -1927,6 +1968,39 @@
leaveClient(clientId: string, data?: any): Promise<void>;
}

/**
* Enables devices to subscribe to push notifications for a channel.
*/
export declare interface PushChannel {
/**
* Subscribes the device to push notifications for the channel.
*/
subscribeDevice(): Promise<void>;

/**
* Unsubscribes the device from receiving push notifications for the channel.
*/
unsubscribeDevice(): Promise<void>;

/**
* Subscribes all devices associated with the current device's `clientId` to push notifications for the channel.
*/
subscribeClient(): Promise<void>;

/**
* Unsubscribes all devices associated with the current device's `clientId` from receiving push notifications for the channel.
*/
unsubscribeClient(): Promise<void>;

/**
* Retrieves all push subscriptions for the channel. Subscriptions can be filtered using a params object.
*
* @param params - An object containing key-value pairs to filter subscriptions by. Can contain `clientId`, `deviceId` or a combination of both, and a `limit` on the number of subscriptions returned, up to 1,000.
* @returns a {@link PaginatedResult} object containing an array of {@link PushChannelSubscription} objects.
*/
listSubscriptions(params?: Record<string, string>): Promise<PaginatedResult<PushChannelSubscription>>;
}

/**
* Enables messages to be published and historic messages to be retrieved for a channel.
*/
Expand All @@ -1940,6 +2014,10 @@
* A {@link Presence} object.
*/
presence: Presence;
/**
* A {@link PushChannel} object.
*/
push: PushChannel;
/**
* Retrieves a {@link PaginatedResult} object, containing an array of historical {@link InboundMessage} objects for the channel. If the channel is configured to persist messages, then messages can be retrieved from history for up to 72 hours in the past. If not, messages can only be retrieved from history for up to two minutes in the past.
*
Expand Down Expand Up @@ -2194,7 +2272,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 2275 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 Expand Up @@ -2540,6 +2618,21 @@
* A {@link PushAdmin} object.
*/
admin: PushAdmin;

/**
* Activates the device for push notifications. Subsequently registers the device with Ably and stores the deviceIdentityToken in local storage.
*
* @param registerCallback - A function passed to override the default implementation to register the local device for push activation.
* @param updateFailedCallback - A callback to be invoked when the device registration failed to update.
*/
activate(registerCallback?: RegisterCallback, updateFailedCallback?: ErrorCallback): Promise<void>;

/**
* Deactivates the device from receiving push notifications.
*
* @param deregisterCallback - A function passed to override the default implementation to deregister the local device for push activation.
*/
deactivate(deregisterCallback: DeregisterCallback): Promise<void>;
}

/**
Expand Down
9 changes: 9 additions & 0 deletions grunt/esbuild/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,18 @@ const nodeConfig = {
external: ['ws', 'got'],
};

const pushPluginConfig = {
...createBaseConfig(),
entryPoints: ['src/plugins/push/index.ts'],
plugins: [umdWrapper.default({ libraryName: 'AblyPushPlugin', amdNamedModule: false })],
outfile: 'build/push.js',
external: ['ulid'],
};

module.exports = {
webConfig,
minifiedWebConfig,
modularConfig,
nodeConfig,
pushPluginConfig,
};
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,16 @@
"./react": {
"require": "./react/cjs/index.js",
"import": "./react/mjs/index.js"
},
"./push": {
"types": "./push.d.ts",
"import": "./build/push.js"
}
},
"files": [
"build/**",
"ably.d.ts",
"push.d.ts",
"modular.d.ts",
"resources/**",
"src/**",
Expand All @@ -39,6 +44,7 @@
"@ably/msgpack-js": "^0.4.0",
"fastestsmallesttextencoderdecoder": "^1.0.22",
"got": "^11.8.5",
"ulid": "^2.3.0",
"ws": "^8.17.1"
},
"peerDependencies": {
Expand Down Expand Up @@ -130,7 +136,7 @@
"start:react": "npx vite serve",
"grunt": "grunt",
"test": "npm run test:node",
"test:node": "npm run build:node && mocha",
"test:node": "npm run build:node && npm run build:push && mocha",
"test:node:skip-build": "mocha",
"test:webserver": "grunt test:webserver",
"test:playwright": "node test/support/runPlaywrightTests.js",
Expand All @@ -143,6 +149,7 @@
"build:react": "npm run build:react:mjs && npm run build:react:cjs && cp src/platform/react-hooks/res/package.react.json react/package.json",
"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",
"requirejs": "grunt requirejs",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
Expand Down
28 changes: 28 additions & 0 deletions push.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, RestClient } from './ably';
import { BaseRest, BaseRealtime, Rest } from './modular';
/* eslint-enable no-unused-vars, @typescript-eslint/no-unused-vars */

/**
* Provides a {@link RestClient} or {@link RealtimeClient} instance with the ability to be activated as a target for push notifications.
*
* To create a client that includes this plugin, include it in the client options that you pass to the {@link RestClient.constructor} or {@link RealtimeClient.constructor}:
*
* ```javascript
* import { Realtime } from 'ably';
* import Push from 'ably/push';
* const realtime = new Realtime({ ...options, plugins: { Push } });
* ```
*
* The Push plugin can also be used with a {@link BaseRest} or {@link BaseRealtime} client, with the additional requirement that you must also use the {@link Rest} plugin
*
* ```javascript
* import { BaseRealtime, Rest, WebSocketTransport, FetchRequest } from 'ably/modular';
* import Push from 'ably/push';
* const realtime = new BaseRealtime({ ...options, plugins: { Rest, WebSocketTransport, FetchRequest, Push } });
* ```
*/
declare const Push: any;

export = Push;
Loading
Loading