Skip to content
Open
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,8 @@ public final class io/getstream/chat/android/client/api/ChatClientConfig {
public fun <init> (ZLjava/util/Set;ZZLio/getstream/chat/android/models/TimeDuration;Lkotlin/jvm/functions/Function0;)V
public fun <init> (ZLjava/util/Set;ZZLio/getstream/chat/android/models/TimeDuration;Lkotlin/jvm/functions/Function0;Lio/getstream/chat/android/client/api/MessageLimitConfig;)V
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
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
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
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
public final fun component1 ()Z
public final fun component2 ()Ljava/util/Set;
public final fun component3 ()Z
Expand All @@ -340,9 +341,11 @@ public final class io/getstream/chat/android/client/api/ChatClientConfig {
public final fun component6 ()Lkotlin/jvm/functions/Function0;
public final fun component7 ()Lio/getstream/chat/android/client/api/MessageLimitConfig;
public final fun component8 ()Z
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;
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;
public final fun component9 ()Z
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;
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;
public fun equals (Ljava/lang/Object;)Z
public final fun getFastEventParsing ()Z
public final fun getIgnoredOfflineChannelTypes ()Ljava/util/Set;
public final fun getMessageLimitConfig ()Lio/getstream/chat/android/client/api/MessageLimitConfig;
public final fun getNow ()Lkotlin/jvm/functions/Function0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4947,8 +4947,9 @@ internal constructor(
warmUp = warmUp,
loggerConfig = ChatLoggerConfigImpl(logLevel, loggerHandler),
distinctApiCalls = distinctApiCalls,
debugRequests,
notificationConfig,
debugRequests = debugRequests,
notificationConfig = notificationConfig,
fastEventParsing = chatClientConfig.fastEventParsing,
)
setupStreamLog()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ import io.getstream.chat.android.client.notifications.handler.NotificationConfig
* @param distinctApiCalls Controls whether [DistinctChatApi] is enabled or not.
* @param debugRequests Controls whether requests can be recorded or not.
* @param notificationConfig A notification config to be used by the client.
* @param fastEventParsing Enables the fast event-parsing path for incoming WebSocket events.
* When `true`, supported event types are parsed directly into domain models, bypassing the DTO
* intermediate layer; unsupported event types fall back to the default DTO-based parser.
* Currently supported event types: `message.new`. Disabled by default.
*/
@Suppress("LongParameterList")
internal class ChatApiConfig @JvmOverloads constructor(
Expand All @@ -46,6 +50,7 @@ internal class ChatApiConfig @JvmOverloads constructor(
var distinctApiCalls: Boolean = true,
val debugRequests: Boolean,
val notificationConfig: NotificationConfig,
val fastEventParsing: Boolean = false,
) {
var isAnonymous: Boolean = false
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ import io.getstream.chat.android.models.TimeDuration
*
* @param ignoredOfflineChannelTypes Set of channel types to be ignored by offline support. Default is
* an empty set.
*
* @param fastEventParsing Enables the fast event-parsing path for incoming WebSocket events. When enabled, supported
* event types are parsed directly into domain models, bypassing the DTO intermediate layer; unsupported event types
* fall back to the default DTO-based parser, so behavior is preserved for events the fast path does not yet handle.
* Currently supported event types: `message.new`. Disabled by default. The set of supported event types may grow over
* time.
*/
public data class ChatClientConfig @JvmOverloads constructor(
public val offlineEnabled: Boolean = true,
Expand All @@ -89,6 +95,7 @@ public data class ChatClientConfig @JvmOverloads constructor(
public val now: () -> Long = { System.currentTimeMillis() },
public val messageLimitConfig: MessageLimitConfig = MessageLimitConfig(),
public val useLegacyChannelLogic: Boolean = false,
public val fastEventParsing: Boolean = false,
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import io.getstream.chat.android.client.notifications.handler.NotificationConfig
import io.getstream.chat.android.client.notifications.handler.NotificationHandler
import io.getstream.chat.android.client.notifications.handler.NotificationHandlerFactory
import io.getstream.chat.android.client.parser.ChatParser
import io.getstream.chat.android.client.parser2.DirectEventParser
import io.getstream.chat.android.client.parser2.MoshiChatParser
import io.getstream.chat.android.client.plugins.requests.ApiRequestsAnalyser
import io.getstream.chat.android.client.scope.ClientScope
Expand Down Expand Up @@ -160,10 +161,23 @@ constructor(
}
private val eventMapping by lazy { EventMapping(domainMapping) }

private val directEventParser: DirectEventParser? by lazy {
if (config.fastEventParsing) {
DirectEventParser(
currentUserIdProvider = currentUserIdProvider,
messageTransformer = apiModelTransformers.incomingMessageTransformer,
userTransformer = apiModelTransformers.incomingUserTransformer,
)
} else {
null
}
}

private val moshiParser: ChatParser by lazy {
MoshiChatParser(
eventMapping = eventMapping,
dtoMapping = dtoMapping,
directEventParser = directEventParser,
)
}
private val socketFactory: SocketFactory by lazy { SocketFactory(moshiParser, tokenManager, headersUtil) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import io.getstream.chat.android.client.events.MessageDeletedEvent
import io.getstream.chat.android.client.events.MessageUpdatedEvent
import io.getstream.chat.android.client.events.NewMessageEvent
import io.getstream.chat.android.client.events.NotificationMessageNewEvent
import io.getstream.chat.android.client.events.NotificationThreadMessageNewEvent
import io.getstream.chat.android.client.events.ReactionDeletedEvent
import io.getstream.chat.android.client.events.ReactionNewEvent
import io.getstream.chat.android.client.events.ReactionUpdateEvent
Expand All @@ -43,5 +44,6 @@ public fun ChatEvent.enrichIfNeeded(): ChatEvent = when (this) {
is ChannelTruncatedEvent -> copy(message = message?.enrichWithCid(cid))
is ChannelUpdatedByUserEvent -> copy(message = message?.enrichWithCid(cid))
is NotificationMessageNewEvent -> copy(message = message.enrichWithCid(cid))
is NotificationThreadMessageNewEvent -> copy(message = message.enrichWithCid(cid))
else -> this
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* Copyright (c) 2014-2026 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getstream.chat.android.client.parser2

import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.Moshi
import io.getstream.chat.android.client.events.ChatEvent
import io.getstream.chat.android.client.parser2.adapters.DateAdapter
import io.getstream.chat.android.client.parser2.direct.AttachmentAdapter
import io.getstream.chat.android.client.parser2.direct.ChannelInfoAdapter
import io.getstream.chat.android.client.parser2.direct.DeviceAdapter
import io.getstream.chat.android.client.parser2.direct.LocationAdapter
import io.getstream.chat.android.client.parser2.direct.MessageAdapter
import io.getstream.chat.android.client.parser2.direct.MessageModerationDetailsAdapter
import io.getstream.chat.android.client.parser2.direct.MessageReminderInfoAdapter
import io.getstream.chat.android.client.parser2.direct.ModerationAdapter
import io.getstream.chat.android.client.parser2.direct.NewMessageEventAdapter
import io.getstream.chat.android.client.parser2.direct.OptionAdapter
import io.getstream.chat.android.client.parser2.direct.PollAdapter
import io.getstream.chat.android.client.parser2.direct.PrivacySettingsAdapter
import io.getstream.chat.android.client.parser2.direct.ReactionAdapter
import io.getstream.chat.android.client.parser2.direct.ReactionGroupAdapter
import io.getstream.chat.android.client.parser2.direct.UserAdapter
import io.getstream.chat.android.models.EventType
import io.getstream.chat.android.models.MessageTransformer
import io.getstream.chat.android.models.UserId
import io.getstream.chat.android.models.UserTransformer
import io.getstream.log.taggedLogger
import okio.Buffer
import java.util.Date

/**
* Routes incoming JSON events to hand-written [JsonAdapter]s that parse directly into domain models,
* bypassing the DTO intermediate layer. Returns `null` for unsupported event types so the caller
* can fall back to the existing DTO path.
*/
internal class DirectEventParser(
private val currentUserIdProvider: () -> UserId?,
private val messageTransformer: MessageTransformer,
private val userTransformer: UserTransformer,
) {

// region Leaf adapters

private val moshi by lazy { Moshi.Builder().add(Date::class.java, DateAdapter()).build() }
private val dateAdapter by lazy { moshi.adapter(Date::class.java) }
private val deviceAdapter by lazy { DeviceAdapter() }
private val privacySettingsAdapter by lazy { PrivacySettingsAdapter() }
private val attachmentAdapter by lazy { AttachmentAdapter() }
private val channelInfoAdapter by lazy { ChannelInfoAdapter() }
private val moderationDetailsAdapter by lazy { MessageModerationDetailsAdapter() }
private val moderationAdapter by lazy { ModerationAdapter() }
private val optionAdapter by lazy { OptionAdapter() }
private val locationAdapter by lazy { LocationAdapter(dateAdapter) }
private val reactionGroupAdapter by lazy { ReactionGroupAdapter(dateAdapter) }

// endregion

// region Composed adapters

private val userAdapter by lazy {
UserAdapter(deviceAdapter, privacySettingsAdapter, dateAdapter, userTransformer)
}
private val reactionAdapter by lazy { ReactionAdapter(userAdapter, dateAdapter) }
private val pollAdapter by lazy {
PollAdapter(userAdapter, optionAdapter, dateAdapter, currentUserIdProvider)
}
private val reminderAdapter by lazy { MessageReminderInfoAdapter(dateAdapter) }
private val messageAdapter by lazy {
MessageAdapter(
attachmentAdapter, channelInfoAdapter, reactionAdapter,
reactionGroupAdapter, userAdapter, moderationDetailsAdapter, moderationAdapter,
pollAdapter, reminderAdapter, locationAdapter, dateAdapter, messageTransformer,
)
}

// endregion

// region Event adapters

private val newMessageEventAdapter by lazy {
NewMessageEventAdapter(messageAdapter, userAdapter)
}

// endregion

/** Registry mapping event type strings to their direct adapters. */
private val adapterMap: Map<String, JsonAdapter<out ChatEvent>> by lazy {
mapOf(EventType.MESSAGE_NEW to newMessageEventAdapter)
}

/**
* Attempts to parse [raw] JSON into a [ChatEvent] using a direct adapter.
* Returns `null` if the event type is not supported by any direct adapter.
*/
fun parse(raw: String): ChatEvent? {
val type = extractType(raw) ?: return null
val adapter = adapterMap[type] ?: return null
return adapter.fromJson(raw)
}
Comment on lines +111 to +115
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard direct adapter failures and fall back to DTO path.

A thrown exception from a supported direct adapter currently bypasses fallback and fails the whole event parse. For this opt-in path, parser failures should degrade to DTO parsing.

🛡️ Suggested fallback-safe implementation
     fun parse(raw: String): ChatEvent? {
         val type = extractType(raw) ?: return null
         val adapter = adapterMap[type] ?: return null
-        return adapter.fromJson(raw)
+        return runCatching { adapter.fromJson(raw) }
+            .onFailure { e ->
+                logger.v { "Direct parse failed for '$type'; falling back to DTO path: ${e.message}" }
+            }
+            .getOrNull()
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/DirectEventParser.kt`
around lines 111 - 115, In parse(raw: String) guard against adapter
deserialization throwing: after resolving type via extractType and fetching
adapter from adapterMap, call adapter.fromJson(raw) inside a try/catch; if it
throws, log/handle the error and fall back to the DTO parsing route (the
existing DTO deserializer/path used elsewhere in this class) and return its
result instead of propagating the exception; ensure this fallback only runs for
supported direct adapters and preserves returning null when both adapter and DTO
parsing fail.


companion object {

private val logger by taggedLogger("DirectEventParser")

/**
* Extracts the `"type"` field value from the top level of a JSON object
* using a streaming [JsonReader]. Stops as soon as the field is found.
*/
@Suppress("NestedBlockDepth")
internal fun extractType(raw: String): String? {
if (raw.isBlank()) return null
val reader = JsonReader.of(Buffer().writeUtf8(raw))
return try {
reader.use {
if (it.peek() != JsonReader.Token.BEGIN_OBJECT) return null
it.beginObject()
while (it.hasNext()) {
if (it.nextName() == "type") {
return if (it.peek() == JsonReader.Token.NULL) {
it.nextNull<String>()
} else {
it.nextString()
}
} else {
it.skipValue()
}
}
null
}
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
logger.v { "extractType failed; falling back to DTO path: ${e.message}" }
null
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import retrofit2.converter.moshi.MoshiConverterFactory
internal class MoshiChatParser(
private val eventMapping: EventMapping,
private val dtoMapping: DtoMapping,
private val directEventParser: DirectEventParser?,
) : ChatParser {

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

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

private fun parseAndProcessEvent(raw: String): ChatEvent = with(eventMapping) {
val event = chatEventDtoAdapter.fromJson(raw)!!.toDomain()
return event.enrichIfNeeded()
private fun parseAndProcessEvent(raw: String): ChatEvent {
val directEvent = directEventParser?.parse(raw)
if (directEvent != null) {
// Direct adapters handle enrichment inline — no enrichIfNeeded() needed.
return directEvent
}
return with(eventMapping) { chatEventDtoAdapter.fromJson(raw)!!.toDomain() }.enrichIfNeeded()
}
}
Loading
Loading