From c540f76130a0d5b4845bf720247ea8dcca3b85e9 Mon Sep 17 00:00:00 2001 From: Mike Christensen Date: Thu, 11 Sep 2025 17:22:54 +0100 Subject: [PATCH 1/6] Add annotations docs --- .../_authentication_capabilities.textile | 2 + src/data/nav/pubsub.ts | 4 + src/pages/docs/auth/capabilities.mdx | 2 + src/pages/docs/channels/index.mdx | 1 + src/pages/docs/channels/options/index.mdx | 4 + src/pages/docs/messages/annotations.mdx | 486 ++++++++++++++++++ src/pages/docs/messages/index.mdx | 4 + src/pages/docs/platform/errors/codes.mdx | 14 + src/pages/docs/pub-sub/index.mdx | 3 + 9 files changed, 520 insertions(+) create mode 100644 src/pages/docs/messages/annotations.mdx diff --git a/content/partials/core-features/_authentication_capabilities.textile b/content/partials/core-features/_authentication_capabilities.textile index 51ef064fb0..10f21c78d3 100644 --- a/content/partials/core-features/_authentication_capabilities.textile +++ b/content/partials/core-features/_authentication_capabilities.textile @@ -5,6 +5,8 @@ The following capability operations are available for API keys and issued tokens - presence := can register presence on a channel (enter, update and leave) - object-subscribe := can subscribe to updates to objects on a channel - object-publish := can update objects on a channel +- annotation-subscribe := can subscribe to individual annotations on a channel +- annotation-publish := can publish annotations to messages on a channel - history := can retrieve message and presence state history on channels - stats := can retrieve current and historical usage statistics for an app - push-subscribe := can subscribe devices for push notifications diff --git a/src/data/nav/pubsub.ts b/src/data/nav/pubsub.ts index efd127b762..3d949f045d 100644 --- a/src/data/nav/pubsub.ts +++ b/src/data/nav/pubsub.ts @@ -195,6 +195,10 @@ export default { name: 'Message batching', link: '/docs/messages/batch', }, + { + name: 'Message annotations', + link: '/docs/messages/annotations', + }, ], }, { diff --git a/src/pages/docs/auth/capabilities.mdx b/src/pages/docs/auth/capabilities.mdx index fd25089dc0..1551dac175 100644 --- a/src/pages/docs/auth/capabilities.mdx +++ b/src/pages/docs/auth/capabilities.mdx @@ -40,6 +40,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/src/pages/docs/channels/index.mdx b/src/pages/docs/channels/index.mdx index d326c89279..096f84435a 100644 --- a/src/pages/docs/channels/index.mdx +++ b/src/pages/docs/channels/index.mdx @@ -200,6 +200,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. | +| Message annotations, updates, and deletes | If enabled, allows message "annotations":/docs/annotations to be used, as well as updates and deletes to be published to messages. Note that these features are currently Experimental and its features are still in development and subject to change. When this feature is enabled, messages will be "persisted":/docs/storage-history/storage#all-message-persistence (necessary in order from them later be annotated or updated), and "continuous history":/docs/storage-history/history#continuous-history features will unfortunately not work (yet). To set a channel rule in the Ably dashboard: diff --git a/src/pages/docs/channels/options/index.mdx b/src/pages/docs/channels/options/index.mdx index ec53601110..7f08fe1c02 100644 --- a/src/pages/docs/channels/options/index.mdx +++ b/src/pages/docs/channels/options/index.mdx @@ -438,6 +438,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. @@ -450,6 +452,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/src/pages/docs/messages/annotations.mdx b/src/pages/docs/messages/annotations.mdx new file mode 100644 index 0000000000..8184f3a42b --- /dev/null +++ b/src/pages/docs/messages/annotations.mdx @@ -0,0 +1,486 @@ +--- +title: Message annotations +meta_description: "Annotate messages on a channel with additional metadata." +--- + + + +Message annotations enable clients to add metadata to existing messages on a channel. You can use annotations to implement features like: + +* **Message reactions** - add emoji reactions (👍, ❤️, 😂) to messages +* **Content categorization** - tag messages with categories such as "important" or "urgent" +* **Community moderation** - flag inappropriate content for review +* **Read receipts** - mark messages as "read" or "delivered" + +When clients publish or delete an annotation, Ably automatically creates a [summary](#annotation-summaries) that provides an aggregated view of all annotations for that message. + +## Enable annotations + +Annotations can be enabled for a channel or channel namespace with the *Message annotations, updates, and deletes* channel rule. + + + +1. On your [dashboard](https://ably.com/accounts/any), select one of your apps. +2. Go to **Settings**. +3. Under [channel rules](/docs/channels#rules), click **Add new rule**. +4. Enter the channel name or channel namespace on which to enable message annotations. +5. Check **Message annotations, updates, and deletes** to enable message annotations. +6. Click **Create channel rule** to save. + +## Annotation types + +Annotation types determine how annotations are processed and aggregated into [summaries](#annotation-summaries). + +The annotation type is a string of the format `namespace:summarization.version` where: + +* `namespace` is a string (e.g. `reactions`) that groups related annotations. Only annotations in the same namespace will be aggregated together to produce [summaries](#annotation-summaries). +* `summarization` specifies how annotations are aggregated to produce [summaries](#annotation-summaries), such as `total`, `flag`, `distinct`, `unique`, or `multiple`. +* `version` specifies the version component which allows for future changes to summarization behavior. + +### Total + +The `total.v1` summarization method counts the number of annotations of a given type that were published for a message. + +Deleting an annotation decrements the total count for that message. + +Using `total.v1` does not attribute counts to individual clients in the summary; it maintains only a simple count per annotation type and does not organize counts by name. [Unidentified](/docs/auth/identified-clients#unidentified) clients can publish `total.v1` annotations. Use the [identified channel rule](/docs/channels#rules) if you want to prevent unidentified clients from publishing annotations. + +If the same client publishes an annotation of a given type to the same message twice, the `total` count is incremented twice. + + +```json +{ + "metrics:total.v1": { + "total": 42 + } +} +``` + + +### Flag + +The `flag.v1` summarization method counts how many distinct clients have published an annotation of a given type and maintains a list of those `clientId`s. Clients must be [identified](/docs/auth/identified-clients) to publish `flag.v1` annotations. + +Deleting an annotation decrements the total count for that message and removes the `clientId` from the list of clients that contributed to the summary. + +A given client can contribute to the summary only once per annotation type. + + +```json +{ + "reactions:flag.v1": { + "total": 3, + "clientIds": ["client1", "client2", "client3"] + } +} +``` + + +### Distinct + +The `distinct.v1` summarization method counts how many unique clients have published an annotation with a given `name` for each annotation type along with the corresponding list of `clientId`s that published it. Clients must be [identified](/docs/auth/identified-clients) to publish `distinct.v1` annotations. + +A given client can contribute to the summary for a particular annotation `name` only once, but the same client may publish additional annotations with different `name`s. + +Deleting an annotation removes the `clientId` from the list of clients that contributed to the summary for that `name`, and decrements the total count for that `name`. + + +```json +{ + "categories:distinct.v1": { + "important": { + "total": 2, + "clientIds": ["client1", "client3"] + }, + "urgent": { + "total": 3, + "clientIds": ["client1", "client2", "client3"] + } + } +} +``` + + +### Unique + +The `unique.v1` summarization method counts how many unique clients have published an annotation with a given `name` for each annotation type, while guaranteeing that each client contributes to the summary for only one `name` at a time. The summary for each annotation `name` holds a `total` count of the number of distinct clients that have published an annotation with that `name` along with the corresponding list of `clientId`s that published it. Clients must be [identified](/docs/auth/identified-clients) to publish `unique.v1` annotations. + +A given client can contribute to the summary for a particular annotation `name` only once. Publishing an annotation with a different `name` automatically removes that client from the summary for the previous `name` and adds them to the new one, updating the affected total values and list of `clientId`s. + +Deleting an annotation removes the `clientId` from the list of clients that contributed to the summary for that `name`, and decrements the total count for that `name`. + + +```json +{ + "status:unique.v1": { + "important": { + "total": 2, + "clientIds": ["client1", "client3"] + }, + "urgent": { + "total": 1, + "clientIds": ["client2"] + } + } +} +``` + + +### Multiple + +The `multiple.v1` summarization method counts both a total and a per-client count of the number of annotations that were published with a given `name` for each annotation type. Additionally it includes a count for the number of annotations published with a given `name` from unidentified clients. Use the [identified channel rule](/docs/channels#rules) if you want to prevent unidentified clients from publishing annotations. + +A given client can contribute to the summary for a particular annotation `name` multiple times. The same client may also publish additional annotations with different `name`s. + +If a client specifies a `count` when publishing an annotation, the client's contribution to the summary is incremented by the specified value. + +Deleting an annotation removes all contributions made by that `clientId` for that `name`. + + +```json +{ + "voting:multiple.v1": { + "option-a": { + "total": 7, + "clientCounts": { + "client1": 3, + "client2": 2 + }, + "totalUnidentified": 2 + }, + "option-b": { + "total": 4, + "clientCounts": { + "client1": 2, + "client3": 1 + }, + "totalUnidentified": 1 + } + } +} +``` + + +## Publish annotations + +To publish an annotation for a message, use the `annotations.publish()` method on a channel. Pass in either a [message](/docs/messages) instance or the `serial` of the message to annotate. This method will publish an annotation message with an action of `annotation.create`. + +The `clientId` specified in the [client options](/docs/api/realtime-sdk#client-options) will be associated with the published annotation. Note that certain annotation types require the client to be identified with a `clientId` in order to publish annotations. + +Specify the [annotation type](#annotation-types) using the `type` field of the annotation object, and optionally specify a `name` for the annotation. The `name` is used to aggregate certain annotations when producing an [annotation summary](#annotation-summaries). + + +```javascript +// Create an Ably Realtime client specifying the clientId that will +// be associated with annotations published by this client +const realtime = new Ably.Realtime({ key: '{{API_KEY}}', clientId: 'my-client-id' }); + +// Create a channel in a namespace called `annotations` +// which has message annotations enabled +const channel = realtime.channels.get('annotations:example'); + +// Publish an annotation for a message that flags it as delivered +await channel.annotations.publish(message, { + type: 'receipts:flag.v1', + name: 'delivered' +}); + +// You can also use a message's serial number +await channel.annotations.publish(message.serial, { + type: 'receipts:flag.v1', + name: 'delivered' +}); +``` + +```nodejs +// Create an Ably Realtime client specifying the clientId that will +// be associated with annotations published by this client +const realtime = new Ably.Realtime({ key: '{{API_KEY}}', clientId: 'my-client-id' }); + +// Create a channel in a namespace called `annotations` +// which has message annotations enabled +const channel = realtime.channels.get('annotations:example'); + +// Publish an annotation for a message that flags it as delivered +await channel.annotations.publish(message, { + type: 'receipts:flag.v1', + name: 'delivered' +}); + +// You can also use a message's serial number +await channel.annotations.publish(message.serial, { + type: 'receipts:flag.v1', + name: 'delivered' +}); +``` + + +When using `multiple.v1`, you can optionally specify a `count` by which to increment a client's contribution to the summary: + + +```javascript +await channel.annotations.publish(message.serial, { + type: 'rating:multiple.v1', + name: 'stars', + count: 4 +}); +``` + +```nodejs +await channel.annotations.publish(message.serial, { + type: 'rating:multiple.v1', + name: 'stars', + count: 4 +}); +``` + + +## Delete annotations + +To delete an annotation, use the `annotations.delete()` method on a channel. Pass in either a [message](/docs/messages) instance or the `serial` of the message to annotate. This method will publish an annotation message with an action of `annotation.delete`. + +Deleting an annotation does not remove the original annotation that was published. Instead, they affect the [annotation summary](#annotation-summaries) for that message by removing the contribution specified by the annotation. + +The `clientId` specified in the [client options](/docs/api/realtime-sdk#client-options) will be associated with the published delete annotation. + +Specify the [annotation type](#annotation-types) using the `type` field of the annotation object, and optionally specify a `name` for the annotation. The `name` is used to aggregate certain annotations when producing an annotation summary. + + +```javascript +// Create an Ably Realtime client specifying the clientId that will +// be associated with annotations published by this client +const realtime = new Ably.Realtime({ key: '{{API_KEY}}', clientId: 'my-client-id' }); + +// Create a channel in a namespace called `annotations` +// which has message annotations enabled +const channel = realtime.channels.get('annotations:example'); + +// Delete a 'delivered' annotation +await channel.annotations.delete(message.serial, { + type: 'receipts:flag.v1', + name: 'delivered' +}); +``` + +```nodejs +// Create an Ably Realtime client specifying the clientId that will +// be associated with annotations published by this client +const realtime = new Ably.Realtime({ key: '{{API_KEY}}', clientId: 'my-client-id' }); + +// Create a channel in a namespace called `annotations` +// which has message annotations enabled +const channel = realtime.channels.get('annotations:example'); + +// Delete a 'delivered' annotation +await channel.annotations.delete(message.serial, { + type: 'receipts:flag.v1', + name: 'delivered' +}); +``` + + +## Subscribe to annotations + +The recommended way to receive annotation updates is through annotation summaries. These events provide a summary of the complete, current state of all annotations for a message whenever an annotation is published or deleted. + +Annotation summaries are delivered to subscribers as messages with an `action` of `message.summary`. These messages have a `summary` field which provides a summary of all the annotations for the message, identified by the `serial` field on the summary message. + +The value of the `summary` field is an object where the keys are the [annotation types](#annotation-types). The structure of the value of each key depends on the summarization method used, for example `total.v1` will have a `total` field, while `flag.v1` will have `total` and `clientIds` fields. + + + + +```javascript +// Create an Ably Realtime client specifying the clientId that will +// be associated with annotations published by this client +const realtime = new Ably.Realtime({ key: '{{API_KEY}}', clientId: 'my-client-id' }); + +// Create a channel in a namespace called `annotations` +// which has message annotations enabled +const channel = realtime.channels.get('annotations:example'); + +await channel.subscribe((message) => { + if (message.action === 'message.summary') { + console.log(message.summary); + } +}); +``` + +```nodejs +// Create an Ably Realtime client specifying the clientId that will +// be associated with annotations published by this client +const realtime = new Ably.Realtime({ key: '{{API_KEY}}', clientId: 'my-client-id' }); + +// Create a channel in a namespace called `annotations` +// which has message annotations enabled +const channel = realtime.channels.get('annotations:example'); + +await channel.subscribe((message) => { + if (message.action === 'message.summary') { + console.log(message.summary); + } +}); +``` + + +### Annotation summaries + +When annotations for a message are published, Ably automatically generates a summary that provides an aggregated view of all annotations for that message. + +A separate summary is produced for each distinct [annotation type](#annotation-types). The summarization method specified in the annotation type determines how annotations in the same namespace for a given message are aggregated into a summary. A summary is constructed from the set of [individual annotation events](#individual-annotations) (annotation messages with an `action` of `annotation.create` or `annotation.delete`). + +The summary will be included in the message's `summary` field, which is an object whose keys are the annotation types and whose values describe the annotation summary for that type. For example: + + +```json +{ + "metrics:total.v1": { + "total": 42 + }, + "reactions:flag.v1": { + "total": 3, + "clientIds": ["client1", "client2", "client3"] + }, + "categories:distinct.v1": { + "important": { + "total": 2, + "clientIds": ["client1", "client3"] + }, + "urgent": { + "total": 3, + "clientIds": ["client1", "client2", "client3"] + } + }, + "status:unique.v1": { + "important": { + "total": 2, + "clientIds": ["client1", "client3"] + }, + "urgent": { + "total": 1, + "clientIds": ["client2"] + } + }, + "voting:multiple.v1": { + "option-a": { + "total": 7, + "clientCounts": { + "client1": 3, + "client2": 2 + }, + "totalUnidentified": 2 + }, + "option-b": { + "total": 4, + "clientCounts": { + "client1": 2, + "client3": 1 + }, + "totalUnidentified": 1 + } + } +} +``` + + +## Individual annotation events + +It is also possible to subscribe to individual annotation events, rather than annotation summaries. These are the emitted when [publishing](#publish) or [deleting](#delete) an annotation. + +Individual events can be useful for activity feeds or detailed logging, however annotation summaries are generally more reliable and efficient for maintaining UI state. + +### Publish individual annotation events + +Publishing annotations is the [same as for summaries](#publish). The only difference is that you can additionally specify a `data` payload when publishing an annotation, which isn't included in an annotation summary. + + +```javascript +await channel.annotations.publish(message.serial, { + type: 'receipts:flag.v1', + name: 'delivered', + data: 'Message delivered!' +}); +``` + +```nodejs +await channel.annotations.publish(message.serial, { + type: 'receipts:flag.v1', + name: 'delivered', + data: 'Message delivered!' +}); +``` + + +### Subscribe to individual annotations + +Subscribe to individual annotation events using the `annotations.subscribe()` method on a channel. To subscribe to individual annotations, you must request the `ANNOTATION_SUBSCRIBE` [mode](/docs/channels/options#modes). + + + +Annotations delivered to the `annotations.subscribe()` listener will have an `action` of `annotation.create` or `annotation.delete`. + + +```javascript +// Create an Ably Realtime client specifying the clientId that will +// be associated with annotations published by this client +const realtime = new Ably.Realtime({ key: '{{API_KEY}}', clientId: 'my-client-id' }); + +// Create a channel in a namespace called `annotations` +// which has message annotations enabled. +// Specify the ANNOTATION_SUBSCRIBE mode to enable annotation subscriptions. +const channel = realtime.channels.get('annotations:example', { modes: ['ANNOTATION_SUBSCRIBE', ... /* all other modes you need, such as MESSAGE_SUBSCRIBE */] }); + +await channel.annotations.subscribe((annotation) => { + if (annotation.action === 'annotation.create') { + console.log(`New ${annotation.type} annotation with name ${annotation.name} from ${annotation.clientId}`); + } else if (annotation.action === 'annotation.delete') { + console.log(`${annotation.clientId} deleted a ${annotation.type} annotation with name ${annotation.name}`); + } +}); +``` + +```nodejs +// Create an Ably Realtime client specifying the clientId that will +// be associated with annotations published by this client +const realtime = new Ably.Realtime({ key: '{{API_KEY}}', clientId: 'my-client-id' }); + +// Create a channel in a namespace called `annotations` +// which has message annotations enabled. +// Specify the ANNOTATION_SUBSCRIBE mode to enable annotation subscriptions. +const channel = realtime.channels.get('annotations:example', { modes: ['ANNOTATION_SUBSCRIBE'] }); + +await channel.annotations.subscribe((annotation) => { + if (annotation.action === 'annotation.create') { + console.log(`New ${annotation.type} annotation with name ${annotation.name} from ${annotation.clientId}`); + } else if (annotation.action === 'annotation.delete') { + console.log(`${annotation.clientId} deleted a ${annotation.type} annotation with name ${annotation.name}`); + } +}); +``` + + +### Annotation message properties + +Annotations are a special type of message with the following properties: + +| Property | Description | +| -------- | ----------- | +| id | An Ably-generated ID used to uniquely identify the annotation. | +| action | The action specifies whether this is an annotation being added (`annotation.create`) or removed (`annotation.delete`). | +| serial | This annotation's unique serial (lexicographically totally ordered). | +| messageSerial | The serial of the message that this annotation is annotating. | +| type | The [annotation type](#annotation-types). | +| name | The name of the annotation, used by some [annotation types](#annotation-types) for aggregation. | +| clientId | The client identifier of the user that published this annotation. | +| count | An optional count, only relevant to certain [annotation types](#annotation-types). | +| data | An optional payload for the annotation. Available on an [individual annotation](#individual-annotations) but not aggregated or included in [annotation summaries](#annotation-summaries). | +| encoding | This is typically empty, as all annotations received from Ably are automatically decoded client-side using this value. However, if the annotation encoding cannot be processed, this attribute contains the remaining transformations not applied to the `data` payload. | +| timestamp | The timestamp of when the annotation was received by Ably, as milliseconds since the Unix epoch. | diff --git a/src/pages/docs/messages/index.mdx b/src/pages/docs/messages/index.mdx index e30609c3ce..522169e1cf 100644 --- a/src/pages/docs/messages/index.mdx +++ b/src/pages/docs/messages/index.mdx @@ -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 | + + ## Message conflation Use message conflation to ensure that clients only ever receive the most up-to-date message, by removing redundant and outdated messages. Message conflation will aggregate published messages for a set period of time and evaluate all messages against a [conflation key](#routing). All but the latest message for each conflation key value will be discarded, and the resulting message, or messages, will be delivered to subscribers as a single batch once the period of time elapses. diff --git a/src/pages/docs/platform/errors/codes.mdx b/src/pages/docs/platform/errors/codes.mdx index 0e08d4d0b6..8af4bf8d46 100644 --- a/src/pages/docs/platform/errors/codes.mdx +++ b/src/pages/docs/platform/errors/codes.mdx @@ -765,6 +765,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. +## 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. + +## 93002: Annotations are only supported on channels with message annotations, updates, and deletes enabled + +This error occurs when attempting to use [message annotations](/docs/messages/annotations) on a channel that does not have them enabled. + +**Resolution:** +* Create a [channel rule](/docs/channels#rules) for the channel or channel namespace with **Message annotations, updates, and deletes** enabled. + ## 101000: Space name missing This error occurs when calling [`spaces.get()`](/docs/spaces/space#options) without specifying a space name. The name parameter is required to retrieve a space. diff --git a/src/pages/docs/pub-sub/index.mdx b/src/pages/docs/pub-sub/index.mdx index e7a9eaeae2..bbf4218150 100644 --- a/src/pages/docs/pub-sub/index.mdx +++ b/src/pages/docs/pub-sub/index.mdx @@ -441,4 +441,7 @@ if err := channel.Publish(context.Background(), "example", "message data"); err You can find out more detail about how [channels](/docs/channels) and [messages](/docs/messages) work. There are also more advanced ways that you can [subscribe](/docs/pub-sub/advanced#subscribe) to channels, and [publish](/docs/pub-sub/advanced#publish) messages, such as applying filters to your subscriptions or having a server publish messages on behalf of a client. + +[Annotate](/docs/messages/annotations) messages to add reactions, categorization, and other metadata to them. + From 2f01a7555f0bd907a330d2faab886b9891243a53 Mon Sep 17 00:00:00 2001 From: Simon Woolf Date: Thu, 11 Sep 2025 18:44:23 +0100 Subject: [PATCH 2/6] Add .claude to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b1ba4f8b95..11c7f8ca85 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ config/nginx-redirects.conf src/gatsby-types.d.ts .idea/* **/*.swp +.claude From 12abbdc7f2f66ce206410374ad9790452bb5fd98 Mon Sep 17 00:00:00 2001 From: Simon Woolf Date: Thu, 11 Sep 2025 18:45:26 +0100 Subject: [PATCH 3/6] Update message / annotations docs for protocol v4 messages And remove references to `version` for now until we add the update/delete docs. Our docs will just unconditionally use protocol v4 message docs for annotations and updates, since by the time we release those features experimentally all 3 sdks which had support for them will be updated to v4. --- content/api/realtime-sdk/messages.textile | 5 +++++ content/api/realtime-sdk/types.textile | 6 +++--- content/partials/types/_message.textile | 20 +++++-------------- .../types/_message_annotations.textile | 7 +++++++ content/partials/types/_operation.textile | 10 ---------- src/pages/docs/messages/annotations.mdx | 11 +++++----- src/pages/docs/messages/index.mdx | 11 +++++----- 7 files changed, 30 insertions(+), 40 deletions(-) create mode 100644 content/partials/types/_message_annotations.textile delete mode 100644 content/partials/types/_operation.textile diff --git a/content/api/realtime-sdk/messages.textile b/content/api/realtime-sdk/messages.textile index e9a092bc1c..c4519ed736 100644 --- a/content/api/realtime-sdk/messages.textile +++ b/content/api/realtime-sdk/messages.textile @@ -25,3 +25,8 @@ h2(#properties). python: Attributes <%= partial partial_version('types/_message') %> + +h2(#message-annotations). + default: MessageAnnotations + +<%= partial partial_version('types/_message_annotations') %> diff --git a/content/api/realtime-sdk/types.textile b/content/api/realtime-sdk/types.textile index 71e3bcde68..b3f817d4d5 100644 --- a/content/api/realtime-sdk/types.textile +++ b/content/api/realtime-sdk/types.textile @@ -87,10 +87,10 @@ blang[jsall]. <%= partial partial_version('types/_message_action') %> - h3(#message-operation). - default: Operation +h3(#message-annotations). + default: MessageAnnotations - <%= partial partial_version('types/_operation') %> +<%= partial partial_version('types/_message_annotations') %> h3(#presence-message). default: PresenceMessage diff --git a/content/partials/types/_message.textile b/content/partials/types/_message.textile index b9532413f4..1c7d67f4d8 100644 --- a/content/partials/types/_message.textile +++ b/content/partials/types/_message.textile @@ -51,7 +51,7 @@ h6(#timestamp). default: timestamp csharp: Timestamp -Timestamp when the message was received by the Ably, as milliseconds since the epocha @Time@ object
.__Type: @Integer@@Long Integer@@DateTimeOffset@@Time@@NSDate@__ +Timestamp when the message was first received by the Ably, as milliseconds since the epocha @Time@ object
.__Type: @Integer@@Long Integer@@DateTimeOffset@@Time@@NSDate@__ h6(#encoding). default: encoding @@ -69,22 +69,12 @@ blang[jsall]. h6(#serial). default: serial - This message's unique serial (an identifier that will be the same in all future updates of this message).
__Type: @String@__ + The message's serial (a server-assigned identifier that will be the same in all future updates of this message, and can be used to add annotations). Right now this will only be set if you enable annotations in "channel rules":/docs/channels#rules .
__Type: @String@__ - h6(#created-at). - default: createdAt + h6(#annotations). + default: annotations - The timestamp of the very first version of a given message (will differ from @timestamp@ only if the message has been updated or deleted).
__Type: @Integer@__ - - h6(#version). - default: version - - The version of the message, lexicographically-comparable with other versions (that share the same serial). Will differ from the serial only if the message has been updated or deleted.
__Type: @String@__ - - h6(#operation). - default: operation - - In the case of an updated or deleted message, this will contain metadata about the update or delete operation.
__Type: "@Operation@":/docs/api/realtime-sdk/types#message-operation__ + An object containing information about annotations that have been made to the object.
__Type: "@MessageAnnotations@":/docs/api/realtime-sdk/types#message-annotations__ h3(constructors). default: Message constructors diff --git a/content/partials/types/_message_annotations.textile b/content/partials/types/_message_annotations.textile new file mode 100644 index 0000000000..69fad98670 --- /dev/null +++ b/content/partials/types/_message_annotations.textile @@ -0,0 +1,7 @@ +h4. + default: Properties + java: Members + ruby: Attributes + python: Attributes + +- summary := An object whose keys are annotation types, and the values are aggregated summaries for that annotation type
__Type: @Record@__ diff --git a/content/partials/types/_operation.textile b/content/partials/types/_operation.textile deleted file mode 100644 index 02e0e9d02f..0000000000 --- a/content/partials/types/_operation.textile +++ /dev/null @@ -1,10 +0,0 @@ -An @Operation@ contains the details of an operation, such as update or deletion, supplied by the actioning client. - -h4. - default: Properties - -- clientId := The client ID of the client that initiated the operation
__Type: @String@__ - -- description := The description provided by the client that initiated the operation
__Type: @String@__ - -- metadata := A JSON object of string key-value pairs that may contain metadata associated with the operation
__Type: @Record@__ diff --git a/src/pages/docs/messages/annotations.mdx b/src/pages/docs/messages/annotations.mdx index 8184f3a42b..49af96837d 100644 --- a/src/pages/docs/messages/annotations.mdx +++ b/src/pages/docs/messages/annotations.mdx @@ -24,8 +24,7 @@ Annotations can be enabled for a channel or channel namespace with the *Message Note that when message annotations are 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. -1. On your [dashboard](https://ably.com/accounts/any), select one of your apps. -2. Go to **Settings**. +1. Go to the [**Settings**](https://ably.com/accounts/any/apps/any/edit) tab of an app in your dashboard. 3. Under [channel rules](/docs/channels#rules), click **Add new rule**. 4. Enter the channel name or channel namespace on which to enable message annotations. 5. Check **Message annotations, updates, and deletes** to enable message annotations. @@ -189,7 +188,7 @@ await channel.annotations.publish(message, { name: 'delivered' }); -// You can also use a message's serial number +// You can also use a message's `serial` await channel.annotations.publish(message.serial, { type: 'receipts:flag.v1', name: 'delivered' @@ -211,7 +210,7 @@ await channel.annotations.publish(message, { name: 'delivered' }); -// You can also use a message's serial number +// You can also use a message's `serial` await channel.annotations.publish(message.serial, { type: 'receipts:flag.v1', name: 'delivered' @@ -283,11 +282,11 @@ await channel.annotations.delete(message.serial, { ``` -## Subscribe to annotations
+## Subscribe 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 an annotation is published or deleted. -Annotation summaries are delivered to subscribers as messages with an `action` of `message.summary`. These messages have a `summary` field which provides a summary of all the annotations for the message, identified by the `serial` field on the summary message. +Annotation summaries are delivered to subscribers as messages with an `action` of `message.summary`, and a `serial` matching the `serial` of the message that they are updating. They have an `annotations` field which contains a `summary` of all the annotations for the message. The value of the `summary` field is an object where the keys are the [annotation types](#annotation-types). The structure of the value of each key depends on the summarization method used, for example `total.v1` will have a `total` field, while `flag.v1` will have `total` and `clientIds` fields. diff --git a/src/pages/docs/messages/index.mdx b/src/pages/docs/messages/index.mdx index 522169e1cf..5c41d59998 100644 --- a/src/pages/docs/messages/index.mdx +++ b/src/pages/docs/messages/index.mdx @@ -27,16 +27,15 @@ The following are the properties of a message: |----------|-------------| | **name** | The name of the message | | **data** | The contents of the message. Also known as the message payload | -| **id** | Each message sent through Ably is assigned a unique ID, unless you provide your own ID. Client specified IDs ensure [publishes are idempotent](/docs/pub-sub/advanced#idempotency) | +| **id** | Each message sent through Ably is assigned a unique ID, unless you provide your own ID, which serves as the [idempotency key](/docs/pub-sub/advanced#idempotency) | | **clientId** | The [ID of the client](/docs/auth/identified-clients) that published the message | | **connectionId** | The ID of the connection used to publish the message | -| **timestamp** | The timestamp of when the message was received by Ably, as milliseconds since the Unix epoch | +| **timestamp** | The timestamp of when the message was first received by Ably, as milliseconds since the Unix epoch | | **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 | - - +| **action** | An [enum](/docs/api/realtime-sdk/types#message-action) telling you whether this is a normal ('create') message, an update to a previous message, an annotation summary, etc. | +| **serial** | The message's serial (a server-assigned identifier that will be the same in all future updates of this message, and can be used to add [annotations](/docs/messages/annotations)). Right now this will only be set if you enable annotations in [channel rules](/docs/channels#rules) | +| **annotations** | An object containing a summary of any [annotations](/docs/messages/annotations) that have been made to the message | ## Message conflation From c7a39bb5a7c809491c38b0a0cf111947517bdcb4 Mon Sep 17 00:00:00 2001 From: Simon Woolf Date: Thu, 11 Sep 2025 19:49:52 +0100 Subject: [PATCH 4/6] Annotations: tweak annotations publish section Removed 'Publishing individual annotation events' section separate from 'annotation publish', I think that's confusing. Just mentioned that you can specify a data payload at the bottom of the publish section. --- src/pages/docs/messages/annotations.mdx | 34 +++++-------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/src/pages/docs/messages/annotations.mdx b/src/pages/docs/messages/annotations.mdx index 49af96837d..fdd1fb3401 100644 --- a/src/pages/docs/messages/annotations.mdx +++ b/src/pages/docs/messages/annotations.mdx @@ -170,7 +170,7 @@ To publish an annotation for a message, use the `annotations.publish()` method o 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). +Specify the [annotation type](#annotation-types) using the `type` field of the annotation object. ```javascript @@ -218,7 +218,9 @@ await channel.annotations.publish(message.serial, { ``` -When using `multiple.v1`, you can optionally specify a `count` by which to increment a client's contribution to the summary: +In the case of the `distinct`, `unique`, or `multiple` aggregation types, you should also specify a `name`. For these types, each different name will be aggregated separately in the [annotation summary](#annotation-summaries). + +In the case of the `multiple` aggregation type, you should specify both a `name` and a `count`, by which to increment a client's contribution to the summary. ```javascript @@ -238,6 +240,8 @@ await channel.annotations.publish(message.serial, { ``` +You can additionally specify a `data` payload when publishing an annotation. This is not included in an annotation summary, so only readably by someone [subscribing to individual annotation events](#individual-annotations). + ## Delete annotations To delete an annotation, use the `annotations.delete()` method on a channel. Pass in either a [message](/docs/messages) instance or the `serial` of the message to annotate. This method will publish an annotation message with an action of `annotation.delete`. @@ -388,36 +392,12 @@ The summary will be included in the message's `summary` field, which is an objec ``` -## Individual annotation events +## Subscribe to individual annotation events It is also possible to subscribe to individual annotation events, rather than annotation summaries. These are the emitted when [publishing](#publish) or [deleting](#delete) an annotation. Individual events can be useful for activity feeds or detailed logging, however annotation summaries are generally more reliable and efficient for maintaining UI state. -### Publish individual annotation events - -Publishing annotations is the [same as for summaries](#publish). The only difference is that you can additionally specify a `data` payload when publishing an annotation, which isn't included in an annotation summary. - - -```javascript -await channel.annotations.publish(message.serial, { - type: 'receipts:flag.v1', - name: 'delivered', - data: 'Message delivered!' -}); -``` - -```nodejs -await channel.annotations.publish(message.serial, { - type: 'receipts:flag.v1', - name: 'delivered', - data: 'Message delivered!' -}); -``` - - -### Subscribe to individual annotations - Subscribe to individual annotation events using the `annotations.subscribe()` method on a channel. To subscribe to individual annotations, you must request the `ANNOTATION_SUBSCRIBE` [mode](/docs/channels/options#modes).