diff --git a/content/auth/capabilities.textile b/content/auth/capabilities.textile index d8e189a954..a569d460f5 100644 --- a/content/auth/capabilities.textile +++ b/content/auth/capabilities.textile @@ -44,6 +44,8 @@ The following capability operations are available for API keys and issued tokens - presence := can register presence on a channel (enter, update and leave) - object-subscribe := can subscribe to updates to objects on a channel - object-publish := can update objects on a channel +- annotation-subscribe := can subscribe to individual annotations on a channel +- annotation-publish := can publish annotations to messages on a channel - history := can retrieve message and presence state history on channels - stats := can retrieve current and historical usage statistics for an app - push-subscribe := can subscribe devices for push notifications diff --git a/content/channels/index.textile b/content/channels/index.textile index b2a6be26d6..70c7f0c662 100644 --- a/content/channels/index.textile +++ b/content/channels/index.textile @@ -190,19 +190,20 @@ Channel rules can be used to enforce settings for specific channels, or channel The channel rules related to message storage are: -- Persist last message := if enabled, the very last message published on a channel will be stored for a year. This message is retrievable using "rewind":/docs/channels/options/rewind by attaching to the channel with @rewind=1@. If you send multiple messages in a single protocol message, for example calling @publish()@ with an array of messages, you would receive all of them as one message. Be aware that presence messages are not stored and that messages stored in this manner are not accessible using "history":/docs/storage-history/history. Note that for each message stored using this rule, an additional message is deducted from your monthly allocation. -- Persist all messages := if enabled, all messages published on a channel will be stored according to the storage rules for your account. This is 24 hours for free accounts and 72 hours for paid accounts. Messages stored in this manner are accessible using "history":/docs/storage-history/history. Note that for each message stored using this rule, an additional message is deducted from your monthly allocation. +- Persist last message := If enabled, the very last message published on a channel will be stored for a year. This message is retrievable using "rewind":/docs/channels/options/rewind by attaching to the channel with @rewind=1@. If you send multiple messages in a single protocol message, for example calling @publish()@ with an array of messages, you would receive all of them as one message. Be aware that presence messages are not stored and that messages stored in this manner are not accessible using "history":/docs/storage-history/history. Note that for each message stored using this rule, an additional message is deducted from your monthly allocation. +- Persist all messages := If enabled, all messages published on a channel will be stored according to the storage rules for your account. This is 24 hours for free accounts and 72 hours for paid accounts. Messages stored in this manner are accessible using "history":/docs/storage-history/history. Note that for each message stored using this rule, an additional message is deducted from your monthly allocation. The channel rules related to security and client identity are: -- Identified := if enabled, clients will not be permitted to use (including to attach, publish, or subscribe) matching channels unless they are "identified":/docs/auth/identified-clients (they have an assigned client ID). Anonymous clients are not permitted to join these channels. Find out more about "authenticated and identified clients":/docs/auth/identified-clients. -- TLS only := if enabled, only clients who have connected to Ably over TLS will be allowed to use matching channels. By default all of Ably's client libraries use TLS when communicating with Ably over REST or when using our Realtime transports such as Websockets. +- Identified := If enabled, clients will not be permitted to use (including to attach, publish, or subscribe) matching channels unless they are "identified":/docs/auth/identified-clients (they have an assigned client ID). Anonymous clients are not permitted to join these channels. Find out more about "authenticated and identified clients":/docs/auth/identified-clients. +- TLS only := If enabled, only clients who have connected to Ably over TLS will be allowed to use matching channels. By default all of Ably's client libraries use TLS when communicating with Ably over REST or when using our Realtime transports such as Websockets. The channel rules related to enabling features are: -- Push notifications enabled := If checked, publishing messages with a push payload in the @extras@ field is permitted. This triggers the delivery of a "Push Notification":/docs/push to devices registered for push on the channel. -- Server-side batching := if enabled, messages are grouped into batches before being sent to subscribers. "Server-side batching":/docs/messages/batch#server-side reduces the overall message count, lowers costs, and mitigates the risk of hitting rate limits during high-throughput scenarios. -- Message conflation := if enabled, messages are aggregated over a set period of time and evaluated against a conflation key. All but the latest message for each conflation key value will be discarded, and the resulting message, or messages, will be delivered to subscribers as a single batch once the period of time elapses. "Message conflation":/docs/messages#conflation reduces costs in high-throughput scenarios by removing redundant and outdated messages. +- Push notifications enabled := If enabled, publishing messages with a push payload in the @extras@ field is permitted. This triggers the delivery of a "Push Notification":/docs/push to devices registered for push on the channel. +- Server-side batching := If enabled, messages are grouped into batches before being sent to subscribers. "Server-side batching":/docs/messages/batch#server-side reduces the overall message count, lowers costs, and mitigates the risk of hitting rate limits during high-throughput scenarios. +- Message conflation := If enabled, messages are aggregated over a set period of time and evaluated against a conflation key. All but the latest message for each conflation key value will be discarded, and the resulting message, or messages, will be delivered to subscribers as a single batch once the period of time elapses. "Message conflation":/docs/messages#conflation reduces costs in high-throughput scenarios by removing redundant and outdated messages. +- Advanced message features := If enabled, advanced messaging features including message "annotations":/docs/annotations are available on the channel. Note that advanced messaging is currently Experimental and its features are still in development and subject to rapid change. Currently, when advanced messaging is enabled, messages are "persisted":/docs/storage-history/storage#all-message-persistence by default, and "continuous history":/docs/storage-history/history#continuous-history features are not currently supported. To set a channel rule in the Ably dashboard: diff --git a/content/channels/options/index.textile b/content/channels/options/index.textile index 7ecb5fa953..6d871a5289 100644 --- a/content/channels/options/index.textile +++ b/content/channels/options/index.textile @@ -431,6 +431,8 @@ The available set of channel mode flags are: | @PRESENCE@ | Can register presence on the channel. | Yes | | @OBJECT_PUBLISH@ | Can update objects on the channel. | No | | @OBJECT_SUBSCRIBE@ | Can subscribe to receive updates to objects on the channel. | No | +| @ANNOTATION_PUBLISH@ | Can publish annotations to messages on the channel. | Yes | +| @ANNOTATION_SUBSCRIBE@ | Can subscribe to individual annotations on the channel. | No | The set of modes available to a client is determined by the set of "capabilities":/docs/auth/capabilities granted by their token or API key. @@ -442,6 +444,8 @@ The modes granted by each capability are: | @presence@ | @PRESENCE@ | | @object-subscribe@ | @OBJECT_SUBSCRIBE@ | | @object-publish@ | @OBJECT_PUBLISH@ | +| @annotation-publish@ | @ANNOTATION_PUBLISH@ | +| @annotation-subscribe@ | @ANNOTATION_SUBSCRIBE@ | The actual modes assigned to a client will be the **intersection** of the requested @modes@ and the modes available to the client according to its capabilities. For example, a client with the @subscribe@ capability which explicitly requests @SUBSCRIBE@ and @PUBLISH@ modes will be assigned only the @SUBSCRIBE@ mode. diff --git a/content/errors/codes.textile b/content/errors/codes.textile index 5bbf7705e2..22c00e1522 100644 --- a/content/errors/codes.textile +++ b/content/errors/codes.textile @@ -693,6 +693,20 @@ This is a client-side issue that occurs when an up-to-date "presence":/docs/pres * Ensure the client has an active connection before calling @presence.get()@. * If an outdated presence set is acceptable, set @waitForSync@ to @false@ to retrieve the presence data even when out of sync. +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. diff --git a/content/messages/index.textile b/content/messages/index.textile index 98ddfd4070..05fcf0be55 100644 --- a/content/messages/index.textile +++ b/content/messages/index.textile @@ -34,6 +34,10 @@ The following are the properties of a message: - extras := A JSON object of arbitrary key-value pairs that may contain metadata, and/or ancillary payloads. Valid payloads include those related to "Push Notifications":/docs/push, "deltas":/docs/channels/options/deltas and headers. - encoding := This is typically empty, as all messages received from Ably are automatically decoded client-side using this value. However, if the message encoding cannot be processed, this attribute contains the remaining transformations not applied to the data payload. + + h2(#conflation). Message conflation Use message conflation to ensure that clients only ever receive the most up-to-date message, by removing redundant and outdated messages. Message conflation will aggregate published messages for a set period of time and evaluate all messages against a "conflation key":#routing. All but the latest message for each conflation key value will be discarded, and the resulting message, or messages, will be delivered to subscribers as a single batch once the period of time elapses. diff --git a/content/partials/core-features/_authentication_capabilities.textile b/content/partials/core-features/_authentication_capabilities.textile index c4893023c4..de90b27bdc 100644 --- a/content/partials/core-features/_authentication_capabilities.textile +++ b/content/partials/core-features/_authentication_capabilities.textile @@ -5,6 +5,8 @@ The following capability operations are available for API keys and issued tokens - presence := can register presence on a channel (enter, update and leave) - object-subscribe := can subscribe to updates to objects on a channel - object-publish := can update objects on a channel +- annotation-subscribe := can subscribe to individual annotations on a channel +- annotation-publish := can publish annotations to messages on a channel - history := can retrieve message and presence state history on channels - stats := can retrieve current and historical usage statistics for an app - push-subscribe := can subscribe devices for push notifications diff --git a/content/pub-sub/index.textile b/content/pub-sub/index.textile index c7ad028b4d..4ebbe3a562 100644 --- a/content/pub-sub/index.textile +++ b/content/pub-sub/index.textile @@ -444,4 +444,5 @@ if err := channel.Publish(context.Background(), "example", "message data"); err diff --git a/src/data/nav/pubsub.ts b/src/data/nav/pubsub.ts index 3212f93c33..ff803d89ec 100644 --- a/src/data/nav/pubsub.ts +++ b/src/data/nav/pubsub.ts @@ -146,6 +146,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..911a078b4b --- /dev/null +++ b/src/pages/docs/messages/annotations.mdx @@ -0,0 +1,529 @@ +--- +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" + +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 + +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 **Advanced message features** 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 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. | + + +## 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. 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. + + + +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 +}); +``` + + + +## 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 +// 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' +}); +``` + + + +## 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 +// 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); + } +}); +``` + + + + +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 + +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. + +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 + } + } +} +``` + + + +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](#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 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 +{ + "metrics:total.v1": { + "total": 42 + } +} +``` + + + + +### 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"] + } +} +``` + + + + +### Distinct + +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. + +```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. + +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"] + } + } +} +``` + +[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. + +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 + } + } +} +``` + +[Deleting](#delete) an annotation removes all contributions made by that `clientId` for that `name`. + +