Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 94 additions & 2 deletions src/pages/docs/chat/rooms/messages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,98 @@ fun MyComponent(room: Room) {
```
</Code>

### Handle self-published messages <a id="self-published-messages"/>

When you send a message using `send()`, the server echoes it back to all subscribers in the room, including the sender. If your application adds the message to the UI immediately before/after calling `send()` and also appends it when received via `subscribe()`, the message will appear twice. There are two approaches to handle this.

#### Wait for the subscriber <a id="wait-for-subscriber"/>

The recommended approach is to not add the message to the UI immediately before/after calling `send()`. Instead, only append messages to the UI inside the `subscribe()` listener. Since the server echoes every sent message back to the sender as a subscriber event, the message will still appear in the UI when it arrives through the subscription. This eliminates the duplication problem entirely and requires no deduplication logic in the subscriber.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users still have to contend with cross-region publishes, so they still need to handle ordering. They also need to apply updates/deletes, which means existence checks. At this point, the deduplication logic is essentially mostly implemented (and we recommend to use the .with() function to handle this in JS land).

I agree with handling concurrent writes to a data structure being a concern, but I don't think this means we should recommend in all cases for non-optimistic updates.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cases where we recommend non-optimistic are updates/deletes and annotations.


This approach has the advantage that the message list is only written to from a single place — the subscriber. This means you don't need a concurrent data structure or additional synchronization to protect the list from simultaneous writes. The tradeoff is that the sent message must complete a round trip to the server before appearing in the UI. While Ably's realtime delivery is always near-instantaneous, this may introduce a slight delay rarely in poor network conditions.

#### Deduplicate with optimistic UI <a id="deduplicate-optimistic-ui"/>
Copy link
Contributor Author

@sacOO7 sacOO7 Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If chat REST API starts respecting echoMessages=false as per https://ably.atlassian.net/browse/DX-456, section Deduplicate with optimistic UI will only have a config. echoMessages=false and a note to use concurrent or thread-safe data structure to handle simultaneous writes safely.


If your application adds the message to the UI immediately before/after calling `send()` for a more responsive experience, you need to add a safety check in the subscriber to avoid duplicates. Validate the incoming message `serial` and `version` against existing messages.

Because the message list is written to from two places — once before/after `send()` and again inside the subscriber — you must use a concurrent or thread-safe data structure to handle simultaneous writes safely. Additionally, each incoming message requires a lookup through the existing message list to check for duplicates, which adds CPU overhead that grows with the size of the list:

<Code>
```javascript
const {unsubscribe} = room.messages.subscribe((event) => {
// Early return if a message with the same serial and version.serial already exists
const existingMessage = myMessageList.find(msg => msg.serial === event.message.serial);
if (existingMessage && existingMessage.version.serial === event.message.version.serial) {
return;
}
// Process the message
});
```

```react
import { useMessages } from '@ably/chat/react';

const MyComponent = () => {
useMessages({
listener: (event) => {
// Early return if a message with the same serial and version.serial already exists
const existingMessage = myMessageList.find(msg => msg.serial === event.message.serial);
if (existingMessage && existingMessage.version.serial === event.message.version.serial) {
return;
}
// Process the message
},
});

return <div>...</div>;
};
```

```swift
let messagesList: [Message]
let messagesSubscription = try await room.messages.subscribe()
for await message in messagesSubscription {
// Early return if a message with the same serial and version already exists
let existingMessage = messagesList.first(where: { $0.serial == message.serial })
if existingMessage != nil && existingMessage?.version.serial == message.version.serial {
continue
}
// Process the message
}
```

```kotlin
val myMessageList: List<Message>
val subscription = room.messages.subscribe { event: ChatMessageEvent ->
// Early return if a message with the same serial and version.serial already exists
val existingMessage = myMessageList.find { it.serial == event.message.serial }
if (existingMessage != null && existingMessage.version.serial == event.message.version.serial) return@subscribe
// Process the message
}
```

```android
import androidx.compose.runtime.*
import com.ably.chat.Message
import com.ably.chat.Room
import com.ably.chat.asFlow

@Composable
fun MyComponent(room: Room) {
var myMessageList by remember { mutableStateOf<List<Message>>(emptyList()) }

LaunchedEffect(room) {
room.messages.asFlow().collect { event ->
// Early return if a message with the same serial and version.serial already exists
val existingMessage = myMessageList.find { it.serial == event.message.serial }
if (existingMessage != null && existingMessage.version.serial == event.message.version.serial) return@collect
// Process the message
}
}
}
```
</Code>

## Get a single message <a id="get-by-serial"/>

<If lang="javascript,swift,kotlin,android">
Expand Down Expand Up @@ -446,7 +538,7 @@ for await message in messagesSubscription {
```

```kotlin
val myMessageList: List<Messages>
val myMessageList: List<Message>
val messagesSubscription = room.messages.subscribe { event ->
when (event.type) {
ChatMessageEventType.Created -> println("Received message: ${event.message}")
Expand Down Expand Up @@ -700,7 +792,7 @@ for await message in messagesSubscription {
```

```kotlin
val myMessageList: List<Messages>
val myMessageList: List<Message>
val messagesSubscription = room.messages.subscribe { event ->
when (event.type) {
ChatMessageEventType.Created -> println("Received message: ${event.message}")
Expand Down