Skip to content

Commit

Permalink
feat: push activation plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
owenpearson committed Jun 20, 2024
1 parent c56e25a commit 59ce31b
Show file tree
Hide file tree
Showing 22 changed files with 1,560 additions and 9 deletions.
1 change: 1 addition & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ module.exports = function (grunt) {
esbuild.build(esbuildConfig.webConfig),
esbuild.build(esbuildConfig.minifiedWebConfig),
esbuild.build(esbuildConfig.modularConfig),
esbuild.build(esbuildConfig.pushPluginConfig),
])
.then(() => {
console.log('esbuild succeeded');
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 @@ export interface ClientOptions<Plugins = CorePlugins> extends AuthOptions {
* @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 @@ export interface CorePlugins {
* 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 @@ export type recoverConnectionCallback = (
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 @@ export declare interface RealtimePresence {
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 @@ export declare interface Channel {
* 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 @@ -2540,6 +2618,21 @@ export declare interface Push {
* 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,
};
5 changes: 5 additions & 0 deletions 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 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;
2 changes: 1 addition & 1 deletion 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: 96, gzip: 29 };
const minimalUsefulRealtimeBundleSizeThresholdsKiB = { raw: 98, gzip: 30 };

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

Expand Down
22 changes: 22 additions & 0 deletions src/common/lib/client/baseclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Stats from '../types/stats';
import { Http, RequestParams } from '../../types/http';
import ClientOptions, { NormalisedClientOptions } from '../../types/ClientOptions';
import * as API from '../../../../ably';
import * as Utils from '../util/utils';

import Platform from '../../platform';
import { Rest } from './rest';
Expand All @@ -15,6 +16,7 @@ import { throwMissingPluginError } from '../util/utils';
import { MsgPack } from 'common/types/msgpack';
import { HTTPRequestImplementations } from 'platform/web/lib/http/http';
import { FilteredSubscriptions } from './filteredsubscriptions';
import type { LocalDevice } from 'plugins/push/pushactivation';

type BatchResult<T> = API.BatchResult<T>;
type BatchPublishSpec = API.BatchPublishSpec;
Expand Down Expand Up @@ -45,6 +47,7 @@ class BaseClient {
readonly _additionalHTTPRequestImplementations: HTTPRequestImplementations | null;
private readonly __FilteredSubscriptions: typeof FilteredSubscriptions | null;
readonly logger: Logger;
_device?: LocalDevice;

constructor(options: ClientOptions) {
this._additionalHTTPRequestImplementations = options.plugins ?? null;
Expand Down Expand Up @@ -119,6 +122,16 @@ class BaseClient {
return this.rest.push;
}

get device() {
if (!this.options.plugins?.Push || !this.push.LocalDevice) {
throwMissingPluginError('Push');
}
if (!this._device) {
this._device = this.push.LocalDevice.load(this);
}
return this._device;
}

baseUri(host: string) {
return Defaults.getHttpScheme(this.options) + host + ':' + Defaults.getPort(this.options, false);
}
Expand Down Expand Up @@ -157,6 +170,15 @@ class BaseClient {
}

static Platform = Platform;

/**
* These exports are for use by UMD plugins; reason being so that constructors and static methods can be accessed by these plugins without needing to import the classes directly and result in the class existing in both the plugin and the core library.
*/
Platform = Platform;
ErrorInfo = ErrorInfo;
Logger = Logger;
Defaults = Defaults;
Utils = Utils;
}

export default BaseClient;
2 changes: 2 additions & 0 deletions src/common/lib/client/modularplugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
fromValuesArray as presenceMessagesFromValuesArray,
} from '../types/presencemessage';
import { TransportCtor } from '../transport/transport';
import * as PushPlugin from 'plugins/push';

export interface PresenceMessagePlugin {
presenceMessageFromValues: typeof presenceMessageFromValues;
Expand All @@ -30,6 +31,7 @@ export interface ModularPlugins {
XHRRequest?: typeof XHRRequest;
FetchRequest?: typeof fetchRequest;
MessageInteractions?: typeof FilteredSubscriptions;
Push?: typeof PushPlugin;
}

export const allCommonModularPlugins: ModularPlugins = { Rest };
69 changes: 69 additions & 0 deletions src/common/lib/client/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,83 @@ import ErrorInfo from '../types/errorinfo';
import PushChannelSubscription from '../types/pushchannelsubscription';
import BaseClient from './baseclient';
import Defaults from '../util/defaults';
import type {
ActivationStateMachine,
DeregisterCallback,
LocalDeviceFactory,
RegisterCallback,
} from 'plugins/push/pushactivation';
import Platform from 'common/platform';
import type { ErrCallback } from 'common/types/utils';

class Push {
client: BaseClient;
admin: Admin;
stateMachine?: ActivationStateMachine;
LocalDevice?: LocalDeviceFactory;

constructor(client: BaseClient) {
this.client = client;
this.admin = new Admin(client);
if (Platform.Config.push && client.options.plugins?.Push) {
this.stateMachine = new client.options.plugins.Push.ActivationStateMachine(client);
this.LocalDevice = client.options.plugins.Push.localDeviceFactory(DeviceDetails);
}
}

async activate(registerCallback?: RegisterCallback, updateFailedCallback?: ErrCallback) {
await new Promise<void>((resolve, reject) => {
if (!this.client.options.plugins?.Push) {
reject(Utils.createMissingPluginError('Push'));
return;
}
if (!this.stateMachine) {
reject(new ErrorInfo('This platform is not supported as a target of push notifications', 40000, 400));
return;
}
if (this.stateMachine.activatedCallback) {
reject(new ErrorInfo('Activation already in progress', 40000, 400));
return;
}
this.stateMachine.activatedCallback = (err: ErrorInfo) => {
if (err) {
reject(err);
return;
}
resolve();
};
this.stateMachine.updateFailedCallback = updateFailedCallback;
this.stateMachine.handleEvent(
new this.client.options.plugins.Push.CalledActivate(this.stateMachine, registerCallback),
);
});
}

async deactivate(deregisterCallback: DeregisterCallback) {
await new Promise<void>((resolve, reject) => {
if (!this.client.options.plugins?.Push) {
reject(Utils.createMissingPluginError('Push'));
return;
}
if (!this.stateMachine) {
reject(new ErrorInfo('This platform is not supported as a target of push notifications', 40000, 400));
return;
}
if (this.stateMachine.deactivatedCallback) {
reject(new ErrorInfo('Deactivation already in progress', 40000, 400));
return;
}
this.stateMachine.deactivatedCallback = (err: ErrorInfo) => {
if (err) {
reject(err);
return;
}
resolve();
};
this.stateMachine.handleEvent(
new this.client.options.plugins.Push.CalledDeactivate(this.stateMachine, deregisterCallback),
);
});
}
}

Expand Down
Loading

0 comments on commit 59ce31b

Please sign in to comment.