From 9110e795761cb4cd2231e992bde52001274a7247 Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Mon, 2 Jun 2025 09:22:40 +0100 Subject: [PATCH 01/19] channels: add annotations channel rule Adds "annotations" to the list of documented channel rules. --- content/channels/index.textile | 1 + 1 file changed, 1 insertion(+) diff --git a/content/channels/index.textile b/content/channels/index.textile index b2a6be26d6..398110899c 100644 --- a/content/channels/index.textile +++ b/content/channels/index.textile @@ -203,6 +203,7 @@ 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. +- Annotations := if enabled, allows use of message "annotations":/docs/annotations . To set a channel rule in the Ably dashboard: From 9a56e64c011876cd06a07048a3b9e4b2e9539598 Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Tue, 3 Jun 2025 14:36:30 +0100 Subject: [PATCH 02/19] messages: add annotations docs Adds a page to the Messages section of the Pub/Sub product nav to document the message annotation feature. --- src/data/nav/pubsub.ts | 4 ++++ src/pages/docs/messages/annotations.mdx | 14 ++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 src/pages/docs/messages/annotations.mdx 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..601b6ca92a --- /dev/null +++ b/src/pages/docs/messages/annotations.mdx @@ -0,0 +1,14 @@ +--- +title: Message Annotations +meta_description: "Annotate messages on a channel with additional metadata." +languages: + - javascript + - nodejs +--- + +Message annotations allow 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 like "important" or "urgent" +- **Community moderation** - Flag inappropriate content for review +- **Read receipts** - Mark messages as "read" or "delivered" From 9076e8f04ae92c8e7b437fc1426ff9c466911c48 Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Tue, 3 Jun 2025 14:38:02 +0100 Subject: [PATCH 03/19] annotations: add docs for enabling annotations Describes how to enable message annotations by configuring an annotations channel rule. --- src/pages/docs/messages/annotations.mdx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/pages/docs/messages/annotations.mdx b/src/pages/docs/messages/annotations.mdx index 601b6ca92a..249c7bb3c0 100644 --- a/src/pages/docs/messages/annotations.mdx +++ b/src/pages/docs/messages/annotations.mdx @@ -12,3 +12,15 @@ Message annotations allow clients to add metadata to existing messages on a chan - **Content categorization** - Tag messages with categories like "important" or "urgent" - **Community moderation** - Flag inappropriate content for review - **Read receipts** - Mark messages as "read" or "delivered" + + +## Enable annotations + +Annotations can be enabled for a channel or channel namespace with the *Annotations* 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 **Annotations** to enable message annotations. +6. Click **Create channel rule** to save. From 37c3af6f36da51e8dd550a4cfdc9a1d710c6818e Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Tue, 3 Jun 2025 14:41:56 +0100 Subject: [PATCH 04/19] annotations: publish, subscribe, summary and type Add docs for publishing annotations, subscribing to annotation summaries and individual annotation events, and descriptions of annotation types and the available summarization methods and their semantics. --- src/pages/docs/messages/annotations.mdx | 340 ++++++++++++++++++++++++ 1 file changed, 340 insertions(+) diff --git a/src/pages/docs/messages/annotations.mdx b/src/pages/docs/messages/annotations.mdx index 249c7bb3c0..c75fc83f5a 100644 --- a/src/pages/docs/messages/annotations.mdx +++ b/src/pages/docs/messages/annotations.mdx @@ -13,6 +13,8 @@ Message annotations allow clients to add metadata to existing messages on a chan - **Community moderation** - Flag inappropriate content for review - **Read receipts** - Mark messages as "read" or "delivered" +When clients publish or delete annotations, Ably automatically creates a [summary](#annotation-summaries) that provides an aggregated view of all annotations for a message. + ## Enable annotations @@ -24,3 +26,341 @@ Annotations can be enabled for a channel or channel namespace with the *Annotati 4. Enter the channel name or channel namespace on which to enable message annotations. 5. Check **Annotations** to enable message annotations. 6. Click **Create channel rule** to save. + + +## Annotation properties + +Annotations are a special type of message with the following properties: + +| Property | Description | +| -------- | ----------- | +| id | An Ably-generated ID used to uniquely identify the message. | +| action | The action specifies whether this is an annotation being added (`annotation.create`) or removed (`annotation.delete`). See [subscribing to annotation updates](#subscribe). | +| serial | This message'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 summarization methods for aggregation. See [annotation summaries](#annotation-summaries). | +| clientId | The client identifier of the user that published this annotation. | +| count | An optional count, only relevant to certain summarization methods. See [annotation summaries](#annotation-summaries) for more information. | +| data | An optional payload for the annotation. Available on an [individual annotation](#subscribe-individual-annotations) but not aggregated or included in [annotation summaries](#annotation-summaries). | +| 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. | +| timestamp | The timestamp of when the message was received by Ably, as milliseconds since the Unix epoch. | + + +## Publishing annotations + +To publish an annotation for a message, use the `annotations.publish()` method on a channel, passing either a [message](/docs/messages) instance or the `serial` of the message to annotate. + +The `clientId` specified in the [client options](/docs/api/realtime-sdk#client-options) will be associated with the published annotation. + + + +When publishing an annotation, specify the [annotation type](#annotation-types) on the `type` field of the annotation object. + +You can also optionally specify a `name` for the annotation which is used by certain [summarization methods](#annotation-summaries) that aggregate by `name`. + + +```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' +}); +``` + + +You can also specify a payload on the `data` field when publishing an annotation. While the `data` payload is available on the [individual annotation](#subscribe-individual-annotations) events, it is not included in [annotation summaries](#annotation-summaries). + + +```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!' +}); +``` + + +When using the `multiple.v1` summarization method you can optionally specify a `count` by which to increment this 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 +}); +``` + + + +## Subscribing to annotation updates + +There are two ways to subscribe to annotation updates: + +1. **Annotation summaries** (recommended) - Receive aggregated summaries of all annotations for a message +2. **Individual annotations** - Receive individual events when annotations are published or deleted + +### Subscribing to annotation summaries + +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 annotations are 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 exact structure of the value of each key depends on the [summarization method](#annotation-summaries) specified in the annotation type. + + +```javascript +await channel.subscribe((message) => { + if (message.action === 'message.summary') { + console.log(message.summary); + } +}); +``` + +```nodejs +await channel.subscribe((message) => { + if (message.action === 'message.summary') { + console.log(message.summary); + } +}); +``` + + + + +For more details on summary structure and usage, see [annotation summaries](#annotation-summaries). + +### Subscribing to individual annotations + +For more fine-grained visibility, you can also subscribe directly to individual annotation events using the `annotations.subscribe()` method on a channel. + +Annotations delivered to the `annotations.subscribe()` listener will have an `action` of `annotation.create` or `annotation.delete`. + + +```javascript +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 +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 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 the summarization method (e.g. `flag`) which determines how annotations are aggregated to produce [summaries](#annotation-summaries). +- `version` specifies the version component of the summarization method (e.g. `v1`), which allows for future changes to summarization behavior. + +## 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](#annotation-types) determines how annotations in the same namespace for a given message are aggregated into a summary. + +When [publishing](#publish) an annotation you can optionally specify a `name` which is used by some summarization methods to produce a summary. + +Ably provides five summarization methods to support different use cases which are described below. + +### Total + +The `total.v1` summarization method counts the number of annotations of a given type that were published for a message. + + + +The `total.v1` summarization method does not attribute counts to individual clients in the summary; it maintains only a simple counter per type and does not organize counts by name. + +```javascript +{ + "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. + +A given client can contribute to the summary only once per annotation type. + +```javascript +{ + "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. + +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. + +```javascript +{ + "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 pubilshed an annotation with that `name` along with the corresponding list of `clientId`s that published it. + +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. + +```javascript +{ + "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. + +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. + +```javascript +{ + "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 + } + } +} +``` \ No newline at end of file From fe6d45cd53c5c1db5854744355af62ad9628d3a4 Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Tue, 3 Jun 2025 14:52:25 +0100 Subject: [PATCH 05/19] annotations: delete annotations Documents the annotations.delete() method and describes how the are treated when producing summaries. --- src/pages/docs/messages/annotations.mdx | 78 +++++++++++++++++++------ 1 file changed, 59 insertions(+), 19 deletions(-) diff --git a/src/pages/docs/messages/annotations.mdx b/src/pages/docs/messages/annotations.mdx index c75fc83f5a..30b91be149 100644 --- a/src/pages/docs/messages/annotations.mdx +++ b/src/pages/docs/messages/annotations.mdx @@ -49,7 +49,7 @@ Annotations are a special type of message with the following properties: ## Publishing annotations -To publish an annotation for a message, use the `annotations.publish()` method on a channel, passing either a [message](/docs/messages) instance or the `serial` of the message to annotate. +To publish an annotation for a message, use the `annotations.publish()` method on a channel, passing 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. @@ -148,6 +148,41 @@ await channel.annotations.publish(message.serial, { +## Deleting annotations + +To delete a published annotation, use the `annotations.delete()` method on a channel, passing either a [message](/docs/messages) instance or the `serial` of the message to annotate. This method will publish a delete annotation message with an action of `annotation.delete`. + +Deleting an annotation does not remove the original annotation that was published. Instead, annotation delete messages affect the [annotation summary](#annotation-summaries) for that message by removing the contribution as specified by the delete annotation. + +The `clientId` specified in the [client options](/docs/api/realtime-sdk#client-options) will be associated with the published delete annotation. + + + +When publishing a delete annotation, specify the [annotation type](#annotation-types) on the `type` field of the annotation object. + +You can also optionally specify a `name` for the annotation which is used by certain [summarization methods](#annotation-summaries) that aggregate by `name`. + + +```javascript +// Delete a 'delivered' annotation +await channel.annotations.delete(message.serial, { + type: 'receipts:flag.v1', + name: 'delivered' +}); +``` + +```nodejs +// Delete a 'delivered' annotation +await channel.annotations.delete(message.serial, { + type: 'receipts:flag.v1', + name: 'delivered' +}); +``` + + + ## Subscribing to annotation updates There are two ways to subscribe to annotation updates: @@ -236,7 +271,7 @@ When annotations for a message are published, Ably automatically generates a sum A separate summary is produced for each distinct [annotation type](#annotation-types). -The summarization method specified in the [annotation type](#annotation-types) determines how annotations in the same namespace for a given message are aggregated into a summary. +The summarization method specified in the [annotation type](#annotation-types) determines how annotations in the same namespace for a given message are aggregated into a summary. A summary is constructed from the set of `annotation.create` and `annotation.delete` messages that have been published for a message. When [publishing](#publish) an annotation you can optionally specify a `name` which is used by some summarization methods to produce a summary. @@ -244,13 +279,13 @@ Ably provides five summarization methods to support different use cases which ar ### Total -The `total.v1` summarization method counts the number of annotations of a given type that were published for a message. +The `total.v1` summarization method counts the number of annotations of a given type that were [published](#publish) for a message. - +[Deleting](#delete) an annotation decrements the total count for that message. -The `total.v1` summarization method does not attribute counts to individual clients in the summary; it maintains only a simple counter per type and does not organize counts by name. +The `total.v1` summarization method does not attribute counts to individual clients in the summary; it maintains only a simple count per [annotation type](#annotation-types) and does not organize counts by name. + +If the same client publishes an annotation of a given type to the same message twice, the `total` count is incremented twice. ```javascript { @@ -260,15 +295,14 @@ The `total.v1` summarization method does not attribute counts to individual clie } ``` - ### 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. +The `flag.v1` summarization method counts how many distinct clients have [published](#publish) an annotation of a given type and maintains a list of those `clientId`s. + +[Deleting](#delete) 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. +A given client can contribute to the summary only once per [annotation type](#annotation-type). ```javascript { @@ -280,13 +314,13 @@ A given client can contribute to the summary only once per annotation type. ``` ### 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. +The `distinct.v1` summarization method counts how many unique clients have [published](#publish) an annotation with a given `name` for each annotation type along with the corresponding list of `clientId`s that published it. 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. @@ -305,13 +339,15 @@ A given client can contribute to the summary for a particular annotation `name` } ``` +[Deleting](#delete) 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`. + ### 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 pubilshed an annotation with that `name` along with the corresponding list of `clientId`s that published it. +The `unique.v1` summarization method counts how many unique clients have [published](#publish) 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 pubilshed an annotation with that `name` along with the corresponding list of `clientId`s that published it. 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. @@ -330,13 +366,15 @@ A given client can contribute to the summary for a particular annotation `name` } ``` +[Deleting](#delete) 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`. + ### 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. +The `multiple.v1` summarization method counts both a total and a per-client count of the number of annotations that were [published](#publish) 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. 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. @@ -363,4 +401,6 @@ If a client specifies a `count` when publishing an annotation, the client's cont } } } -``` \ No newline at end of file +``` + +[Deleting](#delete) an annotation removes all contributions made by that `clientId` for that `name`. From 151a351256aceccd319271abb43bc341a41539fc Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Tue, 3 Jun 2025 14:53:27 +0100 Subject: [PATCH 06/19] messages: call out additional annotation fields Link to the annotations docs that specify annotation message properties. --- content/messages/index.textile | 4 ++++ 1 file changed, 4 insertions(+) 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. From 124691c576a26ed1313a19309e322512c18a1409 Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Tue, 3 Jun 2025 14:54:50 +0100 Subject: [PATCH 07/19] pubsub: add annotations to further reading --- content/pub-sub/index.textile | 1 + 1 file changed, 1 insertion(+) 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 From 7e9697cc74c073e5cc8cf388a62b529efc5ac1a1 Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Tue, 3 Jun 2025 19:59:23 +0100 Subject: [PATCH 08/19] channels: document annotation modes & capabilities Documents the annotation modes ANNOTATION_SUBSCRIBE and ANNOTATION_PUBLISH and the associated capabilities annotation-subscribe and annotation-publish. --- content/auth/capabilities.textile | 2 ++ content/channels/options/index.textile | 4 ++++ .../core-features/_authentication_capabilities.textile | 2 ++ 3 files changed, 8 insertions(+) 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/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/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 From 04d600f622616d5d4f4de188012a79690a8591e1 Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Tue, 3 Jun 2025 20:23:30 +0100 Subject: [PATCH 09/19] annotations: (un)identified client clarifications Specifies which summarization methods unidentified clients may use. Describes how to require that all clients are identified via the Identified channel rule. --- src/pages/docs/messages/annotations.mdx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/pages/docs/messages/annotations.mdx b/src/pages/docs/messages/annotations.mdx index 30b91be149..8d6c634104 100644 --- a/src/pages/docs/messages/annotations.mdx +++ b/src/pages/docs/messages/annotations.mdx @@ -295,6 +295,12 @@ If the same client publishes an annotation of a given type to the same message t } ``` + + ### Flag @@ -314,7 +320,7 @@ A given client can contribute to the summary only once per [annotation type](#an ``` @@ -342,7 +348,7 @@ A given client can contribute to the summary for a particular annotation `name` [Deleting](#delete) 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`. ### Unique @@ -369,7 +375,7 @@ A given client can contribute to the summary for a particular annotation `name` [Deleting](#delete) 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`. ### Multiple @@ -404,3 +410,9 @@ If a client specifies a `count` when publishing an annotation, the client's cont ``` [Deleting](#delete) an annotation removes all contributions made by that `clientId` for that `name`. + + From 3c3b743c818d79ed3b9fda3896ec3d48fac1010b Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Fri, 13 Jun 2025 17:24:49 +0100 Subject: [PATCH 10/19] fixup! annotations: publish, subscribe, summary and type --- src/pages/docs/messages/annotations.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/docs/messages/annotations.mdx b/src/pages/docs/messages/annotations.mdx index 8d6c634104..c958026450 100644 --- a/src/pages/docs/messages/annotations.mdx +++ b/src/pages/docs/messages/annotations.mdx @@ -34,17 +34,17 @@ Annotations are a special type of message with the following properties: | Property | Description | | -------- | ----------- | -| id | An Ably-generated ID used to uniquely identify the message. | +| 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`). See [subscribing to annotation updates](#subscribe). | -| serial | This message's unique serial (lexicographically totally ordered). | +| 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 summarization methods for aggregation. See [annotation summaries](#annotation-summaries). | | clientId | The client identifier of the user that published this annotation. | | count | An optional count, only relevant to certain summarization methods. See [annotation summaries](#annotation-summaries) for more information. | | data | An optional payload for the annotation. Available on an [individual annotation](#subscribe-individual-annotations) but not aggregated or included in [annotation summaries](#annotation-summaries). | -| 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. | -| timestamp | The timestamp of when the message was received by Ably, as milliseconds since the Unix epoch. | +| 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. | ## Publishing annotations From 0402c4c16c0b7f97973195b6a98bb04381e2b8f2 Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Fri, 13 Jun 2025 17:39:03 +0100 Subject: [PATCH 11/19] fixup! annotations: delete annotations --- src/pages/docs/messages/annotations.mdx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/pages/docs/messages/annotations.mdx b/src/pages/docs/messages/annotations.mdx index c958026450..c331c704ce 100644 --- a/src/pages/docs/messages/annotations.mdx +++ b/src/pages/docs/messages/annotations.mdx @@ -166,6 +166,14 @@ You can also optionally specify a `name` for the annotation which is used by cer ```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', @@ -174,6 +182,14 @@ await channel.annotations.delete(message.serial, { ``` ```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', From e91438f040835bfcc92bf1812c47604f691afbc5 Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Fri, 13 Jun 2025 17:39:33 +0100 Subject: [PATCH 12/19] fixup! annotations: publish, subscribe, summary and type --- src/pages/docs/messages/annotations.mdx | 36 ++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/pages/docs/messages/annotations.mdx b/src/pages/docs/messages/annotations.mdx index c331c704ce..0e751beadf 100644 --- a/src/pages/docs/messages/annotations.mdx +++ b/src/pages/docs/messages/annotations.mdx @@ -216,6 +216,14 @@ The value of the `summary` field is an object where the keys are the [annotation ```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); @@ -224,6 +232,14 @@ await channel.subscribe((message) => { ``` ```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); @@ -240,12 +256,21 @@ For more details on summary structure and usage, see [annotation summaries](#ann ### Subscribing to individual annotations -For more fine-grained visibility, you can also subscribe directly to individual annotation events using the `annotations.subscribe()` method on a channel. +For more fine-grained visibility, you can also subscribe directly 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'] }); + await channel.annotations.subscribe((annotation) => { if (annotation.action === 'annotation.create') { console.log(`New ${annotation.type} annotation with name ${annotation.name} from ${annotation.clientId}`); @@ -256,6 +281,15 @@ await channel.annotations.subscribe((annotation) => { ``` ```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}`); From 1067985c4df7733bad477424bdee7bf7f3c6bfd1 Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Fri, 13 Jun 2025 17:54:25 +0100 Subject: [PATCH 13/19] fixup! annotations: publish, subscribe, summary and type --- src/pages/docs/messages/annotations.mdx | 57 ++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/src/pages/docs/messages/annotations.mdx b/src/pages/docs/messages/annotations.mdx index 0e751beadf..c74fc282b7 100644 --- a/src/pages/docs/messages/annotations.mdx +++ b/src/pages/docs/messages/annotations.mdx @@ -319,9 +319,62 @@ The annotation type is a string of the format `namespace:summarization.version` 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). +A separate summary is produced for each distinct [annotation type](#annotation-types). The summarization method specified in the [annotation type](#annotation-types) determines how annotations in the same namespace for a given message are aggregated into a summary. A summary is constructed from the set of `annotation.create` and `annotation.delete` messages that have been published for a message. + +You can [subscribe](#subscribe-annotation-summaries) to these summaries using the `subscribe()` method on a channel, and they will be delivered as messages with an `action` of `message.summary`. The summary will be included on the message's `summary` field, which is an object whose keys are the [annotation types](#annotation-types) and whose values describe the annotation summary for that type: + + +```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 + } + } +} +``` + -The summarization method specified in the [annotation type](#annotation-types) determines how annotations in the same namespace for a given message are aggregated into a summary. A summary is constructed from the set of `annotation.create` and `annotation.delete` messages that have been published for a message. When [publishing](#publish) an annotation you can optionally specify a `name` which is used by some summarization methods to produce a summary. From ef9bb63e40351d6c0ff91c6a37a1fc6430c44279 Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Mon, 16 Jun 2025 12:55:29 +0100 Subject: [PATCH 14/19] annotations: add annotations error codes Adds the error codes used for message annotations as defined in https://github.com/ably/ably-common/blob/main/protocol/errors.json --- content/channels/index.textile | 2 +- content/errors/codes.textile | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/content/channels/index.textile b/content/channels/index.textile index 398110899c..d2d05bb681 100644 --- a/content/channels/index.textile +++ b/content/channels/index.textile @@ -203,7 +203,7 @@ 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. -- Annotations := if enabled, allows use of message "annotations":/docs/annotations . +- Mutable messages := if enabled, allows use of message "annotations":/docs/annotations . To set a channel rule in the Ably dashboard: diff --git a/content/errors/codes.textile b/content/errors/codes.textile index 660b324146..70db2c2dfd 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 the Mutable Messages feature enabled + +This error occurs when attempting to use "message annotations":/docs/messages/annotations on a channel that does not have Mutable Messages enabled. + +*Resolution*: +* Create a "channel rule":/docs/channels#rules for the channel or channel namespace with Mutable Messages 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. From 9fa8c3e1126a6ddc1587c0a1929b018ae1108698 Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Mon, 16 Jun 2025 16:17:48 +0100 Subject: [PATCH 15/19] annotations: channel rule name & mark experimental Updates the name used for the channel rule that enabled mutable messages functionality to "Advanced message features" per ADR-144. Marks the annotations feature as Experimental and adds the appropriate caveats to the channel rule documentation. --- content/channels/index.textile | 16 ++++++++-------- src/pages/docs/messages/annotations.mdx | 12 ++++++++++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/content/channels/index.textile b/content/channels/index.textile index d2d05bb681..70c7f0c662 100644 --- a/content/channels/index.textile +++ b/content/channels/index.textile @@ -190,20 +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. -- Mutable messages := if enabled, allows use of message "annotations":/docs/annotations . +- 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/src/pages/docs/messages/annotations.mdx b/src/pages/docs/messages/annotations.mdx index c74fc282b7..911a078b4b 100644 --- a/src/pages/docs/messages/annotations.mdx +++ b/src/pages/docs/messages/annotations.mdx @@ -6,6 +6,10 @@ languages: - nodejs --- + + Message annotations allow 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 @@ -18,13 +22,17 @@ When clients publish or delete annotations, Ably automatically creates a [summar ## Enable annotations -Annotations can be enabled for a channel or channel namespace with the *Annotations* channel rule. +Annotations can be enabled for a channel or channel namespace with the *Advanced message features* 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 **Annotations** to enable message annotations. +5. Check **Advanced message features** to enable message annotations. 6. Click **Create channel rule** to save. From 847e99269f974ff961b4e0d4131f38efb8865538 Mon Sep 17 00:00:00 2001 From: Mark Hulbert <39801222+m-hulbert@users.noreply.github.com> Date: Thu, 26 Jun 2025 19:51:52 +0200 Subject: [PATCH 16/19] annotations: restructure annotations page to split summaries and individual events --- src/pages/docs/messages/annotations.mdx | 501 +++++++++++------------- 1 file changed, 227 insertions(+), 274 deletions(-) diff --git a/src/pages/docs/messages/annotations.mdx b/src/pages/docs/messages/annotations.mdx index 911a078b4b..dd1c88638f 100644 --- a/src/pages/docs/messages/annotations.mdx +++ b/src/pages/docs/messages/annotations.mdx @@ -1,24 +1,20 @@ --- -title: Message Annotations +title: Message annotations meta_description: "Annotate messages on a channel with additional metadata." -languages: - - javascript - - nodejs --- -Message annotations allow clients to add metadata to existing messages on a channel. You can use annotations to implement features like: +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 like "important" or "urgent" -- **Community moderation** - Flag inappropriate content for review -- **Read receipts** - Mark messages as "read" or "delivered" - -When clients publish or delete annotations, Ably automatically creates a [summary](#annotation-summaries) that provides an aggregated view of all annotations for a message. +* **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 @@ -35,39 +31,147 @@ Note that when advanced messaging is enabled, messages are [persisted](/docs/sto 5. Check **Advanced message features** to enable message annotations. 6. Click **Create channel rule** to save. +## Annotation types -## Annotation properties +Annotation types determine how annotations are processed and aggregated into [summaries](#annotation-summaries). -Annotations are a special type of message with the following properties: +The annotation type is a string of the format `namespace:summarization.version` where: -| 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`). See [subscribing to annotation updates](#subscribe). | -| 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 summarization methods for aggregation. See [annotation summaries](#annotation-summaries). | -| clientId | The client identifier of the user that published this annotation. | -| count | An optional count, only relevant to certain summarization methods. See [annotation summaries](#annotation-summaries) for more information. | -| data | An optional payload for the annotation. Available on an [individual annotation](#subscribe-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. | +* `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 -## Publishing annotations +The `total.v1` summarization method counts the number of annotations of a given type that were published for a message. -To publish an annotation for a message, use the `annotations.publish()` method on a channel, passing 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`. +Deleting an annotation decrements the total count for that message. -The `clientId` specified in the [client options](/docs/api/realtime-sdk#client-options) will be associated with the published annotation. +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. -When publishing an annotation, specify the [annotation type](#annotation-types) on the `type` field of the annotation object. + +```json +{ + "metrics:total.v1": { + "total": 42 + } +} +``` + + +### Flag -You can also optionally specify a `name` for the annotation which is used by certain [summarization methods](#annotation-summaries) that aggregate by `name`. +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 @@ -76,7 +180,7 @@ You can also optionally specify a `name` for the annotation which is used by cer 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 +// which has message annotations enabled const channel = realtime.channels.get('annotations:example'); // Publish an annotation for a message that flags it as delivered @@ -87,7 +191,7 @@ await channel.annotations.publish(message, { // You can also use a message's serial number await channel.annotations.publish(message.serial, { - type: 'receipts:flag.v1', + type: 'receipts:flag.v1', name: 'delivered' }); ``` @@ -98,7 +202,7 @@ await channel.annotations.publish(message.serial, { 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 +// which has message annotations enabled const channel = realtime.channels.get('annotations:example'); // Publish an annotation for a message that flags it as delivered @@ -109,33 +213,13 @@ await channel.annotations.publish(message, { // You can also use a message's serial number await channel.annotations.publish(message.serial, { - type: 'receipts:flag.v1', + type: 'receipts:flag.v1', name: 'delivered' }); ``` -You can also specify a payload on the `data` field when publishing an annotation. While the `data` payload is available on the [individual annotation](#subscribe-individual-annotations) events, it is not included in [annotation summaries](#annotation-summaries). - - -```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!' -}); -``` - - -When using the `multiple.v1` summarization method you can optionally specify a `count` by which to increment this client's contribution to the summary: +When using `multiple.v1`, you can optionally specify a `count` by which to increment a client's contribution to the summary: ```javascript @@ -155,22 +239,15 @@ await channel.annotations.publish(message.serial, { ``` +## Delete annotations -## Deleting 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`. -To delete a published annotation, use the `annotations.delete()` method on a channel, passing either a [message](/docs/messages) instance or the `serial` of the message to annotate. This method will publish a delete annotation message with an action of `annotation.delete`. - -Deleting an annotation does not remove the original annotation that was published. Instead, annotation delete messages affect the [annotation summary](#annotation-summaries) for that message by removing the contribution as specified by the delete annotation. +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. - - -When publishing a delete annotation, specify the [annotation type](#annotation-types) on the `type` field of the annotation object. - -You can also optionally specify a `name` for the annotation which is used by certain [summarization methods](#annotation-summaries) that aggregate by `name`. +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 @@ -179,7 +256,7 @@ You can also optionally specify a `name` for the annotation which is used by cer 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 +// which has message annotations enabled const channel = realtime.channels.get('annotations:example'); // Delete a 'delivered' annotation @@ -195,7 +272,7 @@ await channel.annotations.delete(message.serial, { 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 +// which has message annotations enabled const channel = realtime.channels.get('annotations:example'); // Delete a 'delivered' annotation @@ -206,21 +283,17 @@ await channel.annotations.delete(message.serial, { ``` +## Subscribe to annotations -## Subscribing to annotation updates - -There are two ways to subscribe to annotation updates: +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. -1. **Annotation summaries** (recommended) - Receive aggregated summaries of all annotations for a message -2. **Individual annotations** - Receive individual events when annotations are 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. -### Subscribing to annotation summaries +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. -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 annotations are 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 exact structure of the value of each key depends on the [summarization method](#annotation-summaries) specified in the annotation type. + ```javascript @@ -229,7 +302,7 @@ The value of the `summary` field is an object where the keys are the [annotation 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 +// which has message annotations enabled const channel = realtime.channels.get('annotations:example'); await channel.subscribe((message) => { @@ -245,7 +318,7 @@ await channel.subscribe((message) => { 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 +// which has message annotations enabled const channel = realtime.channels.get('annotations:example'); await channel.subscribe((message) => { @@ -256,80 +329,13 @@ await channel.subscribe((message) => { ``` - - -For more details on summary structure and usage, see [annotation summaries](#annotation-summaries). - -### Subscribing to individual annotations - -For more fine-grained visibility, you can also subscribe directly 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'] }); - -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 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 the summarization method (e.g. `flag`) which determines how annotations are aggregated to produce [summaries](#annotation-summaries). -- `version` specifies the version component of the summarization method (e.g. `v1`), which allows for future changes to summarization behavior. - -## Annotation summaries +### 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](#annotation-types) determines how annotations in the same namespace for a given message are aggregated into a summary. A summary is constructed from the set of `annotation.create` and `annotation.delete` messages that have been published for a 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`). -You can [subscribe](#subscribe-annotation-summaries) to these summaries using the `subscribe()` method on a channel, and they will be delivered as messages with an `action` of `message.summary`. The summary will be included on the message's `summary` field, which is an object whose keys are the [annotation types](#annotation-types) and whose values describe the annotation summary for that type: +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 @@ -383,147 +389,94 @@ You can [subscribe](#subscribe-annotation-summaries) to these summaries using th ``` +## Individual annotation events -When [publishing](#publish) an annotation you can optionally specify a `name` which is used by some summarization methods to produce a summary. +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. -Ably provides five summarization methods to support different use cases which are described below. +Individual events can be useful for activity feeds or detailed logging, however annotation summaries are generally more reliable and efficient for maintaining UI state. -### Total - -The `total.v1` summarization method counts the number of annotations of a given type that were [published](#publish) for a message. - -[Deleting](#delete) an annotation decrements the total count for that message. +### Publish individual annotation events -The `total.v1` summarization method does not attribute counts to individual clients in the summary; it maintains only a simple count per [annotation type](#annotation-types) and does not organize counts by name. - -If the same client publishes an annotation of a given type to the same message twice, the `total` count is incremented twice. +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 -{ - "metrics:total.v1": { - "total": 42 - } -} +await channel.annotations.publish(message.serial, { + type: 'receipts:flag.v1', + name: 'delivered', + data: 'Message delivered!' +}); ``` - - - -### Flag - -The `flag.v1` summarization method counts how many distinct clients have [published](#publish) an annotation of a given type and maintains a list of those `clientId`s. - -[Deleting](#delete) 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](#annotation-type). - -```javascript -{ - "reactions:flag.v1": { - "total": 3, - "clientIds": ["client1", "client2", "client3"] - } -} +```nodejs +await channel.annotations.publish(message.serial, { + type: 'receipts:flag.v1', + name: 'delivered', + data: 'Message delivered!' +}); ``` + - - +### Subscribe to individual annotations -### Distinct +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). -The `distinct.v1` summarization method counts how many unique clients have [published](#publish) an annotation with a given `name` for each annotation type along with the corresponding list of `clientId`s that published it. - -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. +Annotations delivered to the `annotations.subscribe()` listener will have an `action` of `annotation.create` or `annotation.delete`. + ```javascript -{ - "categories:distinct.v1": { - "important": { - "total": 2, - "clientIds": ["client1", "client3"] - }, - "urgent": { - "total": 3, - "clientIds": ["client1", "client2", "client3"] - } - } -} -``` - -[Deleting](#delete) 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`. - - - -### Unique - -The `unique.v1` summarization method counts how many unique clients have [published](#publish) 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 pubilshed an annotation with that `name` along with the corresponding list of `clientId`s that published it. +// 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' }); -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. +// 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'] }); -```javascript -{ - "status:unique.v1": { - "important": { - "total": 2, - "clientIds": ["client1", "client3"] - }, - "urgent": { - "total": 1, - "clientIds": ["client2"] - } +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}`); } -} +}); ``` -[Deleting](#delete) 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`. - - - -### Multiple - -The `multiple.v1` summarization method counts both a total and a per-client count of the number of annotations that were [published](#publish) 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. - -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. +```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' }); -If a client specifies a `count` when publishing an annotation, the client's contribution to the summary is incremented by the specified value. +// 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'] }); -```javascript -{ - "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 - } +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}`); } -} +}); ``` + -[Deleting](#delete) an annotation removes all contributions made by that `clientId` for that `name`. +### Annotation message 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. | From 0b29a4e7ad0feae3404f290224b23010c0ed696e Mon Sep 17 00:00:00 2001 From: Mark Hulbert <39801222+m-hulbert@users.noreply.github.com> Date: Wed, 9 Jul 2025 12:40:58 +0200 Subject: [PATCH 17/19] fixup! annotations: restructure annotations page to split summaries and individual events --- src/pages/docs/messages/annotations.mdx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/docs/messages/annotations.mdx b/src/pages/docs/messages/annotations.mdx index dd1c88638f..896d09ffe4 100644 --- a/src/pages/docs/messages/annotations.mdx +++ b/src/pages/docs/messages/annotations.mdx @@ -421,6 +421,10 @@ await channel.annotations.publish(message.serial, { 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`. @@ -432,7 +436,7 @@ 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'] }); +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') { From 3539bc7c3b9f5fef6c391af7fe5292003ab0f823 Mon Sep 17 00:00:00 2001 From: Mark Hulbert <39801222+m-hulbert@users.noreply.github.com> Date: Wed, 9 Jul 2025 12:53:50 +0200 Subject: [PATCH 18/19] fixup! annotations: channel rule name & mark experimental --- src/pages/docs/messages/annotations.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/docs/messages/annotations.mdx b/src/pages/docs/messages/annotations.mdx index 896d09ffe4..8184f3a42b 100644 --- a/src/pages/docs/messages/annotations.mdx +++ b/src/pages/docs/messages/annotations.mdx @@ -18,17 +18,17 @@ When clients publish or delete an annotation, Ably automatically creates a [summ ## Enable annotations -Annotations can be enabled for a channel or channel namespace with the *Advanced message features* channel rule. +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 **Advanced message features** to enable message annotations. +5. Check **Message annotations, updates, and deletes** to enable message annotations. 6. Click **Create channel rule** to save. ## Annotation types @@ -422,7 +422,7 @@ await channel.annotations.publish(message.serial, { 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`. From 612c0c0fab5775d47ee09785309693f9fc053cf0 Mon Sep 17 00:00:00 2001 From: Mark Hulbert <39801222+m-hulbert@users.noreply.github.com> Date: Wed, 9 Jul 2025 13:08:10 +0200 Subject: [PATCH 19/19] fixup! annotations: add annotations error codes --- content/errors/codes.textile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/content/errors/codes.textile b/content/errors/codes.textile index 70db2c2dfd..c980e074f3 100644 --- a/content/errors/codes.textile +++ b/content/errors/codes.textile @@ -764,12 +764,12 @@ This error occurs when attempting to "subscribe to individual annotations":/docs *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 the Mutable Messages feature enabled +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 Mutable Messages 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 Mutable Messages enabled. +* 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