@@ -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 >
285293val 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
463471import { ChatMessageEventType } from ' @ably/chat' ;
464472const {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';
493496const 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 = () => {
525523let messagesList: [Message]
526524let messagesSubscription = try await room.messages .subscribe ()
527525for 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 >
551542val 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
739725import { ChatMessageEventType } from ' @ably/chat' ;
740726const {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';
769750const 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 = () => {
801777let messagesList: [Message]
802778let messagesSubscription = try await room.messages .subscribe ()
803779for 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 >
827796val 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)
966926room .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
991944room .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