Skip to content

Commit

Permalink
Support annotations (rest and realtime)
Browse files Browse the repository at this point in the history
  • Loading branch information
SimonWoolf committed Jan 28, 2025
1 parent 4aafc0e commit 99c7cca
Show file tree
Hide file tree
Showing 27 changed files with 874 additions and 22 deletions.
203 changes: 202 additions & 1 deletion ably.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,18 @@ export interface RestHistoryParams {
limit?: number;
}

/**
* Describes the parameters accepted by {@link RestAnnotations.get}.
*/
export interface GetAnnotationsParams {
/**
* An upper limit on the number of messages returned. The default is 100, and the maximum is 1000.
*
* @defaultValue 100
*/
limit?: number;
}

/**
* The `RestPresenceParams` interface describes the parameters accepted by {@link Presence.get}.
*/
Expand Down Expand Up @@ -2026,6 +2038,68 @@ export declare interface RealtimePresence {
leaveClient(clientId: string, data?: any): Promise<void>;
}

/**
* Functionality for annotating messages with small pieces of data, such as emoji
* reactions, that the server will roll up into the message as a summary.
*/
export declare interface RealtimeAnnotations {
/**
* Registers a listener that is called each time an {@link Annotation} matching a given refType.
* Note that if you want to receive individual realtime annotations (instead of just the rolled-up summaries), you will need to request the annotation_subscribe ChannelMode in ChannelOptions, since they are not delivered by default. In general, most clients will not bother with subscribing to individual annotations, and will instead just look at the summary updates.
*
* @param refType - A specific refType string or an array of them to register the listener for.
* @param listener - An event listener function.
* @returns A promise which resolves upon success of the channel {@link RealtimeChannel.attach | `attach()`} operation and rejects with an {@link ErrorInfo} object upon its failure.
*/
subscribe(refType: string | Array<string>, listener?: messageCallback<PresenceMessage>): Promise<void>;
/**
* Registers a listener that is called each time an {@link Annotation} is received on the channel.
* Note that if you want to receive individual realtime annotations (instead of just the rolled-up summaries), you will need to request the annotation_subscribe ChannelMode in ChannelOptions, since they are not delivered by default. In general, most clients will not bother with subscribing to individual annotations, and will instead just look at the summary updates.
*
* @param listener - An event listener function.
* @returns A promise which resolves upon success of the channel {@link RealtimeChannel.attach | `attach()`} operation and rejects with an {@link ErrorInfo} object upon its failure.
*/
subscribe(listener?: messageCallback<Annotation>): Promise<void>;
/**
* Deregisters a specific listener that is registered to receive {@link Annotation} on the channel for a given refType.
*
* @param refType - A specific refType (or array of refTypes) to deregister the listener for.
* @param listener - An event listener function.
*/
unsubscribe(refType: string | Array<string>, listener: messageCallback<Annotation>): void;
/**
* Deregisters any listener that is registered to receive {@link Annotation} on the channel for a specific refType
*
* @param refType - A specific refType (or array of refTypes) to deregister the listeners for.
*/
unsubscribe(refType: string | Array<string>): void;
/**
* Deregisters a specific listener that is registered to receive {@link Annotation} on the channel.
*
* @param listener - An event listener function.
*/
unsubscribe(listener: messageCallback<Annotation>): void;
/**
* Deregisters all listeners currently receiving {@link Annotation} for the channel.
*/
unsubscribe(): void;
/**
* Publish a new annotation for a message.
*
* @param refSerial - The `serial` of the message to annotate.
* @param refType - What type of annotation you want.
* @param data - The contents of the annotation.
*/
publish(refSerial: string, refType: string, data: string | ArrayBuffer | Uint8Array): Promise<void>;
/**
* Get all annotations for a given message (as a paginated result)
*
* @param serial - The `serial` of the message to get annotations for.
* @param params - Restrictions on which annotations to get (in particular a limit)
*/
get(serial: string, params: GetAnnotationsParams | null): Promise<void>;
}

