Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CHA-RC1][ECO-5101] Implement Async Room Get #56

Draft
wants to merge 11 commits into
base: tests/room-lifecycle-manager-retry
Choose a base branch
from
1 change: 1 addition & 0 deletions chat-android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ dependencies {
testImplementation(libs.junit)
testImplementation(libs.mockk)
testImplementation(libs.coroutine.test)
testImplementation(libs.bundles.ktor.client)
androidTestImplementation(libs.androidx.test.core)
androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.androidx.junit)
Expand Down
5 changes: 5 additions & 0 deletions chat-android/src/main/java/com/ably/chat/ErrorCodes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ enum class ErrorCodes(val errorCode: Int) {
*/
RoomIsReleased(102_103),

/**
* Room was released before the operation could complete.
*/
RoomReleasedBeforeOperationCompleted(102_106),

/**
* Cannot perform operation because the previous operation failed.
*/
Expand Down
4 changes: 1 addition & 3 deletions chat-android/src/main/java/com/ably/chat/Messages.kt
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ internal class DefaultMessages(
private val roomId: String,
private val realtimeChannels: AblyRealtime.Channels,
private val chatApi: ChatApi,
) : Messages, ContributesToRoomLifecycleImpl(), ResolvedContributor {
) : Messages, ContributesToRoomLifecycleImpl() {

override val featureName: String = "messages"

Expand All @@ -243,8 +243,6 @@ internal class DefaultMessages(

override val channel = realtimeChannels.get(messagesChannelName, ChatChannelOptions())

override val contributor: ContributesToRoomLifecycle = this

override val attachmentErrorCode: ErrorCodes = ErrorCodes.MessagesAttachmentFailed

override val detachmentErrorCode: ErrorCodes = ErrorCodes.MessagesDetachmentFailed
Expand Down
4 changes: 1 addition & 3 deletions chat-android/src/main/java/com/ably/chat/Occupancy.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,12 @@ data class OccupancyEvent(

internal class DefaultOccupancy(
private val messages: Messages,
) : Occupancy, ContributesToRoomLifecycleImpl(), ResolvedContributor {
) : Occupancy, ContributesToRoomLifecycleImpl() {

override val featureName: String = "occupancy"

override val channel = messages.channel

override val contributor: ContributesToRoomLifecycle = this

override val attachmentErrorCode: ErrorCodes = ErrorCodes.OccupancyAttachmentFailed

override val detachmentErrorCode: ErrorCodes = ErrorCodes.OccupancyDetachmentFailed
Expand Down
104 changes: 72 additions & 32 deletions chat-android/src/main/java/com/ably/chat/Presence.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@

package com.ably.chat

import android.text.PrecomputedText.Params
import io.ably.lib.types.ErrorInfo
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import io.ably.lib.realtime.Channel
import io.ably.lib.realtime.Presence.GET_CLIENTID
import io.ably.lib.realtime.Presence.GET_CONNECTIONID
import io.ably.lib.realtime.Presence.GET_WAITFORSYNC
import io.ably.lib.types.Param
import io.ably.lib.types.PresenceMessage
import io.ably.lib.realtime.Channel as AblyRealtimeChannel
import io.ably.lib.realtime.Presence as PubSubPresence
import io.ably.lib.realtime.Presence.PresenceListener as PubSubPresenceListener

typealias PresenceData = Any
typealias PresenceData = JsonElement

/**
* This interface is used to interact with presence in a chat room: subscribing to presence events,
Expand All @@ -20,14 +26,15 @@ interface Presence : EmitsDiscontinuities {
* Get the underlying Ably realtime channel used for presence in this chat room.
* @returns The realtime channel.
*/
val channel: AblyRealtimeChannel
val channel: Channel

/**
* Method to get list of the current online users and returns the latest presence messages associated to it.
* @param {Ably.RealtimePresenceParams} params - Parameters that control how the presence set is retrieved.
* @returns {Promise<PresenceMessage[]>} or upon failure, the promise will be rejected with an [[Ably.ErrorInfo]] object which explains the error.
* Method to get list of the current online users and returns the latest presence messages associated to it.
* @param {Ably.RealtimePresenceParams} params - Parameters that control how the presence set is retrieved.
* @throws {@link io.ably.lib.types.AblyException} object which explains the error.
* @returns {List<PresenceMessage>}
*/
suspend fun get(params: List<Params>): List<PresenceMember>
suspend fun get(waitForSync: Boolean = true, clientId: String? = null, connectionId: String? = null): List<PresenceMember>

/**
* Method to check if user with supplied clientId is online
Expand All @@ -39,23 +46,23 @@ interface Presence : EmitsDiscontinuities {
/**
* Method to join room presence, will emit an enter event to all subscribers. Repeat calls will trigger more enter events.
* @param {PresenceData} data - The users data, a JSON serializable object that will be sent to all subscribers.
* @returns {Promise<void>} or upon failure, the promise will be rejected with an {@link ErrorInfo} object which explains the error.
* @throws {@link io.ably.lib.types.AblyException} object which explains the error.
*/
suspend fun enter(data: PresenceData?)
suspend fun enter(data: PresenceData? = null)

/**
* Method to update room presence, will emit an update event to all subscribers. If the user is not present, it will be treated as a join event.
* @param {PresenceData} data - The users data, a JSON serializable object that will be sent to all subscribers.
* @returns {Promise<void>} or upon failure, the promise will be rejected with an {@link ErrorInfo} object which explains the error.
* @throws {@link io.ably.lib.types.AblyException} object which explains the error.
*/
suspend fun update(data: PresenceData?)
suspend fun update(data: PresenceData? = null)

/**
* Method to leave room presence, will emit a leave event to all subscribers. If the user is not present, it will be treated as a no-op.
* @param {PresenceData} data - The users data, a JSON serializable object that will be sent to all subscribers.
* @returns {Promise<void>} or upon failure, the promise will be rejected with an {@link ErrorInfo} object which explains the error.
* @throws {@link io.ably.lib.types.AblyException} object which explains the error.
*/
suspend fun leave(data: PresenceData?)
suspend fun leave(data: PresenceData? = null)

/**
* Subscribe the given listener to all presence events.
Expand Down Expand Up @@ -87,7 +94,7 @@ data class PresenceMember(
/**
* The data associated with the presence member.
*/
val data: PresenceData,
val data: PresenceData?,

/**
* The current state of the presence member.
Expand Down Expand Up @@ -122,50 +129,83 @@ data class PresenceEvent(
/**
* The timestamp of the presence event.
*/
val timestamp: Int,
val timestamp: Long,

/**
* The data associated with the presence event.
*/
val data: PresenceData,
val data: PresenceData?,
)

internal class DefaultPresence(
private val messages: Messages,
) : Presence, ContributesToRoomLifecycleImpl(), ResolvedContributor {
private val clientId: String,
override val channel: Channel,
private val presence: PubSubPresence,
) : Presence, ContributesToRoomLifecycleImpl() {

override val featureName = "presence"

override val channel = messages.channel

override val contributor: ContributesToRoomLifecycle = this

override val attachmentErrorCode: ErrorCodes = ErrorCodes.PresenceAttachmentFailed

override val detachmentErrorCode: ErrorCodes = ErrorCodes.PresenceDetachmentFailed

override suspend fun get(params: List<Params>): List<PresenceMember> {
TODO("Not yet implemented")
suspend fun get(params: List<Param>): List<PresenceMember> {
val usersOnPresence = presence.getCoroutine(params)
return usersOnPresence.map { user ->
PresenceMember(
clientId = user.clientId,
action = user.action,
data = (user.data as? JsonObject)?.get("userCustomData"),
updatedAt = user.timestamp,
)
}
}

override suspend fun isUserPresent(clientId: String): Boolean {
TODO("Not yet implemented")
override suspend fun get(waitForSync: Boolean, clientId: String?, connectionId: String?): List<PresenceMember> {
val params = buildList {
if (waitForSync) add(Param(GET_WAITFORSYNC, true))
clientId?.let { add(Param(GET_CLIENTID, it)) }
connectionId?.let { add(Param(GET_CONNECTIONID, it)) }
}
return get(params)
}

override suspend fun isUserPresent(clientId: String): Boolean = presence.getCoroutine(Param(GET_CLIENTID, clientId)).isNotEmpty()

override suspend fun enter(data: PresenceData?) {
TODO("Not yet implemented")
presence.enterClientCoroutine(clientId, wrapInUserCustomData(data))
}

override suspend fun update(data: PresenceData?) {
TODO("Not yet implemented")
presence.updateClientCoroutine(clientId, wrapInUserCustomData(data))
}

override suspend fun leave(data: PresenceData?) {
TODO("Not yet implemented")
presence.leaveClientCoroutine(clientId, wrapInUserCustomData(data))
}

override fun subscribe(listener: Presence.Listener): Subscription {
TODO("Not yet implemented")
val presenceListener = PubSubPresenceListener {
val presenceEvent = PresenceEvent(
action = it.action,
clientId = it.clientId,
timestamp = it.timestamp,
data = (it.data as? JsonObject)?.get("userCustomData"),
)
listener.onEvent(presenceEvent)
}

presence.subscribe(presenceListener)

return Subscription {
presence.unsubscribe(presenceListener)
}
}

private fun wrapInUserCustomData(data: PresenceData?) = data?.let {
JsonObject().apply {
add("userCustomData", data)
}
}

override fun release() {
Expand Down
Loading