Skip to content

Commit 3a2bf83

Browse files
VelikovPetarclaude
andcommitted
Port V6 fix: Add opt-in fast event parsing (message.new)
Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 4f2d20b commit 3a2bf83

69 files changed

Lines changed: 9498 additions & 36 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

stream-chat-android-client/api/stream-chat-android-client.api

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,8 @@ public final class io/getstream/chat/android/client/api/ChatClientConfig {
331331
public fun <init> (ZLjava/util/Set;ZZLio/getstream/chat/android/models/TimeDuration;Lkotlin/jvm/functions/Function0;)V
332332
public fun <init> (ZLjava/util/Set;ZZLio/getstream/chat/android/models/TimeDuration;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/client/api/MessageLimitConfig;)V
333333
public fun <init> (ZLjava/util/Set;ZZLio/getstream/chat/android/models/TimeDuration;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/client/api/MessageLimitConfig;Z)V
334-
public synthetic fun <init> (ZLjava/util/Set;ZZLio/getstream/chat/android/models/TimeDuration;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/client/api/MessageLimitConfig;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
334+
public fun <init> (ZLjava/util/Set;ZZLio/getstream/chat/android/models/TimeDuration;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/client/api/MessageLimitConfig;ZZ)V
335+
public synthetic fun <init> (ZLjava/util/Set;ZZLio/getstream/chat/android/models/TimeDuration;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/client/api/MessageLimitConfig;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
335336
public final fun component1 ()Z
336337
public final fun component2 ()Ljava/util/Set;
337338
public final fun component3 ()Z
@@ -340,9 +341,11 @@ public final class io/getstream/chat/android/client/api/ChatClientConfig {
340341
public final fun component6 ()Lkotlin/jvm/functions/Function0;
341342
public final fun component7 ()Lio/getstream/chat/android/client/api/MessageLimitConfig;
342343
public final fun component8 ()Z
343-
public final fun copy (ZLjava/util/Set;ZZLio/getstream/chat/android/models/TimeDuration;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/client/api/MessageLimitConfig;Z)Lio/getstream/chat/android/client/api/ChatClientConfig;
344-
public static synthetic fun copy$default (Lio/getstream/chat/android/client/api/ChatClientConfig;ZLjava/util/Set;ZZLio/getstream/chat/android/models/TimeDuration;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/client/api/MessageLimitConfig;ZILjava/lang/Object;)Lio/getstream/chat/android/client/api/ChatClientConfig;
344+
public final fun component9 ()Z
345+
public final fun copy (ZLjava/util/Set;ZZLio/getstream/chat/android/models/TimeDuration;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/client/api/MessageLimitConfig;ZZ)Lio/getstream/chat/android/client/api/ChatClientConfig;
346+
public static synthetic fun copy$default (Lio/getstream/chat/android/client/api/ChatClientConfig;ZLjava/util/Set;ZZLio/getstream/chat/android/models/TimeDuration;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/client/api/MessageLimitConfig;ZZILjava/lang/Object;)Lio/getstream/chat/android/client/api/ChatClientConfig;
345347
public fun equals (Ljava/lang/Object;)Z
348+
public final fun getFastEventParsing ()Z
346349
public final fun getIgnoredOfflineChannelTypes ()Ljava/util/Set;
347350
public final fun getMessageLimitConfig ()Lio/getstream/chat/android/client/api/MessageLimitConfig;
348351
public final fun getNow ()Lkotlin/jvm/functions/Function0;

stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4947,8 +4947,9 @@ internal constructor(
49474947
warmUp = warmUp,
49484948
loggerConfig = ChatLoggerConfigImpl(logLevel, loggerHandler),
49494949
distinctApiCalls = distinctApiCalls,
4950-
debugRequests,
4951-
notificationConfig,
4950+
debugRequests = debugRequests,
4951+
notificationConfig = notificationConfig,
4952+
fastEventParsing = chatClientConfig.fastEventParsing,
49524953
)
49534954
setupStreamLog()
49544955

stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApiConfig.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ import io.getstream.chat.android.client.notifications.handler.NotificationConfig
3434
* @param distinctApiCalls Controls whether [DistinctChatApi] is enabled or not.
3535
* @param debugRequests Controls whether requests can be recorded or not.
3636
* @param notificationConfig A notification config to be used by the client.
37+
* @param fastEventParsing Enables the fast event-parsing path for incoming WebSocket events.
38+
* When `true`, supported event types are parsed directly into domain models, bypassing the DTO
39+
* intermediate layer; unsupported event types fall back to the default DTO-based parser.
40+
* Currently supported event types: `message.new`. Disabled by default.
3741
*/
3842
@Suppress("LongParameterList")
3943
internal class ChatApiConfig @JvmOverloads constructor(
@@ -46,6 +50,7 @@ internal class ChatApiConfig @JvmOverloads constructor(
4650
var distinctApiCalls: Boolean = true,
4751
val debugRequests: Boolean,
4852
val notificationConfig: NotificationConfig,
53+
val fastEventParsing: Boolean = false,
4954
) {
5055
var isAnonymous: Boolean = false
5156
}

stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatClientConfig.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ import io.getstream.chat.android.models.TimeDuration
7979
*
8080
* @param ignoredOfflineChannelTypes Set of channel types to be ignored by offline support. Default is
8181
* an empty set.
82+
*
83+
* @param fastEventParsing Enables the fast event-parsing path for incoming WebSocket events. When enabled, supported
84+
* event types are parsed directly into domain models, bypassing the DTO intermediate layer; unsupported event types
85+
* fall back to the default DTO-based parser, so behavior is preserved for events the fast path does not yet handle.
86+
* Currently supported event types: `message.new`. Disabled by default. The set of supported event types may grow over
87+
* time.
8288
*/
8389
public data class ChatClientConfig @JvmOverloads constructor(
8490
public val offlineEnabled: Boolean = true,
@@ -89,6 +95,7 @@ public data class ChatClientConfig @JvmOverloads constructor(
8995
public val now: () -> Long = { System.currentTimeMillis() },
9096
public val messageLimitConfig: MessageLimitConfig = MessageLimitConfig(),
9197
public val useLegacyChannelLogic: Boolean = false,
98+
public val fastEventParsing: Boolean = false,
9299
)
93100

94101
/**

stream-chat-android-client/src/main/java/io/getstream/chat/android/client/di/ChatModule.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import io.getstream.chat.android.client.notifications.handler.NotificationConfig
7070
import io.getstream.chat.android.client.notifications.handler.NotificationHandler
7171
import io.getstream.chat.android.client.notifications.handler.NotificationHandlerFactory
7272
import io.getstream.chat.android.client.parser.ChatParser
73+
import io.getstream.chat.android.client.parser2.DirectEventParser
7374
import io.getstream.chat.android.client.parser2.MoshiChatParser
7475
import io.getstream.chat.android.client.plugins.requests.ApiRequestsAnalyser
7576
import io.getstream.chat.android.client.scope.ClientScope
@@ -160,10 +161,23 @@ constructor(
160161
}
161162
private val eventMapping by lazy { EventMapping(domainMapping) }
162163

164+
private val directEventParser: DirectEventParser? by lazy {
165+
if (config.fastEventParsing) {
166+
DirectEventParser(
167+
currentUserIdProvider = currentUserIdProvider,
168+
messageTransformer = apiModelTransformers.incomingMessageTransformer,
169+
userTransformer = apiModelTransformers.incomingUserTransformer,
170+
)
171+
} else {
172+
null
173+
}
174+
}
175+
163176
private val moshiParser: ChatParser by lazy {
164177
MoshiChatParser(
165178
eventMapping = eventMapping,
166179
dtoMapping = dtoMapping,
180+
directEventParser = directEventParser,
167181
)
168182
}
169183
private val socketFactory: SocketFactory by lazy { SocketFactory(moshiParser, tokenManager, headersUtil) }

stream-chat-android-client/src/main/java/io/getstream/chat/android/client/extensions/ChatEvent.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import io.getstream.chat.android.client.events.MessageDeletedEvent
2424
import io.getstream.chat.android.client.events.MessageUpdatedEvent
2525
import io.getstream.chat.android.client.events.NewMessageEvent
2626
import io.getstream.chat.android.client.events.NotificationMessageNewEvent
27+
import io.getstream.chat.android.client.events.NotificationThreadMessageNewEvent
2728
import io.getstream.chat.android.client.events.ReactionDeletedEvent
2829
import io.getstream.chat.android.client.events.ReactionNewEvent
2930
import io.getstream.chat.android.client.events.ReactionUpdateEvent
@@ -43,5 +44,6 @@ public fun ChatEvent.enrichIfNeeded(): ChatEvent = when (this) {
4344
is ChannelTruncatedEvent -> copy(message = message?.enrichWithCid(cid))
4445
is ChannelUpdatedByUserEvent -> copy(message = message?.enrichWithCid(cid))
4546
is NotificationMessageNewEvent -> copy(message = message.enrichWithCid(cid))
47+
is NotificationThreadMessageNewEvent -> copy(message = message.enrichWithCid(cid))
4648
else -> this
4749
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Copyright (c) 2014-2026 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.getstream.chat.android.client.parser2
18+
19+
import com.squareup.moshi.JsonAdapter
20+
import com.squareup.moshi.JsonReader
21+
import com.squareup.moshi.Moshi
22+
import io.getstream.chat.android.client.events.ChatEvent
23+
import io.getstream.chat.android.client.parser2.adapters.DateAdapter
24+
import io.getstream.chat.android.client.parser2.direct.AttachmentAdapter
25+
import io.getstream.chat.android.client.parser2.direct.ChannelInfoAdapter
26+
import io.getstream.chat.android.client.parser2.direct.DeviceAdapter
27+
import io.getstream.chat.android.client.parser2.direct.LocationAdapter
28+
import io.getstream.chat.android.client.parser2.direct.MessageAdapter
29+
import io.getstream.chat.android.client.parser2.direct.MessageModerationDetailsAdapter
30+
import io.getstream.chat.android.client.parser2.direct.MessageReminderInfoAdapter
31+
import io.getstream.chat.android.client.parser2.direct.ModerationAdapter
32+
import io.getstream.chat.android.client.parser2.direct.NewMessageEventAdapter
33+
import io.getstream.chat.android.client.parser2.direct.OptionAdapter
34+
import io.getstream.chat.android.client.parser2.direct.PollAdapter
35+
import io.getstream.chat.android.client.parser2.direct.PrivacySettingsAdapter
36+
import io.getstream.chat.android.client.parser2.direct.ReactionAdapter
37+
import io.getstream.chat.android.client.parser2.direct.ReactionGroupAdapter
38+
import io.getstream.chat.android.client.parser2.direct.UserAdapter
39+
import io.getstream.chat.android.models.EventType
40+
import io.getstream.chat.android.models.MessageTransformer
41+
import io.getstream.chat.android.models.UserId
42+
import io.getstream.chat.android.models.UserTransformer
43+
import io.getstream.log.taggedLogger
44+
import okio.Buffer
45+
import java.util.Date
46+
47+
/**
48+
* Routes incoming JSON events to hand-written [JsonAdapter]s that parse directly into domain models,
49+
* bypassing the DTO intermediate layer. Returns `null` for unsupported event types so the caller
50+
* can fall back to the existing DTO path.
51+
*/
52+
internal class DirectEventParser(
53+
private val currentUserIdProvider: () -> UserId?,
54+
private val messageTransformer: MessageTransformer,
55+
private val userTransformer: UserTransformer,
56+
) {
57+
58+
// region Leaf adapters
59+
60+
private val moshi by lazy { Moshi.Builder().add(Date::class.java, DateAdapter()).build() }
61+
private val dateAdapter by lazy { moshi.adapter(Date::class.java) }
62+
private val deviceAdapter by lazy { DeviceAdapter() }
63+
private val privacySettingsAdapter by lazy { PrivacySettingsAdapter() }
64+
private val attachmentAdapter by lazy { AttachmentAdapter() }
65+
private val channelInfoAdapter by lazy { ChannelInfoAdapter() }
66+
private val moderationDetailsAdapter by lazy { MessageModerationDetailsAdapter() }
67+
private val moderationAdapter by lazy { ModerationAdapter() }
68+
private val optionAdapter by lazy { OptionAdapter() }
69+
private val locationAdapter by lazy { LocationAdapter(dateAdapter) }
70+
private val reactionGroupAdapter by lazy { ReactionGroupAdapter(dateAdapter) }
71+
72+
// endregion
73+
74+
// region Composed adapters
75+
76+
private val userAdapter by lazy {
77+
UserAdapter(deviceAdapter, privacySettingsAdapter, dateAdapter, userTransformer)
78+
}
79+
private val reactionAdapter by lazy { ReactionAdapter(userAdapter, dateAdapter) }
80+
private val pollAdapter by lazy {
81+
PollAdapter(userAdapter, optionAdapter, dateAdapter, currentUserIdProvider)
82+
}
83+
private val reminderAdapter by lazy { MessageReminderInfoAdapter(dateAdapter) }
84+
private val messageAdapter by lazy {
85+
MessageAdapter(
86+
attachmentAdapter, channelInfoAdapter, reactionAdapter,
87+
reactionGroupAdapter, userAdapter, moderationDetailsAdapter, moderationAdapter,
88+
pollAdapter, reminderAdapter, locationAdapter, dateAdapter, messageTransformer,
89+
)
90+
}
91+
92+
// endregion
93+
94+
// region Event adapters
95+
96+
private val newMessageEventAdapter by lazy {
97+
NewMessageEventAdapter(messageAdapter, userAdapter)
98+
}
99+
100+
// endregion
101+
102+
/** Registry mapping event type strings to their direct adapters. */
103+
private val adapterMap: Map<String, JsonAdapter<out ChatEvent>> by lazy {
104+
mapOf(EventType.MESSAGE_NEW to newMessageEventAdapter)
105+
}
106+
107+
/**
108+
* Attempts to parse [raw] JSON into a [ChatEvent] using a direct adapter.
109+
* Returns `null` if the event type is not supported by any direct adapter.
110+
*/
111+
fun parse(raw: String): ChatEvent? {
112+
val type = extractType(raw) ?: return null
113+
val adapter = adapterMap[type] ?: return null
114+
return adapter.fromJson(raw)
115+
}
116+
117+
companion object {
118+
119+
private val logger by taggedLogger("DirectEventParser")
120+
121+
/**
122+
* Extracts the `"type"` field value from the top level of a JSON object
123+
* using a streaming [JsonReader]. Stops as soon as the field is found.
124+
*/
125+
@Suppress("NestedBlockDepth")
126+
internal fun extractType(raw: String): String? {
127+
if (raw.isBlank()) return null
128+
val reader = JsonReader.of(Buffer().writeUtf8(raw))
129+
return try {
130+
reader.use {
131+
if (it.peek() != JsonReader.Token.BEGIN_OBJECT) return null
132+
it.beginObject()
133+
while (it.hasNext()) {
134+
if (it.nextName() == "type") {
135+
return if (it.peek() == JsonReader.Token.NULL) {
136+
it.nextNull<String>()
137+
} else {
138+
it.nextString()
139+
}
140+
} else {
141+
it.skipValue()
142+
}
143+
}
144+
null
145+
}
146+
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
147+
logger.v { "extractType failed; falling back to DTO path: ${e.message}" }
148+
null
149+
}
150+
}
151+
}
152+
}

stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/MoshiChatParser.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import retrofit2.converter.moshi.MoshiConverterFactory
5959
internal class MoshiChatParser(
6060
private val eventMapping: EventMapping,
6161
private val dtoMapping: DtoMapping,
62+
private val directEventParser: DirectEventParser?,
6263
) : ChatParser {
6364

6465
private val moshi: Moshi by lazy {
@@ -141,8 +142,12 @@ internal class MoshiChatParser(
141142

142143
private val chatEventDtoAdapter = moshi.adapter(ChatEventDto::class.java)
143144

144-
private fun parseAndProcessEvent(raw: String): ChatEvent = with(eventMapping) {
145-
val event = chatEventDtoAdapter.fromJson(raw)!!.toDomain()
146-
return event.enrichIfNeeded()
145+
private fun parseAndProcessEvent(raw: String): ChatEvent {
146+
val directEvent = directEventParser?.parse(raw)
147+
if (directEvent != null) {
148+
// Direct adapters handle enrichment inline — no enrichIfNeeded() needed.
149+
return directEvent
150+
}
151+
return with(eventMapping) { chatEventDtoAdapter.fromJson(raw)!!.toDomain() }.enrichIfNeeded()
147152
}
148153
}

0 commit comments

Comments
 (0)