/**
* Enables devices to subscribe to push notifications for a channel.
*/
Expand Down Expand Up @@ -2072,6 +2146,10 @@ export declare interface Channel {
* A {@link Presence} object.
*/
presence: Presence;
/**
* {@link RestAnnotations}
*/
annotations: RestAnnotations;
/**
* A {@link PushChannel} object.
*/
Expand Down Expand Up @@ -2116,6 +2194,28 @@ export declare interface Channel {
status(): Promise<ChannelDetails>;
}

/**
* Functionality for annotating messages with small pieces of data, such as emoji
* reactions, that the server will roll up into the message as a summary.
*/
export declare interface RestAnnotations {
/**
* Publish a new annotation for a message.
*
* @param refSerial - The `serial` of the message to annotate.
* @param refType - What type of annotation you want.
* @param data - The contents of the annotation.
*/
publish(refSerial: string, refType: string, data: string | ArrayBuffer | Uint8Array): Promise<void>;
/**
* Get all annotations for a given message (as a paginated result)
*
* @param serial - The `serial` of the message to get annotations for.
* @param params - Restrictions on which annotations to get (in particular a limit)
*/
get(serial: string, params: GetAnnotationsParams | null): Promise<void>;
}

/**
* Enables messages to be published and subscribed to. Also enables historic messages to be retrieved and provides access to the {@link RealtimePresence} object of a channel.
*/
Expand Down Expand Up @@ -2188,6 +2288,10 @@ export declare interface RealtimeChannel extends EventEmitter<channelEventCallba
* A {@link RealtimePresence} object.
*/
presence: RealtimePresence;
/**
* {@link RealtimeAnnotations}
*/
annotations: RealtimeAnnotations;
/**
* 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 @@ -2417,6 +2521,55 @@ export interface Message {
* update or delete operation.
*/
operation?: Operation;
/**
* A summary of all the annotations that have been made to the message. Will always be
* populated for a message.summary, and may be populated for any other type (in
* particular a message retrieved from REST history will have its latest summary
* included).
*/
summary?: any; // TODO improve typings after summary structure finalised
}

/**
* An annotation to a message, received from Ably
*/
export interface Annotation {
/**
* Unique ID assigned by Ably to this annotation.
*/
id: string;
/**
* The client ID of the publisher of this annotation (if any).
*/
clientId?: string;
/**
* The annotation payload, if provided.
*/
data?: any;
/**
* This is typically empty, as all annotations received from Ably are automatically decoded client-side using this value. However, if the annotation encoding cannot be processed, this attribute contains the remaining transformations not applied to the `data` payload.
*/
encoding?: string;
/**
* Timestamp of when the annotation was received by Ably, as milliseconds since the Unix epoch.
*/
timestamp: number;
/**
* The action, whether this is an annotation being added or removed, one of the {@link AnnotationAction} enum values.
*/
action: AnnotationAction;
/**
* This message's unique serial (lexicographically totally ordered).
*/
serial: string;
/**
* The serial of the message (of type message.create) that this annotation is annotating.
*/
refSerial: string;
/**
* The kind of annotation it is (for example, an emoji reaction)
*/
refType: string;
}

/**
Expand Down Expand Up @@ -2459,7 +2612,8 @@ declare namespace MessageActions {
type META_OCCUPANCY = 'meta.occupancy';
/**
* Message action for a message containing the latest rolled-up summary of annotations
* that have been made to this message.
* that have been made to this message. The message.refSerial is the serial of the
* message for which this is a summary.
*/
type MESSAGE_SUMMARY = 'message.summary';
}
Expand All @@ -2474,6 +2628,25 @@ export type MessageAction =
| MessageActions.META_OCCUPANCY
| MessageActions.MESSAGE_SUMMARY;

/**
* The namespace containing the different types of annotation actions.
*/
declare namespace AnnotationActions {
/**
* Annotation action for a created annotation.
*/
type ANNOTATION_CREATE = 'annotation.create';
/**
* Annotation action for a deleted annotation.
*/
type ANNOTATION_DELETE = 'annotation.delete';
}

/**
* The possible values of the 'action' field of an {@link Annotation}.
*/
export type AnnotationAction = AnnotationActions.ANNOTATION_CREATE | AnnotationActions.ANNOTATION_DELETE;

/**
* A message received from Ably.
*/
Expand Down Expand Up @@ -2566,6 +2739,26 @@ export interface PresenceMessageStatic {
fromValues(values: Partial<Pick<PresenceMessage, 'clientId' | 'data' | 'extras'>>): PresenceMessage;
}

/**
* Static utilities related to annotations.
*/
export interface AnnotationStatic {
/**
* Decodes and decrypts a deserialized `Annotation`-like object using the cipher in {@link ChannelOptions}. Any residual transforms that cannot be decoded or decrypted will be in the `encoding` property. Intended for users receiving messages from a source other than a REST or Realtime channel (for example a queue) to avoid having to parse the encoding string and action.
*
* @param JsonObject - The deserialized `Annotation`-like object to decode and decrypt.
* @param channelOptions - A {@link ChannelOptions} object containing the cipher.
*/
fromEncoded: (JsonObject: any, channelOptions?: ChannelOptions) => Promise<Annotation>;
/**
* Decodes and decrypts an array of deserialized `Annotation`-like object using the cipher in {@link ChannelOptions}. Any residual transforms that cannot be decoded or decrypted will be in the `encoding` property. Intended for users receiving messages from a source other than a REST or Realtime channel (for example a queue) to avoid having to parse the encoding string.
*
* @param JsonArray - An array of deserialized `Annotation`-like objects to decode and decrypt.
* @param channelOptions - A {@link ChannelOptions} object containing the cipher.
*/
fromEncodedArray: (JsonArray: any[], channelOptions?: ChannelOptions) => Promise<Annotation[]>;
}

