Skip to content

Commit 8d81ee8

Browse files
committed
Updated Handle self-published messages section with explicit approaches with pros and cons
1 parent e1362a1 commit 8d81ee8

File tree

1 file changed

+65
-117
lines changed

1 file changed

+65
-117
lines changed

src/pages/docs/chat/rooms/messages.mdx

Lines changed: 65 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -230,11 +230,19 @@ fun MyComponent(room: Room) {
230230

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

233-
<Aside data-type='important'>
234-
When you send a message using `send()`, the server echoes it back to all subscribers in the room, including the sender.
235-
If your application adds the message to the UI immediately after calling `send()` and also appends it when received via `subscribe()`, the message will appear twice.
236-
To avoid this, we need to add safety check in the subscribe method that validates incoming message serial and version against existing messages.
237-
</Aside>
233+
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.
234+
235+
#### Wait for the subscriber <a id="wait-for-subscriber"/>
236+
237+
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.
238+
239+
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.
240+
241+
#### Deduplicate with optimistic UI <a id="deduplicate-optimistic-ui"/>
242+
243+
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.
244+
245+
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:
238246

239247
<Code>
240248
```javascript
@@ -281,7 +289,7 @@ for await message in messagesSubscription {
281289
```
282290

283291
```kotlin
284-
val myMessageList: List<Messages>
292+
val myMessageList: List<Message>
285293
val subscription = room.messages.subscribe { event: ChatMessageEvent ->
286294
// Early return if a message with the same serial and version.serial already exists
287295
val existingMessage = myMessageList.find { it.serial == event.message.serial }
@@ -462,18 +470,13 @@ Use the [`useMessages`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typed
462470
```javascript
463471
import { ChatMessageEventType } from '@ably/chat';
464472
const {unsubscribe} = room.messages.subscribe((event) => {
465-
// Early return if a message with the same serial and version.serial already exists
466-
const existingMessage = myMessageList.find(msg => msg.serial === event.message.serial);
467-
if (existingMessage && existingMessage.version.serial === event.message.version.serial) {
468-
return;
469-
}
470-
471473
switch (event.type) {
472474
case ChatMessageEventType.Created:
473475
console.log('Received message: ', event.message);
474476
break;
475477
case ChatMessageEventType.Updated:
476-
if (existingMessage && event.message.version.serial <= existingMessage.version.serial) {
478+
const existing = myMessageList.find(msg => msg.serial === event.message.serial);
479+
if (existing && event.message.version.serial <= existing.version.serial) {
477480
// We've already received a more recent update, so this one can be discarded.
478481
return;
479482
}
@@ -493,18 +496,13 @@ import { useMessages } from '@ably/chat/react';
493496
const MyComponent = () => {
494497
useMessages({
495498
listener: (event) => {
496-
// Early return if a message with the same serial and version.serial already exists
497-
const existingMessage = myMessageList.find(msg => msg.serial === event.message.serial);
498-
if (existingMessage && existingMessage.version.serial === event.message.version.serial) {
499-
return;
500-
}
501-
502499
switch (event.type) {
503500
case ChatMessageEventType.Created:
504501
console.log('Received message: ', event.message);
505502
break;
506503
case ChatMessageEventType.Updated:
507-
if (existingMessage && event.message.version.serial <= existingMessage.version.serial) {
504+
const existing = myMessageList.find(msg => msg.serial === event.message.serial);
505+
if (existing && event.message.version.serial <= existing.version.serial) {
508506
// We've already received a more recent update, so this one can be discarded.
509507
return;
510508
}
@@ -525,19 +523,12 @@ const MyComponent = () => {
525523
let messagesList: [Message]
526524
let messagesSubscription = try await room.messages.subscribe()
527525
for await message in messagesSubscription {
528-
// Early return if a message with the same serial and version already exists
529-
let existingMessage = messagesList.first(where: { $0.serial == message.serial })
530-
if existingMessage != nil && existingMessage?.version.serial == message.version.serial {
531-
continue
532-
}
533-
534526
switch message.action {
535527
case .messageCreate:
536528
messagesList.append(message)
537529
case .messageUpdate:
538530
// compare versions to ensure you are only updating with a newer message
539-
if let existingMessage = existingMessage, message.version > existingMessage.version,
540-
let index = messagesList.firstIndex(of: existingMessage) {
531+
if let index = messagesList.firstIndex(where: { $0.serial == message.serial && message.version > $0.version }) {
541532
messagesList[index] = message
542533
}
543534
default:
@@ -547,19 +538,13 @@ for await message in messagesSubscription {
547538
```
548539

549540
```kotlin
550-
val myMessageList: List<Messages>
541+
val myMessageList: List<Message>
551542
val messagesSubscription = room.messages.subscribe { event ->
552-
// Early return if a message with the same serial and version.serial already exists
553-
val existingMessage = myMessageList.find { it.serial == event.message.serial }
554-
if (existingMessage != null && existingMessage.version.serial == event.message.version.serial) return@subscribe
555-
556543
when (event.type) {
557544
ChatMessageEventType.Created -> println("Received message: ${event.message}")
558-
ChatMessageEventType.Updated -> {
559-
if (existingMessage != null && event.message.version.serial > existingMessage.version.serial) {
560-
println("Message updated: ${event.message}")
561-
}
562-
}
545+
ChatMessageEventType.Updated -> myMessageList.find {
546+
event.message.serial == it.serial && event.message.version.serial > it.version.serial
547+
}?.let { println("Message updated: ${event.message}") }
563548
else -> {}
564549
}
565550
}
@@ -578,17 +563,18 @@ fun MyComponent(room: Room) {
578563
579564
LaunchedEffect(room) {
580565
room.messages.asFlow().collect { event ->
581-
// Early return if a message with the same serial and version.serial already exists
582-
val existingMessage = myMessageList.find { it.serial == event.message.serial }
583-
if (existingMessage != null && existingMessage.version.serial == event.message.version.serial) return@collect
584-
585566
when (event.type) {
586567
ChatMessageEventType.Created -> {
587568
myMessageList = myMessageList + event.message
588569
}
589570
ChatMessageEventType.Updated -> {
590-
if (existingMessage != null && event.message.version.serial > existingMessage.version.serial) {
591-
myMessageList = myMessageList.map { if (it.serial == existingMessage.serial) event.message else it }
571+
myMessageList = myMessageList.map { message ->
572+
if (message.serial == event.message.serial &&
573+
event.message.version.serial > message.version.serial) {
574+
event.message
575+
} else {
576+
message
577+
}
592578
}
593579
}
594580
else -> {}
@@ -738,18 +724,13 @@ Use the [`useMessages`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typed
738724
```javascript
739725
import { ChatMessageEventType } from '@ably/chat';
740726
const {unsubscribe} = room.messages.subscribe((event) => {
741-
// Early return if a message with the same serial and version.serial already exists
742-
const existingMessage = myMessageList.find(msg => msg.serial === event.message.serial);
743-
if (existingMessage && existingMessage.version.serial === event.message.version.serial) {
744-
return;
745-
}
746-
747727
switch (event.type) {
748728
case ChatMessageEventType.Created:
749729
console.log('Received message: ', event.message);
750730
break;
751731
case ChatMessageEventType.Deleted:
752-
if (existingMessage && event.message.version.serial <= existingMessage.version.serial) {
732+
const existing = myMessageList.find(msg => msg.serial === event.message.serial);
733+
if (existing && event.message.version.serial <= existing.version.serial) {
753734
// We've already received a more recent update, so this one can be discarded.
754735
return;
755736
}
@@ -769,18 +750,13 @@ import { useMessages } from '@ably/chat/react';
769750
const MyComponent = () => {
770751
useMessages({
771752
listener: (event) => {
772-
// Early return if a message with the same serial and version.serial already exists
773-
const existingMessage = myMessageList.find(msg => msg.serial === event.message.serial);
774-
if (existingMessage && existingMessage.version.serial === event.message.version.serial) {
775-
return;
776-
}
777-
778753
switch (event.type) {
779754
case ChatMessageEventType.Created:
780755
console.log('Received message: ', event.message);
781756
break;
782757
case ChatMessageEventType.Deleted:
783-
if (existingMessage && event.message.version.serial <= existingMessage.version.serial) {
758+
const existing = myMessageList.find(msg => msg.serial === event.message.serial);
759+
if (existing && event.message.version.serial <= existing.version.serial) {
784760
// We've already received a more recent update, so this one can be discarded.
785761
return;
786762
}
@@ -801,19 +777,12 @@ const MyComponent = () => {
801777
let messagesList: [Message]
802778
let messagesSubscription = try await room.messages.subscribe()
803779
for await message in messagesSubscription {
804-
// Early return if a message with the same serial and version already exists
805-
let existingMessage = messagesList.first(where: { $0.serial == message.serial })
806-
if existingMessage != nil && existingMessage?.version.serial == message.version.serial {
807-
continue
808-
}
809-
810780
switch message.action {
811781
case .messageCreate:
812782
messagesList.append(message)
813783
case .messageDelete:
814784
// version check ensures the message you are deleting is older
815-
if let existingMessage = existingMessage, message.version > existingMessage.version,
816-
let index = messagesList.firstIndex(of: existingMessage) {
785+
if let index = messagesList.firstIndex(where: { $0.serial == message.serial && message.version > $0.version }) {
817786
messagesList.remove(at: index)
818787
}
819788
default:
@@ -823,19 +792,13 @@ for await message in messagesSubscription {
823792
```
824793

825794
```kotlin
826-
val myMessageList: List<Messages>
795+
val myMessageList: List<Message>
827796
val messagesSubscription = room.messages.subscribe { event ->
828-
// Early return if a message with the same serial and version.serial already exists
829-
val existingMessage = myMessageList.find { it.serial == event.message.serial }
830-
if (existingMessage != null && existingMessage.version.serial == event.message.version.serial) return@subscribe
831-
832797
when (event.type) {
833798
ChatMessageEventType.Created -> println("Received message: ${event.message}")
834-
ChatMessageEventType.Deleted -> {
835-
if (existingMessage != null && event.message.version.serial > existingMessage.version.serial) {
836-
println("Message deleted: ${event.message}")
837-
}
838-
}
799+
ChatMessageEventType.Deleted -> myMessageList.find {
800+
event.message.serial == it.serial && event.message.version.serial > it.version.serial
801+
}?.let { println("Message deleted: ${event.message}") }
839802
else -> {}
840803
}
841804
}
@@ -854,17 +817,14 @@ fun MyComponent(room: Room) {
854817
855818
LaunchedEffect(room) {
856819
room.messages.asFlow().collect { event ->
857-
// Early return if a message with the same serial and version.serial already exists
858-
val existingMessage = myMessageList.find { it.serial == event.message.serial }
859-
if (existingMessage != null && existingMessage.version.serial == event.message.version.serial) return@collect
860-
861820
when (event.type) {
862821
ChatMessageEventType.Created -> {
863822
myMessageList = myMessageList + event.message
864823
}
865824
ChatMessageEventType.Deleted -> {
866-
if (existingMessage != null && event.message.version.serial > existingMessage.version.serial) {
867-
myMessageList = myMessageList.filterNot { it.serial == existingMessage.serial }
825+
myMessageList = myMessageList.filterNot { message ->
826+
message.serial == event.message.serial &&
827+
event.message.version.serial > message.version.serial
868828
}
869829
}
870830
else -> {}
@@ -964,22 +924,15 @@ let myMessageList: Message[];
964924

965925
// For messages (create, update, delete)
966926
room.messages.subscribe((event) => {
967-
const existingMessage = myMessageList.find((msg) => msg.serial === event.message.serial);
968-
969-
// Early return if a message with the same serial and version.serial already exists
970-
if (existingMessage && existingMessage.version.serial === event.message.version.serial) {
971-
return;
972-
}
973-
974927
switch (event.type) {
975928
case ChatMessageEventType.Created:
976929
myMessageList.push(event.message);
977930
break;
978931
case ChatMessageEventType.Updated:
979932
case ChatMessageEventType.Deleted:
980-
if (existingMessage) {
981-
const idx = myMessageList.indexOf(existingMessage);
982-
myMessageList[idx] = existingMessage.with(event);
933+
const idx = myMessageList.findIndex((msg) => msg.serial === event.message.serial);
934+
if (idx !== -1) {
935+
myMessageList[idx] = myMessageList[idx].with(event);
983936
}
984937
break;
985938
default:
@@ -989,10 +942,9 @@ room.messages.subscribe((event) => {
989942

990943
// And for message reactions
991944
room.messages.reactions.subscribe((event) => {
992-
const existingMessage = myMessageList.find((msg) => msg.serial === event.messageSerial);
993-
if (existingMessage) {
994-
const idx = myMessageList.indexOf(existingMessage);
995-
myMessageList[idx] = existingMessage.with(event);
945+
const idx = myMessageList.findIndex((msg) => msg.serial === event.messageSerial);
946+
if (idx !== -1) {
947+
myMessageList[idx] = myMessageList[idx].with(event);
996948
}
997949
});
998950
```
@@ -1007,39 +959,35 @@ const MyComponent = () => {
1007959
const [ messages, setMessages ] = useState<{list: Message[]}>({list: []});
1008960
useMessages({
1009961
listener: (event) => {
1010-
setMessages((prev) => {
1011-
const existingMessage = prev.list.find((msg) => msg.serial === event.message.serial);
1012-
1013-
// Early return if a message with the same serial and version.serial already exists
1014-
if (existingMessage && existingMessage.version.serial === event.message.version.serial) {
1015-
return prev;
1016-
}
1017-
1018-
switch (event.type) {
1019-
case ChatMessageEventType.Created:
962+
switch (event.type) {
963+
case ChatMessageEventType.Created:
964+
setMessages((prev) => {
1020965
// append new message
1021966
prev.list.push(event.message);
1022967
// update reference without copying whole array
1023968
return { list: prev.list };
1024-
case ChatMessageEventType.Updated:
1025-
case ChatMessageEventType.Deleted:
1026-
if (!existingMessage) {
969+
});
970+
break;
971+
case ChatMessageEventType.Updated:
972+
case ChatMessageEventType.Deleted:
973+
setMyMessageList((prev) => {
974+
// find existing message to apply update or delete to
975+
const existing = prev.list.findIndex((msg) => msg.serial === event.message.serial);
976+
if (existing === -1) {
1027977
return prev; // no change if not found
1028978
}
1029-
const newMsg = existingMessage.with(event);
1030-
if (newMsg === existingMessage) {
979+
const newMsg = existing.with(event);
980+
if (newMsg === existing) {
1031981
// with() returns the same object if the event is older,
1032982
// so in this case no change is needed
1033983
return prev;
1034984
}
1035985
// set new message and update reference without copying whole array
1036-
const idx = prev.list.indexOf(existingMessage);
1037-
prev.list[idx] = newMsg;
986+
prev.list[existing] = newMsg;
1038987
return { list: prev.list };
1039-
default:
1040-
return prev;
1041-
}
1042-
});
988+
});
989+
break;
990+
}
1043991
},
1044992
});
1045993

0 commit comments

Comments
 (0)