diff --git a/content/auth/capabilities.textile b/content/auth/capabilities.textile index 0561a2f9c0..13618740c9 100644 --- a/content/auth/capabilities.textile +++ b/content/auth/capabilities.textile @@ -44,6 +44,8 @@ The following capability operations are available for API keys and issued tokens - presence := can register presence on a channel (enter, update and leave) - object-subscribe := can subscribe to updates to objects on a channel - object-publish := can update objects on a channel +- annotation-subscribe := can subscribe to individual annotations on a channel +- annotation-publish := can publish annotations to messages on a channel - history := can retrieve message and presence state history on channels - stats := can retrieve current and historical usage statistics for an app - push-subscribe := can subscribe devices for push notifications diff --git a/content/channels/index.textile b/content/channels/index.textile index b2a6be26d6..70c7f0c662 100644 --- a/content/channels/index.textile +++ b/content/channels/index.textile @@ -190,19 +190,20 @@ Channel rules can be used to enforce settings for specific channels, or channel The channel rules related to message storage are: -- Persist last message := if enabled, the very last message published on a channel will be stored for a year. This message is retrievable using "rewind":/docs/channels/options/rewind by attaching to the channel with @rewind=1@. If you send multiple messages in a single protocol message, for example calling @publish()@ with an array of messages, you would receive all of them as one message. Be aware that presence messages are not stored and that messages stored in this manner are not accessible using "history":/docs/storage-history/history. Note that for each message stored using this rule, an additional message is deducted from your monthly allocation. -- Persist all messages := if enabled, all messages published on a channel will be stored according to the storage rules for your account. This is 24 hours for free accounts and 72 hours for paid accounts. Messages stored in this manner are accessible using "history":/docs/storage-history/history. Note that for each message stored using this rule, an additional message is deducted from your monthly allocation. +- Persist last message := If enabled, the very last message published on a channel will be stored for a year. This message is retrievable using "rewind":/docs/channels/options/rewind by attaching to the channel with @rewind=1@. If you send multiple messages in a single protocol message, for example calling @publish()@ with an array of messages, you would receive all of them as one message. Be aware that presence messages are not stored and that messages stored in this manner are not accessible using "history":/docs/storage-history/history. Note that for each message stored using this rule, an additional message is deducted from your monthly allocation. +- Persist all messages := If enabled, all messages published on a channel will be stored according to the storage rules for your account. This is 24 hours for free accounts and 72 hours for paid accounts. Messages stored in this manner are accessible using "history":/docs/storage-history/history. Note that for each message stored using this rule, an additional message is deducted from your monthly allocation. The channel rules related to security and client identity are: -- Identified := if enabled, clients will not be permitted to use (including to attach, publish, or subscribe) matching channels unless they are "identified":/docs/auth/identified-clients (they have an assigned client ID). Anonymous clients are not permitted to join these channels. Find out more about "authenticated and identified clients":/docs/auth/identified-clients. -- TLS only := if enabled, only clients who have connected to Ably over TLS will be allowed to use matching channels. By default all of Ably's client libraries use TLS when communicating with Ably over REST or when using our Realtime transports such as Websockets. +- Identified := If enabled, clients will not be permitted to use (including to attach, publish, or subscribe) matching channels unless they are "identified":/docs/auth/identified-clients (they have an assigned client ID). Anonymous clients are not permitted to join these channels. Find out more about "authenticated and identified clients":/docs/auth/identified-clients. +- TLS only := If enabled, only clients who have connected to Ably over TLS will be allowed to use matching channels. By default all of Ably's client libraries use TLS when communicating with Ably over REST or when using our Realtime transports such as Websockets. The channel rules related to enabling features are: -- Push notifications enabled := If checked, publishing messages with a push payload in the @extras@ field is permitted. This triggers the delivery of a "Push Notification":/docs/push to devices registered for push on the channel. -- Server-side batching := if enabled, messages are grouped into batches before being sent to subscribers. "Server-side batching":/docs/messages/batch#server-side reduces the overall message count, lowers costs, and mitigates the risk of hitting rate limits during high-throughput scenarios. -- Message conflation := if enabled, messages are aggregated over a set period of time and evaluated against a conflation key. All but the latest message for each conflation key value will be discarded, and the resulting message, or messages, will be delivered to subscribers as a single batch once the period of time elapses. "Message conflation":/docs/messages#conflation reduces costs in high-throughput scenarios by removing redundant and outdated messages. +- Push notifications enabled := If enabled, publishing messages with a push payload in the @extras@ field is permitted. This triggers the delivery of a "Push Notification":/docs/push to devices registered for push on the channel. +- Server-side batching := If enabled, messages are grouped into batches before being sent to subscribers. "Server-side batching":/docs/messages/batch#server-side reduces the overall message count, lowers costs, and mitigates the risk of hitting rate limits during high-throughput scenarios. +- Message conflation := If enabled, messages are aggregated over a set period of time and evaluated against a conflation key. All but the latest message for each conflation key value will be discarded, and the resulting message, or messages, will be delivered to subscribers as a single batch once the period of time elapses. "Message conflation":/docs/messages#conflation reduces costs in high-throughput scenarios by removing redundant and outdated messages. +- Advanced message features := If enabled, advanced messaging features including message "annotations":/docs/annotations are available on the channel. Note that advanced messaging is currently Experimental and its features are still in development and subject to rapid change. Currently, when advanced messaging is enabled, messages are "persisted":/docs/storage-history/storage#all-message-persistence by default, and "continuous history":/docs/storage-history/history#continuous-history features are not currently supported. To set a channel rule in the Ably dashboard: diff --git a/content/channels/options/index.textile b/content/channels/options/index.textile index ebf3db3b86..48d85039af 100644 --- a/content/channels/options/index.textile +++ b/content/channels/options/index.textile @@ -431,6 +431,8 @@ The available set of channel mode flags are: | @PRESENCE@ | Can register presence on the channel. | Yes | | @OBJECT_PUBLISH@ | Can update objects on the channel. | No | | @OBJECT_SUBSCRIBE@ | Can subscribe to receive updates to objects on the channel. | No | +| @ANNOTATION_PUBLISH@ | Can publish annotations to messages on the channel. | Yes | +| @ANNOTATION_SUBSCRIBE@ | Can subscribe to individual annotations on the channel. | No | The set of modes available to a client is determined by the set of "capabilities":/docs/auth/capabilities granted by their token or API key. @@ -442,6 +444,8 @@ The modes granted by each capability are: | @presence@ | @PRESENCE@ | | @object-subscribe@ | @OBJECT_SUBSCRIBE@ | | @object-publish@ | @OBJECT_PUBLISH@ | +| @annotation-publish@ | @ANNOTATION_PUBLISH@ | +| @annotation-subscribe@ | @ANNOTATION_SUBSCRIBE@ | The actual modes assigned to a client will be the **intersection** of the requested @modes@ and the modes available to the client according to its capabilities. For example, a client with the @subscribe@ capability which explicitly requests @SUBSCRIBE@ and @PUBLISH@ modes will be assigned only the @SUBSCRIBE@ mode. diff --git a/content/errors/codes.textile b/content/errors/codes.textile index 660b324146..c980e074f3 100644 --- a/content/errors/codes.textile +++ b/content/errors/codes.textile @@ -757,6 +757,20 @@ You may encounter this error when the type of the object located at the specifie *Resolution*: * Ensure that the operation is valid for the type of object at the specified path. +h2(#93001). 93001: Attempt to add an annotation listener without having requested the annotation_subscribe channel mode + +This error occurs when attempting to "subscribe to individual annotations":/docs/messages/annotations#subscribe-individual-annotations without having requested the @annotation_subscribe@ "channel mode":/docs/channels/options#modes . + +*Resolution*: +* Ensure that @annotation_subscribe@ mode is specified in the client "channel options":/docs/channels/options before subscribing to individual annotations. + +h2(#93002). 93002: Annotations are only supported on channels with message annotations, updates, and deletes enabled + +This error occurs when attempting to use "message annotations":/docs/messages/annotations on a channel that does not have them enabled. + +*Resolution*: +* Create a "channel rule":/docs/channels#rules for the channel or channel namespace with *Message annotations, updates, and deletes* enabled. + h2(#101000). 101000: Space name missing This error occurs when calling "@spaces.get()@":/docs/spaces/space#options without specifying a space name. The name parameter is required to retrieve a space. diff --git a/content/messages/index.textile b/content/messages/index.textile index 98ddfd4070..05fcf0be55 100644 --- a/content/messages/index.textile +++ b/content/messages/index.textile @@ -34,6 +34,10 @@ The following are the properties of a message: - extras := A JSON object of arbitrary key-value pairs that may contain metadata, and/or ancillary payloads. Valid payloads include those related to "Push Notifications":/docs/push, "deltas":/docs/channels/options/deltas and headers. - encoding := This is typically empty, as all messages received from Ably are automatically decoded client-side using this value. However, if the message encoding cannot be processed, this attribute contains the remaining transformations not applied to the data payload. + + h2(#conflation). Message conflation Use message conflation to ensure that clients only ever receive the most up-to-date message, by removing redundant and outdated messages. Message conflation will aggregate published messages for a set period of time and evaluate all messages against a "conflation key":#routing. All but the latest message for each conflation key value will be discarded, and the resulting message, or messages, will be delivered to subscribers as a single batch once the period of time elapses. diff --git a/content/partials/core-features/_authentication_capabilities.textile b/content/partials/core-features/_authentication_capabilities.textile index c4893023c4..de90b27bdc 100644 --- a/content/partials/core-features/_authentication_capabilities.textile +++ b/content/partials/core-features/_authentication_capabilities.textile @@ -5,6 +5,8 @@ The following capability operations are available for API keys and issued tokens - presence := can register presence on a channel (enter, update and leave) - object-subscribe := can subscribe to updates to objects on a channel - object-publish := can update objects on a channel +- annotation-subscribe := can subscribe to individual annotations on a channel +- annotation-publish := can publish annotations to messages on a channel - history := can retrieve message and presence state history on channels - stats := can retrieve current and historical usage statistics for an app - push-subscribe := can subscribe devices for push notifications diff --git a/content/pub-sub/index.textile b/content/pub-sub/index.textile index c7ad028b4d..4ebbe3a562 100644 --- a/content/pub-sub/index.textile +++ b/content/pub-sub/index.textile @@ -444,4 +444,5 @@ if err := channel.Publish(context.Background(), "example", "message data"); err diff --git a/src/data/nav/pubsub.ts b/src/data/nav/pubsub.ts index 5e40a16ea0..78c525e056 100644 --- a/src/data/nav/pubsub.ts +++ b/src/data/nav/pubsub.ts @@ -166,6 +166,10 @@ export default { name: 'Message batching', link: '/docs/messages/batch', }, + { + name: 'Message annotations', + link: '/docs/messages/annotations', + }, ], }, { diff --git a/src/pages/docs/messages/annotations.mdx b/src/pages/docs/messages/annotations.mdx new file mode 100644 index 0000000000..8184f3a42b --- /dev/null +++ b/src/pages/docs/messages/annotations.mdx @@ -0,0 +1,486 @@ +--- +title: Message annotations +meta_description: "Annotate messages on a channel with additional metadata." +--- + + + +Message annotations enable clients to add metadata to existing messages on a channel. You can use annotations to implement features like: + +* **Message reactions** - add emoji reactions (👍, ❤️, 😂) to messages +* **Content categorization** - tag messages with categories such as "important" or "urgent" +* **Community moderation** - flag inappropriate content for review +* **Read receipts** - mark messages as "read" or "delivered" + +When clients publish or delete an annotation, Ably automatically creates a [summary](#annotation-summaries) that provides an aggregated view of all annotations for that message. + +## Enable annotations + +Annotations can be enabled for a channel or channel namespace with the *Message annotations, updates, and deletes* channel rule. + + + +1. On your [dashboard](https://ably.com/accounts/any), select one of your apps. +2. Go to **Settings**. +3. Under [channel rules](/docs/channels#rules), click **Add new rule**. +4. Enter the channel name or channel namespace on which to enable message annotations. +5. Check **Message annotations, updates, and deletes** to enable message annotations. +6. Click **Create channel rule** to save. + +## Annotation types + +Annotation types determine how annotations are processed and aggregated into [summaries](#annotation-summaries). + +The annotation type is a string of the format `namespace:summarization.version` where: + +* `namespace` is a string (e.g. `reactions`) that groups related annotations. Only annotations in the same namespace will be aggregated together to produce [summaries](#annotation-summaries). +* `summarization` specifies how annotations are aggregated to produce [summaries](#annotation-summaries), such as `total`, `flag`, `distinct`, `unique`, or `multiple`. +* `version` specifies the version component which allows for future changes to summarization behavior. + +### Total + +The `total.v1` summarization method counts the number of annotations of a given type that were published for a message. + +Deleting an annotation decrements the total count for that message. + +Using `total.v1` does not attribute counts to individual clients in the summary; it maintains only a simple count per annotation type and does not organize counts by name. [Unidentified](/docs/auth/identified-clients#unidentified) clients can publish `total.v1` annotations. Use the [identified channel rule](/docs/channels#rules) if you want to prevent unidentified clients from publishing annotations. + +If the same client publishes an annotation of a given type to the same message twice, the `total` count is incremented twice. + + +```json +{ + "metrics:total.v1": { + "total": 42 + } +} +``` + + +### Flag + +The `flag.v1` summarization method counts how many distinct clients have published an annotation of a given type and maintains a list of those `clientId`s. Clients must be [identified](/docs/auth/identified-clients) to publish `flag.v1` annotations. + +Deleting an annotation decrements the total count for that message and removes the `clientId` from the list of clients that contributed to the summary. + +A given client can contribute to the summary only once per annotation type. + + +```json +{ + "reactions:flag.v1": { + "total": 3, + "clientIds": ["client1", "client2", "client3"] + } +} +``` + + +### Distinct + +The `distinct.v1` summarization method counts how many unique clients have published an annotation with a given `name` for each annotation type along with the corresponding list of `clientId`s that published it. Clients must be [identified](/docs/auth/identified-clients) to publish `distinct.v1` annotations. + +A given client can contribute to the summary for a particular annotation `name` only once, but the same client may publish additional annotations with different `name`s. + +Deleting an annotation removes the `clientId` from the list of clients that contributed to the summary for that `name`, and decrements the total count for that `name`. + + +```json +{ + "categories:distinct.v1": { + "important": { + "total": 2, + "clientIds": ["client1", "client3"] + }, + "urgent": { + "total": 3, + "clientIds": ["client1", "client2", "client3"] + } + } +} +``` + + +### Unique + +The `unique.v1` summarization method counts how many unique clients have published an annotation with a given `name` for each annotation type, while guaranteeing that each client contributes to the summary for only one `name` at a time. The summary for each annotation `name` holds a `total` count of the number of distinct clients that have published an annotation with that `name` along with the corresponding list of `clientId`s that published it. Clients must be [identified](/docs/auth/identified-clients) to publish `unique.v1` annotations. + +A given client can contribute to the summary for a particular annotation `name` only once. Publishing an annotation with a different `name` automatically removes that client from the summary for the previous `name` and adds them to the new one, updating the affected total values and list of `clientId`s. + +Deleting an annotation removes the `clientId` from the list of clients that contributed to the summary for that `name`, and decrements the total count for that `name`. + + +```json +{ + "status:unique.v1": { + "important": { + "total": 2, + "clientIds": ["client1", "client3"] + }, + "urgent": { + "total": 1, + "clientIds": ["client2"] + } + } +} +``` + + +### Multiple + +The `multiple.v1` summarization method counts both a total and a per-client count of the number of annotations that were published with a given `name` for each annotation type. Additionally it includes a count for the number of annotations published with a given `name` from unidentified clients. Use the [identified channel rule](/docs/channels#rules) if you want to prevent unidentified clients from publishing annotations. + +A given client can contribute to the summary for a particular annotation `name` multiple times. The same client may also publish additional annotations with different `name`s. + +If a client specifies a `count` when publishing an annotation, the client's contribution to the summary is incremented by the specified value. + +Deleting an annotation removes all contributions made by that `clientId` for that `name`. + + +```json +{ + "voting:multiple.v1": { + "option-a": { + "total": 7, + "clientCounts": { + "client1": 3, + "client2": 2 + }, + "totalUnidentified": 2 + }, + "option-b": { + "total": 4, + "clientCounts": { + "client1": 2, + "client3": 1 + }, + "totalUnidentified": 1 + } + } +} +``` + + +## Publish annotations + +To publish an annotation for a message, use the `annotations.publish()` method on a channel. Pass in either a [message](/docs/messages) instance or the `serial` of the message to annotate. This method will publish an annotation message with an action of `annotation.create`. + +The `clientId` specified in the [client options](/docs/api/realtime-sdk#client-options) will be associated with the published annotation. Note that certain annotation types require the client to be identified with a `clientId` in order to publish annotations. + +Specify the [annotation type](#annotation-types) using the `type` field of the annotation object, and optionally specify a `name` for the annotation. The `name` is used to aggregate certain annotations when producing an [annotation summary](#annotation-summaries). + + +```javascript +// Create an Ably Realtime client specifying the clientId that will +// be associated with annotations published by this client +const realtime = new Ably.Realtime({ key: '{{API_KEY}}', clientId: 'my-client-id' }); + +// Create a channel in a namespace called `annotations` +// which has message annotations enabled +const channel = realtime.channels.get('annotations:example'); + +// Publish an annotation for a message that flags it as delivered +await channel.annotations.publish(message, { + type: 'receipts:flag.v1', + name: 'delivered' +}); + +// You can also use a message's serial number +await channel.annotations.publish(message.serial, { + type: 'receipts:flag.v1', + name: 'delivered' +}); +``` + +```nodejs +// Create an Ably Realtime client specifying the clientId that will +// be associated with annotations published by this client +const realtime = new Ably.Realtime({ key: '{{API_KEY}}', clientId: 'my-client-id' }); + +// Create a channel in a namespace called `annotations` +// which has message annotations enabled +const channel = realtime.channels.get('annotations:example'); + +// Publish an annotation for a message that flags it as delivered +await channel.annotations.publish(message, { + type: 'receipts:flag.v1', + name: 'delivered' +}); + +// You can also use a message's serial number +await channel.annotations.publish(message.serial, { + type: 'receipts:flag.v1', + name: 'delivered' +}); +``` + + +When using `multiple.v1`, you can optionally specify a `count` by which to increment a client's contribution to the summary: + + +```javascript +await channel.annotations.publish(message.serial, { + type: 'rating:multiple.v1', + name: 'stars', + count: 4 +}); +``` + +```nodejs +await channel.annotations.publish(message.serial, { + type: 'rating:multiple.v1', + name: 'stars', + count: 4 +}); +``` + + +## Delete annotations + +To delete an annotation, use the `annotations.delete()` method on a channel. Pass in either a [message](/docs/messages) instance or the `serial` of the message to annotate. This method will publish an annotation message with an action of `annotation.delete`. + +Deleting an annotation does not remove the original annotation that was published. Instead, they affect the [annotation summary](#annotation-summaries) for that message by removing the contribution specified by the annotation. + +The `clientId` specified in the [client options](/docs/api/realtime-sdk#client-options) will be associated with the published delete annotation. + +Specify the [annotation type](#annotation-types) using the `type` field of the annotation object, and optionally specify a `name` for the annotation. The `name` is used to aggregate certain annotations when producing an annotation summary. + + +```javascript +// Create an Ably Realtime client specifying the clientId that will +// be associated with annotations published by this client +const realtime = new Ably.Realtime({ key: '{{API_KEY}}', clientId: 'my-client-id' }); + +// Create a channel in a namespace called `annotations` +// which has message annotations enabled +const channel = realtime.channels.get('annotations:example'); + +// Delete a 'delivered' annotation +await channel.annotations.delete(message.serial, { + type: 'receipts:flag.v1', + name: 'delivered' +}); +``` + +```nodejs +// Create an Ably Realtime client specifying the clientId that will +// be associated with annotations published by this client +const realtime = new Ably.Realtime({ key: '{{API_KEY}}', clientId: 'my-client-id' }); + +// Create a channel in a namespace called `annotations` +// which has message annotations enabled +const channel = realtime.channels.get('annotations:example'); + +// Delete a 'delivered' annotation +await channel.annotations.delete(message.serial, { + type: 'receipts:flag.v1', + name: 'delivered' +}); +``` + + +## Subscribe to annotations + +The recommended way to receive annotation updates is through annotation summaries. These events provide a summary of the complete, current state of all annotations for a message whenever an annotation is published or deleted. + +Annotation summaries are delivered to subscribers as messages with an `action` of `message.summary`. These messages have a `summary` field which provides a summary of all the annotations for the message, identified by the `serial` field on the summary message. + +The value of the `summary` field is an object where the keys are the [annotation types](#annotation-types). The structure of the value of each key depends on the summarization method used, for example `total.v1` will have a `total` field, while `flag.v1` will have `total` and `clientIds` fields. + + + + +```javascript +// Create an Ably Realtime client specifying the clientId that will +// be associated with annotations published by this client +const realtime = new Ably.Realtime({ key: '{{API_KEY}}', clientId: 'my-client-id' }); + +// Create a channel in a namespace called `annotations` +// which has message annotations enabled +const channel = realtime.channels.get('annotations:example'); + +await channel.subscribe((message) => { + if (message.action === 'message.summary') { + console.log(message.summary); + } +}); +``` + +```nodejs +// Create an Ably Realtime client specifying the clientId that will +// be associated with annotations published by this client +const realtime = new Ably.Realtime({ key: '{{API_KEY}}', clientId: 'my-client-id' }); + +// Create a channel in a namespace called `annotations` +// which has message annotations enabled +const channel = realtime.channels.get('annotations:example'); + +await channel.subscribe((message) => { + if (message.action === 'message.summary') { + console.log(message.summary); + } +}); +``` + + +### Annotation summaries + +When annotations for a message are published, Ably automatically generates a summary that provides an aggregated view of all annotations for that message. + +A separate summary is produced for each distinct [annotation type](#annotation-types). The summarization method specified in the annotation type determines how annotations in the same namespace for a given message are aggregated into a summary. A summary is constructed from the set of [individual annotation events](#individual-annotations) (annotation messages with an `action` of `annotation.create` or `annotation.delete`). + +The summary will be included in the message's `summary` field, which is an object whose keys are the annotation types and whose values describe the annotation summary for that type. For example: + + +```json +{ + "metrics:total.v1": { + "total": 42 + }, + "reactions:flag.v1": { + "total": 3, + "clientIds": ["client1", "client2", "client3"] + }, + "categories:distinct.v1": { + "important": { + "total": 2, + "clientIds": ["client1", "client3"] + }, + "urgent": { + "total": 3, + "clientIds": ["client1", "client2", "client3"] + } + }, + "status:unique.v1": { + "important": { + "total": 2, + "clientIds": ["client1", "client3"] + }, + "urgent": { + "total": 1, + "clientIds": ["client2"] + } + }, + "voting:multiple.v1": { + "option-a": { + "total": 7, + "clientCounts": { + "client1": 3, + "client2": 2 + }, + "totalUnidentified": 2 + }, + "option-b": { + "total": 4, + "clientCounts": { + "client1": 2, + "client3": 1 + }, + "totalUnidentified": 1 + } + } +} +``` + + +## Individual annotation events + +It is also possible to subscribe to individual annotation events, rather than annotation summaries. These are the emitted when [publishing](#publish) or [deleting](#delete) an annotation. + +Individual events can be useful for activity feeds or detailed logging, however annotation summaries are generally more reliable and efficient for maintaining UI state. + +### Publish individual annotation events + +Publishing annotations is the [same as for summaries](#publish). The only difference is that you can additionally specify a `data` payload when publishing an annotation, which isn't included in an annotation summary. + + +```javascript +await channel.annotations.publish(message.serial, { + type: 'receipts:flag.v1', + name: 'delivered', + data: 'Message delivered!' +}); +``` + +```nodejs +await channel.annotations.publish(message.serial, { + type: 'receipts:flag.v1', + name: 'delivered', + data: 'Message delivered!' +}); +``` + + +### Subscribe to individual annotations + +Subscribe to individual annotation events using the `annotations.subscribe()` method on a channel. To subscribe to individual annotations, you must request the `ANNOTATION_SUBSCRIBE` [mode](/docs/channels/options#modes). + + + +Annotations delivered to the `annotations.subscribe()` listener will have an `action` of `annotation.create` or `annotation.delete`. + + +```javascript +// Create an Ably Realtime client specifying the clientId that will +// be associated with annotations published by this client +const realtime = new Ably.Realtime({ key: '{{API_KEY}}', clientId: 'my-client-id' }); + +// Create a channel in a namespace called `annotations` +// which has message annotations enabled. +// Specify the ANNOTATION_SUBSCRIBE mode to enable annotation subscriptions. +const channel = realtime.channels.get('annotations:example', { modes: ['ANNOTATION_SUBSCRIBE', ... /* all other modes you need, such as MESSAGE_SUBSCRIBE */] }); + +await channel.annotations.subscribe((annotation) => { + if (annotation.action === 'annotation.create') { + console.log(`New ${annotation.type} annotation with name ${annotation.name} from ${annotation.clientId}`); + } else if (annotation.action === 'annotation.delete') { + console.log(`${annotation.clientId} deleted a ${annotation.type} annotation with name ${annotation.name}`); + } +}); +``` + +```nodejs +// Create an Ably Realtime client specifying the clientId that will +// be associated with annotations published by this client +const realtime = new Ably.Realtime({ key: '{{API_KEY}}', clientId: 'my-client-id' }); + +// Create a channel in a namespace called `annotations` +// which has message annotations enabled. +// Specify the ANNOTATION_SUBSCRIBE mode to enable annotation subscriptions. +const channel = realtime.channels.get('annotations:example', { modes: ['ANNOTATION_SUBSCRIBE'] }); + +await channel.annotations.subscribe((annotation) => { + if (annotation.action === 'annotation.create') { + console.log(`New ${annotation.type} annotation with name ${annotation.name} from ${annotation.clientId}`); + } else if (annotation.action === 'annotation.delete') { + console.log(`${annotation.clientId} deleted a ${annotation.type} annotation with name ${annotation.name}`); + } +}); +``` + + +### Annotation message properties + +Annotations are a special type of message with the following properties: + +| Property | Description | +| -------- | ----------- | +| id | An Ably-generated ID used to uniquely identify the annotation. | +| action | The action specifies whether this is an annotation being added (`annotation.create`) or removed (`annotation.delete`). | +| serial | This annotation's unique serial (lexicographically totally ordered). | +| messageSerial | The serial of the message that this annotation is annotating. | +| type | The [annotation type](#annotation-types). | +| name | The name of the annotation, used by some [annotation types](#annotation-types) for aggregation. | +| clientId | The client identifier of the user that published this annotation. | +| count | An optional count, only relevant to certain [annotation types](#annotation-types). | +| data | An optional payload for the annotation. Available on an [individual annotation](#individual-annotations) but not aggregated or included in [annotation summaries](#annotation-summaries). | +| encoding | 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. | +| timestamp | The timestamp of when the annotation was received by Ably, as milliseconds since the Unix epoch. |