Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
dd9c4de
Implement new JSON parsers.
VelikovPetar Apr 7, 2026
369ce5e
Implement new JSON parsers.
VelikovPetar Apr 7, 2026
7682c1d
Implement new JSON parsers.
VelikovPetar Apr 7, 2026
6f11575
Api dump.
VelikovPetar Apr 8, 2026
836b8a2
Remove unused adapters
VelikovPetar Apr 8, 2026
a336361
Fix nullable fields parsing
VelikovPetar Apr 9, 2026
0e9714b
Chnage package
VelikovPetar Apr 9, 2026
9667294
Add tests
VelikovPetar Apr 9, 2026
a72a379
Merge branch 'v6' into feature/new_event_parsing
VelikovPetar Apr 15, 2026
4ee8f8f
Merge branch 'v6' into feature/new_event_parsing
VelikovPetar Apr 20, 2026
06ab234
Address CodeRabbit review remarks on direct JSON parsers
VelikovPetar Apr 20, 2026
6d9fd73
Merge branch 'v6' into feature/new_event_parsing
VelikovPetar Apr 27, 2026
963a212
Merge branch 'v6' into feature/new_event_parsing
VelikovPetar Apr 30, 2026
3de3c12
Improve tests.
VelikovPetar Apr 30, 2026
99a8ba0
Add MessageBufferConfig.
VelikovPetar Apr 30, 2026
a8c53db
Merge branch 'v6' into feature/new_event_parsing
VelikovPetar May 6, 2026
a21bdc3
Merge branch 'v6' into feature/new_event_parsing
VelikovPetar May 12, 2026
461870e
Merge branch 'v6' into feature/new_event_parsing
VelikovPetar May 18, 2026
29ca2ca
Merge branch 'v6' into feature/new_event_parsing
VelikovPetar May 20, 2026
a506a8b
Add `ChatClient.Build.fastEventParsing` for enabling the new event pa…
VelikovPetar May 20, 2026
c533286
Revert "Add MessageBufferConfig."
VelikovPetar May 20, 2026
af32544
Remove leftover logs
VelikovPetar May 20, 2026
6bf8637
Address PR remarks
VelikovPetar May 20, 2026
ac694de
Merge branch 'v6' into feature/new_event_parsing
VelikovPetar May 20, 2026
e89bd08
Address PR remarks
VelikovPetar May 21, 2026
329b6d4
Merge branch 'v6' into feature/new_event_parsing
VelikovPetar May 21, 2026
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 @@ -281,6 +281,7 @@ public final class io/getstream/chat/android/client/ChatClient$Builder : io/gets
public final fun debugRequests (Z)Lio/getstream/chat/android/client/ChatClient$Builder;
public final fun disableDistinctApiCalls ()Lio/getstream/chat/android/client/ChatClient$Builder;
public final fun disableWarmUp ()Lio/getstream/chat/android/client/ChatClient$Builder;
public final fun fastEventParsing (Z)Lio/getstream/chat/android/client/ChatClient$Builder;
public final fun fileTransformer (Lio/getstream/chat/android/client/uploader/FileTransformer;)Lio/getstream/chat/android/client/ChatClient$Builder;
public final fun fileUploader (Lio/getstream/chat/android/client/uploader/FileUploader;)Lio/getstream/chat/android/client/ChatClient$Builder;
public final fun forceInsecureConnection ()Lio/getstream/chat/android/client/ChatClient$Builder;
Expand Down Expand Up @@ -322,11 +323,13 @@ public final class io/getstream/chat/android/client/ClientExtensionsKt {
public final class io/getstream/chat/android/client/api/ChatClientConfig {
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLio/getstream/chat/android/client/logger/ChatLoggerConfig;ZLio/getstream/chat/android/client/notifications/handler/NotificationConfig;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLio/getstream/chat/android/client/logger/ChatLoggerConfig;ZZLio/getstream/chat/android/client/notifications/handler/NotificationConfig;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLio/getstream/chat/android/client/logger/ChatLoggerConfig;ZZLio/getstream/chat/android/client/notifications/handler/NotificationConfig;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLio/getstream/chat/android/client/logger/ChatLoggerConfig;ZZLio/getstream/chat/android/client/notifications/handler/NotificationConfig;Z)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLio/getstream/chat/android/client/logger/ChatLoggerConfig;ZZLio/getstream/chat/android/client/notifications/handler/NotificationConfig;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getApiKey ()Ljava/lang/String;
public final fun getCdnHttpUrl ()Ljava/lang/String;
public final fun getDebugRequests ()Z
public final fun getDistinctApiCalls ()Z
public final fun getFastEventParsing ()Z
public final fun getHttpUrl ()Ljava/lang/String;
public final fun getLoggerConfig ()Lio/getstream/chat/android/client/logger/ChatLoggerConfig;
public final fun getNotificationConfig ()Lio/getstream/chat/android/client/notifications/handler/NotificationConfig;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4731,6 +4731,7 @@ internal constructor(
private var retryPolicy: RetryPolicy = NoRetryPolicy()
private var distinctApiCalls: Boolean = true
private var debugRequests: Boolean = false
private var fastEventParsing: Boolean = false
private var repositoryFactoryProvider: RepositoryFactory.Provider? = null
private var uploadAttachmentsNetworkType = UploadAttachmentsNetworkType.CONNECTED
private var fileTransformer: FileTransformer = NoOpFileTransformer
Expand Down Expand Up @@ -4997,6 +4998,22 @@ internal constructor(
this.debugRequests = shouldDebug
}

/**
* 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 fun fastEventParsing(enabled: Boolean): Builder = apply {
this.fastEventParsing = enabled
}

/**
* Sets a custom [RetryPolicy] used to determine whether a particular call should be retried.
* By default, no calls are retried.
Expand Down Expand Up @@ -5070,8 +5087,9 @@ internal constructor(
warmUp = warmUp,
loggerConfig = ChatLoggerConfigImpl(logLevel, loggerHandler),
distinctApiCalls = distinctApiCalls,
debugRequests,
notificationConfig,
debugRequests = debugRequests,
notificationConfig = notificationConfig,
fastEventParsing = 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")
public class ChatClientConfig @JvmOverloads constructor(
Expand All @@ -46,6 +50,7 @@ public class ChatClientConfig @JvmOverloads constructor(
public var distinctApiCalls: Boolean = true,
public val debugRequests: Boolean,
public val notificationConfig: NotificationConfig,
public val fastEventParsing: Boolean = false,
) {
public var isAnonymous: Boolean = false
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,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 @@ -164,10 +165,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))
Comment thread
andremion marked this conversation as resolved.
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)
}

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