/**
* Cipher Key used in {@link CipherParamOptions}. If set to a `string`, the value must be base64 encoded.
*/
Expand Down Expand Up @@ -2926,6 +3119,10 @@ export declare class Rest implements RestClient {
* Static utilities related to presence messages.
*/
static PresenceMessage: PresenceMessageStatic;
/**
* Static utilities related to annotations.
*/
static Annotation: AnnotationStatic;

// Requirements of RestClient

Expand Down Expand Up @@ -2977,6 +3174,10 @@ export declare class Realtime implements RealtimeClient {
* Static utilities related to presence messages.
*/
static PresenceMessage: PresenceMessageStatic;
/**
* Static utilities related to annotations.
*/
static Annotation: AnnotationStatic;

// Requirements of RealtimeClient

Expand Down
20 changes: 20 additions & 0 deletions modular.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,21 @@ export declare const MsgPack: unknown;
*/
export declare const RealtimePresence: unknown;

/**
* Provides a {@link BaseRealtime} instance with the ability to interact with message
* annotations.
*
* To create a client that includes this plugin, include it in the client options that you pass to the {@link BaseRealtime.constructor}:
*
* ```javascript
* import { BaseRealtime, WebSocketTransport, FetchRequest, Annotations } from 'ably/modular';
* const realtime = new BaseRealtime({ ...options, plugins: { WebSocketTransport, FetchRequest, Annotations } });
* ```
*
* If you do not provide this plugin, then attempting to access a channel’s {@link ably!RealtimeChannel.annotations} property will cause a runtime error.
*/
export declare const Annotations: unknown;

/**
* Provides a {@link BaseRealtime} instance with the ability to establish a connection with the Ably realtime service using a [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) connection.
*
Expand Down Expand Up @@ -230,6 +245,11 @@ export interface ModularPlugins {
*/
RealtimePresence?: typeof RealtimePresence;

/**
* See {@link Annotations | documentation for the `Annotations` plugin}.
*/
Annotations?: typeof Annotations;

/**
* See {@link WebSocketTransport | documentation for the `WebSocketTransport` plugin}.
*/
Expand Down
3 changes: 3 additions & 0 deletions src/common/lib/client/baseclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import * as Utils from '../util/utils';
import Platform from '../../platform';
import { Rest } from './rest';
import { IUntypedCryptoStatic } from 'common/types/ICryptoStatic';
import { AnnotationsPlugin } from './modularplugins';
import { throwMissingPluginError } from '../util/utils';
import { MsgPack } from 'common/types/msgpack';
import { HTTPRequestImplementations } from 'platform/web/lib/http/http';
Expand Down Expand Up @@ -46,6 +47,7 @@ class BaseClient {
// Extra HTTP request implementations available to this client, in addition to those in web’s Http.bundledRequestImplementations
readonly _additionalHTTPRequestImplementations: HTTPRequestImplementations | null;
private readonly __FilteredSubscriptions: typeof FilteredSubscriptions | null;
readonly _Annotations: AnnotationsPlugin | null;
readonly logger: Logger;
_device?: LocalDevice;

Expand Down Expand Up @@ -98,6 +100,7 @@ class BaseClient {
this._rest = options.plugins?.Rest ? new options.plugins.Rest(this) : null;
this._Crypto = options.plugins?.Crypto ?? null;
this.__FilteredSubscriptions = options.plugins?.MessageInteractions ?? null;
this._Annotations = options.plugins?.Annotations ?? null;
}

get rest(): Rest {
Expand Down
11 changes: 11 additions & 0 deletions src/common/lib/client/defaultrealtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ import { DefaultMessage } from '../types/defaultmessage';
import { MsgPack } from 'common/types/msgpack';
import RealtimePresence from './realtimepresence';
import { DefaultPresenceMessage } from '../types/defaultpresencemessage';
import { DefaultAnnotation } from '../types/defaultannotation';
import WebSocketTransport from '../transport/websockettransport';
import { FilteredSubscriptions } from './filteredsubscriptions';
import { PresenceMap } from './presencemap';
import PresenceMessage, { WirePresenceMessage } from '../types/presencemessage';
import RealtimeAnnotations from './realtimeannotations';
import RestAnnotations from './restannotations';
import Annotation, { WireAnnotation } from '../types/annotation';
import { Http } from 'common/types/http';
import Defaults from '../util/defaults';
import Logger from '../util/logger';
Expand All @@ -38,6 +42,12 @@ export class DefaultRealtime extends BaseRealtime {
PresenceMessage,
WirePresenceMessage,
},
Annotations: {
Annotation,
WireAnnotation,
RealtimeAnnotations,
RestAnnotations,
},
WebSocketTransport,
MessageInteractions: FilteredSubscriptions,
}),
Expand All @@ -62,6 +72,7 @@ export class DefaultRealtime extends BaseRealtime {

static Message = DefaultMessage;
static PresenceMessage = DefaultPresenceMessage;
static Annotation = DefaultAnnotation;

static _MsgPack: MsgPack | null = null;

Expand Down
Loading

0 comments on commit 99c7cca

Please sign in to comment.