Skip to content

Commit

Permalink
Add Chat.destroy() method, TimerManager class (#66)
Browse files Browse the repository at this point in the history
* Minor code cleanup

* Chat.destroy() and PlatformTimer refactor

* Add test for TimerManager

* Formatting

* Add tests

* Bump pubnub-kotlin version

* Update pubnub-chat-impl/src/iosMain/kotlin/com/pubnub/chat/internal/timer/PlatformTimer.ios.kt

Co-authored-by: jguz-pubnub <[email protected]>

* Update pubnub-chat-impl/src/iosMain/kotlin/com/pubnub/chat/internal/timer/PlatformTimer.ios.kt

Co-authored-by: jguz-pubnub <[email protected]>

* Fix accepted suggestions

---------

Co-authored-by: jguz-pubnub <[email protected]>
  • Loading branch information
wkal-pubnub and jguz-pubnub authored Sep 6, 2024
1 parent fbd4b7b commit 82462a3
Show file tree
Hide file tree
Showing 18 changed files with 234 additions and 107 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ SWIFT_PATH=pubnub-kotlin/swift

ENABLE_TARGET_JS=false
ENABLE_TARGET_IOS=false
ENABLE_TARGET_IOS_SIMULATOR=false
ENABLE_TARGET_IOS_SIMULATOR=true
1 change: 1 addition & 0 deletions pubnub-chat-api/api/pubnub-chat-api.api
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public abstract interface class com/pubnub/chat/Chat {
public abstract fun createUser (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)Lcom/pubnub/kmp/PNFuture;
public abstract fun deleteChannel (Ljava/lang/String;Z)Lcom/pubnub/kmp/PNFuture;
public abstract fun deleteUser (Ljava/lang/String;Z)Lcom/pubnub/kmp/PNFuture;
public abstract fun destroy ()V
public abstract fun emitEvent (Ljava/lang/String;Lcom/pubnub/chat/types/EventContent;Ljava/util/Map;)Lcom/pubnub/kmp/PNFuture;
public abstract fun forwardMessage (Lcom/pubnub/chat/Message;Ljava/lang/String;)Lcom/pubnub/kmp/PNFuture;
public abstract fun getChannel (Ljava/lang/String;)Lcom/pubnub/kmp/PNFuture;
Expand Down
2 changes: 2 additions & 0 deletions pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/Chat.kt
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ interface Chat {
count: Int = 100
): PNFuture<GetCurrentUserMentionsResult>

fun destroy()

// Companion object required for extending this class elsewhere
companion object
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,12 @@ import com.pubnub.api.models.consumer.objects.PNKey
import com.pubnub.api.models.consumer.objects.PNMembershipKey
import com.pubnub.api.models.consumer.objects.PNPage
import com.pubnub.api.models.consumer.objects.PNSortKey
import com.pubnub.api.models.consumer.objects.channel.PNChannelMetadata
import com.pubnub.api.models.consumer.objects.channel.PNChannelMetadataResult
import com.pubnub.api.models.consumer.objects.member.PNMember
import com.pubnub.api.models.consumer.objects.member.PNMemberArrayResult
import com.pubnub.api.models.consumer.objects.membership.PNChannelDetailsLevel
import com.pubnub.api.models.consumer.objects.membership.PNChannelMembership
import com.pubnub.api.models.consumer.objects.membership.PNChannelMembershipArrayResult
import com.pubnub.api.models.consumer.objects.uuid.PNUUIDMetadata
import com.pubnub.api.models.consumer.objects.uuid.PNUUIDMetadataArrayResult
import com.pubnub.api.models.consumer.objects.uuid.PNUUIDMetadataResult
import com.pubnub.api.models.consumer.presence.PNHereNowOccupantData
Expand Down Expand Up @@ -52,7 +50,6 @@ import com.pubnub.chat.internal.error.PubNubErrorMessage.CANNOT_FORWARD_MESSAGE_
import com.pubnub.chat.internal.error.PubNubErrorMessage.CAN_NOT_FIND_CHANNEL_WITH_ID
import com.pubnub.chat.internal.error.PubNubErrorMessage.CHANNEL_ID_ALREADY_EXIST
import com.pubnub.chat.internal.error.PubNubErrorMessage.CHANNEL_ID_IS_REQUIRED
import com.pubnub.chat.internal.error.PubNubErrorMessage.CHANNEL_META_DATA_IS_EMPTY
import com.pubnub.chat.internal.error.PubNubErrorMessage.CHANNEL_NOT_FOUND
import com.pubnub.chat.internal.error.PubNubErrorMessage.COUNT_SHOULD_NOT_EXCEED_100
import com.pubnub.chat.internal.error.PubNubErrorMessage.DEVICE_TOKEN_HAS_TO_BE_DEFINED_IN_CHAT_PUSHNOTIFICATIONS_CONFIG
Expand All @@ -63,11 +60,7 @@ import com.pubnub.chat.internal.error.PubNubErrorMessage.FAILED_TO_RETRIEVE_CHAN
import com.pubnub.chat.internal.error.PubNubErrorMessage.FAILED_TO_RETRIEVE_WHO_IS_PRESENT_DATA
import com.pubnub.chat.internal.error.PubNubErrorMessage.FAILED_TO_SOFT_DELETE_CHANNEL
import com.pubnub.chat.internal.error.PubNubErrorMessage.ID_IS_REQUIRED
import com.pubnub.chat.internal.error.PubNubErrorMessage.NO_DATA_AVAILABLE_TO_CREATE_OR_UPDATE_CHANNEL
import com.pubnub.chat.internal.error.PubNubErrorMessage.ONLY_ONE_LEVEL_OF_THREAD_NESTING_IS_ALLOWED
import com.pubnub.chat.internal.error.PubNubErrorMessage.PNCHANNEL_METADATA_IS_NULL
import com.pubnub.chat.internal.error.PubNubErrorMessage.PNUUID_METADATA_IS_NULL
import com.pubnub.chat.internal.error.PubNubErrorMessage.PNUUID_METADATA_RESULT_IS_NULL
import com.pubnub.chat.internal.error.PubNubErrorMessage.STORE_USER_ACTIVITY_INTERVAL_SHOULD_BE_AT_LEAST_1_MIN
import com.pubnub.chat.internal.error.PubNubErrorMessage.THERE_IS_NO_ACTION_TIMETOKEN_CORRESPONDING_TO_THE_THREAD
import com.pubnub.chat.internal.error.PubNubErrorMessage.THERE_IS_NO_THREAD_TO_BE_DELETED
Expand All @@ -80,8 +73,8 @@ import com.pubnub.chat.internal.error.PubNubErrorMessage.USER_NOT_EXIST
import com.pubnub.chat.internal.error.PubNubErrorMessage.YOU_CAN_NOT_CREATE_THREAD_ON_DELETED_MESSAGES
import com.pubnub.chat.internal.serialization.PNDataEncoder
import com.pubnub.chat.internal.timer.PlatformTimer
import com.pubnub.chat.internal.timer.PlatformTimer.Companion.runPeriodically
import com.pubnub.chat.internal.timer.PlatformTimer.Companion.runWithDelay
import com.pubnub.chat.internal.timer.TimerManager
import com.pubnub.chat.internal.timer.createTimerManager
import com.pubnub.chat.internal.util.channelsUrlDecoded
import com.pubnub.chat.internal.util.getPhraseToLookFor
import com.pubnub.chat.internal.util.logErrorAndReturnException
Expand Down Expand Up @@ -132,13 +125,15 @@ class ChatImpl(
?: MessageActionType.EDITED.toString(),
override val deleteMessageActionName: String = config.customPayloads?.deleteMessageActionName
?: MessageActionType.DELETED.toString(),
override val timerManager: TimerManager = createTimerManager()
) : ChatInternal {
override var currentUser: User =
UserImpl(this, pubNub.configuration.userId.value, name = pubNub.configuration.userId.value)
private set

private val suggestedChannelsCache: MutableMap<String, Set<Channel>> = mutableMapOf()
private val suggestedUsersCache: MutableMap<String, Set<User>> = mutableMapOf()

private var lastSavedActivityInterval: PlatformTimer? = null
private var runWithDelayTimer: PlatformTimer? = null

Expand Down Expand Up @@ -266,10 +261,8 @@ class ChatImpl(
}

return pubNub.getUUIDMetadata(uuid = userId, includeCustom = true)
.then<PNUUIDMetadataResult, User?> { pnUUIDMetadataResult: PNUUIDMetadataResult ->
pnUUIDMetadataResult.data?.let { pnUUIDMetadata ->
UserImpl.fromDTO(this, pnUUIDMetadata)
} ?: log.pnError(PNUUID_METADATA_RESULT_IS_NULL)
.then { pnUUIDMetadataResult: PNUUIDMetadataResult ->
UserImpl.fromDTO(this, pnUUIDMetadataResult.data)
}.catch {
if (it is PubNubException && it.statusCode == HTTP_ERROR_404) {
Result.success(null)
Expand Down Expand Up @@ -431,10 +424,8 @@ class ChatImpl(
return log.logErrorAndReturnException(CHANNEL_ID_IS_REQUIRED).asFuture()
}
return pubNub.getChannelMetadata(channel = channelId)
.then<PNChannelMetadataResult, Channel?> { pnChannelMetadataResult: PNChannelMetadataResult ->
pnChannelMetadataResult.data?.let { pnChannelMetadata: PNChannelMetadata ->
ChannelImpl.fromDTO(this, pnChannelMetadata)
} ?: log.pnError(PNCHANNEL_METADATA_IS_NULL)
.then { pnChannelMetadataResult: PNChannelMetadataResult ->
ChannelImpl.fromDTO(this, pnChannelMetadataResult.data)
}.catch { exception ->
if (exception is PubNubException && exception.statusCode == HTTP_ERROR_404) {
Result.success(null)
Expand Down Expand Up @@ -1020,6 +1011,11 @@ class ChatImpl(
}
}

override fun destroy() {
timerManager.destroy()
pubNub.destroy()
}

private fun getTimetokenFromHistoryMessage(
channelId: String,
pnFetchMessagesResult: PNFetchMessagesResult
Expand All @@ -1042,9 +1038,7 @@ class ChatImpl(
private fun getChannelData(id: String): PNFuture<Channel> {
return pubNub.getChannelMetadata(channel = id, includeCustom = false)
.then { pnChannelMetadataResult: PNChannelMetadataResult ->
pnChannelMetadataResult.data?.let { pnChannelMetadata ->
ChannelImpl.fromDTO(this, pnChannelMetadata)
} ?: log.pnError(CHANNEL_META_DATA_IS_EMPTY)
ChannelImpl.fromDTO(this, pnChannelMetadataResult.data)
}.catch { exception ->
Result.failure(PubNubException(FAILED_TO_RETRIEVE_CHANNEL_DATA, exception))
}
Expand All @@ -1063,9 +1057,7 @@ class ChatImpl(
type = updatedUser.type,
status = updatedUser.status,
).then { pnUUIDMetadataResult ->
pnUUIDMetadataResult.data?.let { pnUUIDMetadata: PNUUIDMetadata ->
UserImpl.fromDTO(this, pnUUIDMetadata)
} ?: log.pnError(PNUUID_METADATA_IS_NULL)
UserImpl.fromDTO(this, pnUUIDMetadataResult.data)
}
}

Expand All @@ -1083,9 +1075,7 @@ class ChatImpl(
type = updatedChannel.type?.stringValue,
status = updatedChannel.status
).then { pnChannelMetadataResult ->
pnChannelMetadataResult.data?.let { pnChannelMetadata: PNChannelMetadata ->
ChannelImpl.fromDTO(this, pnChannelMetadata)
} ?: log.pnError(PNCHANNEL_METADATA_IS_NULL)
ChannelImpl.fromDTO(this, pnChannelMetadataResult.data)
}.catch { exception ->
Result.failure(PubNubException(FAILED_TO_SOFT_DELETE_CHANNEL, exception))
}
Expand All @@ -1111,9 +1101,7 @@ class ChatImpl(
type = type?.stringValue,
status = status
).then { pnChannelMetadataResult ->
pnChannelMetadataResult.data?.let { pnChannelMetadata ->
ChannelImpl.fromDTO(this, pnChannelMetadata)
} ?: log.pnError(NO_DATA_AVAILABLE_TO_CREATE_OR_UPDATE_CHANNEL)
ChannelImpl.fromDTO(this, pnChannelMetadataResult.data)
}.catch { exception ->
Result.failure(PubNubException(FAILED_TO_CREATE_UPDATE_CHANNEL_DATA, exception))
}
Expand All @@ -1140,9 +1128,7 @@ class ChatImpl(
type = type,
status = status
).then { pnUUIDMetadataResult ->
pnUUIDMetadataResult.data?.let { pnUUIDMetadata ->
UserImpl.fromDTO(this, pnUUIDMetadata)
} ?: log.pnError(NO_DATA_AVAILABLE_TO_CREATE_OR_UPDATE_CHANNEL)
UserImpl.fromDTO(this, pnUUIDMetadataResult.data)
}.catch { exception ->
Result.failure(PubNubException(FAILED_TO_CREATE_UPDATE_USER_DATA, exception))
}
Expand Down Expand Up @@ -1210,7 +1196,7 @@ class ChatImpl(
}

val remainingTime = config.storeUserActivityInterval - elapsedTimeSinceLastCheck
runWithDelayTimer = runWithDelay(remainingTime) {
runWithDelayTimer = timerManager.runWithDelay(remainingTime) {
runSaveTimestampInterval().async {}
}

Expand All @@ -1223,7 +1209,7 @@ class ChatImpl(
return saveTimeStampFunc().then {
lastSavedActivityInterval?.cancel()
lastSavedActivityInterval =
runPeriodically(config.storeUserActivityInterval) {
timerManager.runPeriodically(config.storeUserActivityInterval) {
saveTimeStampFunc().async { result: Result<Unit> ->
result.onFailure { e ->
log.error(err = e, msg = { e.message })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import com.pubnub.chat.Channel
import com.pubnub.chat.Chat
import com.pubnub.chat.Message
import com.pubnub.chat.User
import com.pubnub.chat.internal.timer.TimerManager
import com.pubnub.kmp.PNFuture

interface ChatInternal : Chat {
val editMessageActionName: String
val deleteMessageActionName: String
val timerManager: TimerManager

fun createUser(user: User): PNFuture<User>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ import com.pubnub.chat.internal.message.BaseMessage
import com.pubnub.chat.internal.message.MessageImpl
import com.pubnub.chat.internal.restrictions.RestrictionImpl
import com.pubnub.chat.internal.serialization.PNDataEncoder
import com.pubnub.chat.internal.timer.PlatformTimer.Companion.runWithDelay
import com.pubnub.chat.internal.util.channelsUrlDecoded
import com.pubnub.chat.internal.util.getPhraseToLookFor
import com.pubnub.chat.internal.util.logErrorAndReturnException
Expand Down Expand Up @@ -118,7 +117,8 @@ abstract class BaseChannel<C : Channel, M : Message>(
private val sendTextRateLimiter by lazy {
ExponentialRateLimiter(
type?.let { typeNotNull -> chat.config.rateLimitPerChannel[typeNotNull] } ?: Duration.ZERO,
chat.config.rateLimitFactor
chat.config.rateLimitFactor,
chat.timerManager
)
}
private val channelFilterString get() = "channel.id == '${this.id}'"
Expand Down Expand Up @@ -192,7 +192,7 @@ abstract class BaseChannel<C : Channel, M : Message>(
val isTyping = event.payload.value

if (isTyping) {
runWithDelay(typingTimeout + 10.milliseconds) { // +10ms just to make sure the timeout expires
chat.timerManager.runWithDelay(typingTimeout + 10.milliseconds) { // +10ms just to make sure the timeout expires
typingIndicatorsLock.withLock {
removeExpiredTypingIndicators(typingTimeout, typingIndicators, clock.now())
typingIndicators.keys.toList()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,7 @@ abstract class BaseMessage<T : Message>(

override val hasThread: Boolean
get() {
if (actions?.containsKey(THREAD_ROOT_ID) != true) {
return false
}
return actions?.get(THREAD_ROOT_ID)?.entries?.firstOrNull()?.value?.isNotEmpty() ?: false
return actions?.get(THREAD_ROOT_ID)?.values?.firstOrNull()?.isNotEmpty() ?: false
}

@OptIn(ExperimentalSerializationApi::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ package com.pubnub.chat.internal.timer

import kotlin.time.Duration

expect class PlatformTimer {
companion object {
fun runPeriodically(period: Duration, action: () -> Unit): PlatformTimer
expect fun createTimerManager(): TimerManager

interface TimerManager {
fun runPeriodically(period: Duration, action: () -> Unit): PlatformTimer

fun runWithDelay(delay: Duration, action: () -> Unit): PlatformTimer
}
fun runWithDelay(delay: Duration, action: () -> Unit): PlatformTimer

fun destroy()
}

expect class PlatformTimer {
fun cancel()
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.pubnub.chat.internal.utils

import com.pubnub.api.v2.callbacks.Consumer
import com.pubnub.api.v2.callbacks.Result
import com.pubnub.chat.internal.timer.PlatformTimer
import com.pubnub.chat.internal.timer.TimerManager
import com.pubnub.kmp.PNFuture
import kotlinx.atomicfu.locks.reentrantLock
import kotlinx.atomicfu.locks.withLock
Expand All @@ -13,6 +13,7 @@ import kotlin.time.Duration
class ExponentialRateLimiter(
private val baseInterval: Duration = Duration.ZERO,
private val exponentialFactor: Int = 2,
private val timerManager: TimerManager
) {
private val lock = reentrantLock()

Expand All @@ -29,7 +30,7 @@ class ExponentialRateLimiter(
queue.addLast(Pair(future as PNFuture<Any>, completion as Consumer<Result<Any>>))
if (!isProcessing) {
isProcessing = true
PlatformTimer.runWithDelay(Duration.ZERO) {
timerManager.runWithDelay(Duration.ZERO) {
processQueue(0)
}
}
Expand All @@ -48,7 +49,7 @@ class ExponentialRateLimiter(
item.first.async {
item.second.accept(it)
}
PlatformTimer.runWithDelay(this.baseInterval * exponentialFactor.toDouble().pow(penalty)) {
timerManager.runWithDelay(this.baseInterval * exponentialFactor.toDouble().pow(penalty)) {
processQueue(penalty + 1)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,13 @@ class ChatIntegrationTest : BaseChatIntegrationTest() {
assertEquals(mapOf("abc" to "def"), (eventFromHistory.payload as EventContent.Custom).data)
}

@Test
fun destroy_completes_successfully() {
chat.getChannel("abc").async {}
channel01.streamUpdates { }
chat.destroy()
}

private suspend fun assertPushChannels(expectedNumberOfChannels: Int) {
val pushChannels = chat.getPushChannels().await()
assertEquals(expectedNumberOfChannels, pushChannels.size)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.pubnub.internal

import com.pubnub.chat.internal.timer.createTimerManager
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.time.Duration.Companion.seconds

class TimerManagerTest {
@Test
fun cancelOneOff() = runTest {
val timerManager = createTimerManager()
val completable1 = CompletableDeferred<Unit>()
val completable2 = CompletableDeferred<Unit>()

// this should run
timerManager.runWithDelay(1.seconds) {
completable1.complete(Unit)
}

// this shouldn't run
timerManager.runWithDelay(2.seconds) {
completable2.complete(Unit)
}

completable1.await()
timerManager.destroy()

// give completable2 a chance to complete (in case destroy() doesn't work)
withContext(Dispatchers.Default) {
delay(2.seconds)
}

assertFalse { completable2.isCompleted }
}

@Test
fun cancelPeriodic() = runTest {
val timerManager = createTimerManager()
val counter = atomic(0)
val counter2 = atomic(0)

// this should run 1 time
timerManager.runPeriodically(1.seconds) {
counter.incrementAndGet()
}

// this should run 3 times
// let's also try to start it from a background thread to test if cancellation works in that case
withContext(Dispatchers.Default) {
timerManager.runPeriodically(0.5.seconds) {
counter2.incrementAndGet()
}
}

withContext(Dispatchers.Default) {
delay(1.75.seconds)
}
timerManager.destroy()

withContext(Dispatchers.Default) {
delay(1.seconds)
}

assertEquals(1, counter.value)
assertEquals(3, counter2.value)
}
}
Loading

0 comments on commit 82462a3

Please sign in to comment.