diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 33882e6b3..08af5e241 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,11 +5,11 @@ logback = "1.2.11" okhttp = "4.12.0" retrofit2 = "2.9.0" nexus = "2.0.0" -kotlin = "2.0.20" +kotlin = "2.0.21" vanniktech = "0.29.0" ktlint = "12.1.0" dokka = "1.9.20" -kotlinx_datetime = "0.6.0" +kotlinx_datetime = "0.6.1" kotlinx_coroutines = "1.8.1" [libraries] diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index 048f60b36..c41d91c57 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -2,11 +2,6 @@ # yarn lockfile v1 -"@js-joda/core@3.2.0": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273" - integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg== - "@tootallnate/quickjs-emscripten@^0.23.0": version "0.23.0" resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c" diff --git a/pubnub-kotlin/pubnub-kotlin-api/build.gradle.kts b/pubnub-kotlin/pubnub-kotlin-api/build.gradle.kts index b87965540..180ec9906 100644 --- a/pubnub-kotlin/pubnub-kotlin-api/build.gradle.kts +++ b/pubnub-kotlin/pubnub-kotlin-api/build.gradle.kts @@ -1,3 +1,4 @@ +import com.pubnub.gradle.enableAnyIosTarget import com.pubnub.gradle.enableJsTarget plugins { @@ -17,7 +18,15 @@ kotlin { } } + val nonJs = create("nonJs") { + dependsOn(commonMain) + dependencies { + api(libs.kotlinx.datetime) + } + } + val jvmMain by getting { + dependsOn(nonJs) dependencies { api(libs.retrofit2) api(libs.okhttp) @@ -28,6 +37,15 @@ kotlin { } } + if (enableAnyIosTarget) { + val appleMain by getting { + dependsOn(nonJs) + dependencies { + api(libs.kotlinx.datetime) + } + } + } + if (enableJsTarget) { val jsMain by getting { dependencies { diff --git a/pubnub-kotlin/pubnub-kotlin-api/src/commonMain/kotlin/com/pubnub/api/utils/TimetokenUtil.kt b/pubnub-kotlin/pubnub-kotlin-api/src/commonMain/kotlin/com/pubnub/api/utils/TimetokenUtil.kt index cabedab21..755187856 100644 --- a/pubnub-kotlin/pubnub-kotlin-api/src/commonMain/kotlin/com/pubnub/api/utils/TimetokenUtil.kt +++ b/pubnub-kotlin/pubnub-kotlin-api/src/commonMain/kotlin/com/pubnub/api/utils/TimetokenUtil.kt @@ -1,7 +1,5 @@ package com.pubnub.api.utils -import kotlinx.datetime.Instant - /** * Utility object for converting PubNub timetokens to various date-time representations and vice versa. * diff --git a/pubnub-kotlin/pubnub-kotlin-api/src/commonMain/kotlin/com/pubnub/api/utils/datetime.kt b/pubnub-kotlin/pubnub-kotlin-api/src/commonMain/kotlin/com/pubnub/api/utils/datetime.kt new file mode 100644 index 000000000..77532e218 --- /dev/null +++ b/pubnub-kotlin/pubnub-kotlin-api/src/commonMain/kotlin/com/pubnub/api/utils/datetime.kt @@ -0,0 +1,32 @@ +package com.pubnub.api.utils + +import kotlin.time.Duration + +expect class Instant : Comparable { + val epochSeconds: Long + val nanosecondsOfSecond: Int + + fun toEpochMilliseconds(): Long + + operator fun plus(duration: Duration): Instant + + operator fun minus(duration: Duration): Instant + + operator fun minus(other: Instant): Duration + + override operator fun compareTo(other: Instant): Int + + companion object { + fun fromEpochMilliseconds(epochMilliseconds: Long): Instant + + fun fromEpochSeconds(epochSeconds: Long, nanosecondAdjustment: Int): Instant + } +} + +expect interface Clock { + fun now(): Instant + + object System : Clock { + override fun now(): Instant + } +} diff --git a/pubnub-kotlin/pubnub-kotlin-api/src/commonTest/kotlin/com/pubnub/api/util/InstantTest.kt b/pubnub-kotlin/pubnub-kotlin-api/src/commonTest/kotlin/com/pubnub/api/util/InstantTest.kt new file mode 100644 index 000000000..a054c6430 --- /dev/null +++ b/pubnub-kotlin/pubnub-kotlin-api/src/commonTest/kotlin/com/pubnub/api/util/InstantTest.kt @@ -0,0 +1,70 @@ +package com.pubnub.api.util + +import com.pubnub.api.utils.Instant +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.nanoseconds +import kotlin.time.Duration.Companion.seconds + +class InstantTest { + @Test + fun plusDuration() { + val nowMillis = 1732793616984 + val now = Instant.fromEpochMilliseconds(nowMillis) + val later = now + 1500.milliseconds + + assertEquals(nowMillis + 1500, later.toEpochMilliseconds()) + + val laterWithNanos = now + 2000.nanoseconds + assertEquals(now.epochSeconds, laterWithNanos.epochSeconds) + assertEquals(now.nanosecondsOfSecond + 2000, laterWithNanos.nanosecondsOfSecond) + + val laterWithSecondsAndNanos = now + (1.seconds + 2000.nanoseconds) + assertEquals(now.epochSeconds + 1, laterWithSecondsAndNanos.epochSeconds) + assertEquals(now.nanosecondsOfSecond + 2000, laterWithSecondsAndNanos.nanosecondsOfSecond) + } + + @Test + fun minusDuration() { + val nowMillis = 1732793616984 + val now = Instant.fromEpochMilliseconds(nowMillis) + val later = now - 1500.milliseconds + + assertEquals(nowMillis - 1500, later.toEpochMilliseconds()) + + val laterWithNanos = now - 2000.nanoseconds + assertEquals(now.epochSeconds, laterWithNanos.epochSeconds) + assertEquals(now.nanosecondsOfSecond - 2000, laterWithNanos.nanosecondsOfSecond) + + val laterWithSecondsAndNanos = now - (1.seconds + 2000.nanoseconds) + assertEquals(now.epochSeconds - 1, laterWithSecondsAndNanos.epochSeconds) + assertEquals(now.nanosecondsOfSecond - 2000, laterWithSecondsAndNanos.nanosecondsOfSecond) + } + + @Test + fun minusInstant() { + val nowMillis = 1732793616984 + val laterMillis = 1732793616984 + 1500 + val now = Instant.fromEpochMilliseconds(nowMillis) + val later = Instant.fromEpochMilliseconds(laterMillis) + + assertEquals(1500.milliseconds, later - now) + assertEquals(-1500.milliseconds, now - later) + } + + @Test + fun compareTo() { + val nowMillis = 1732793616984 + val laterMillis = 1732793616984 + 1500 + val now = Instant.fromEpochMilliseconds(nowMillis) + val later = Instant.fromEpochMilliseconds(laterMillis) + + assertTrue(now < later) + assertTrue(later > now) + + assertTrue(now + 100.nanoseconds > now) + assertTrue(now - 100.nanoseconds < now) + } +} diff --git a/pubnub-kotlin/pubnub-kotlin-api/src/commonTest/kotlin/com/pubnub/api/util/TimetokenUtilsTest.kt b/pubnub-kotlin/pubnub-kotlin-api/src/commonTest/kotlin/com/pubnub/api/util/TimetokenUtilsTest.kt index 4e978841f..c4e7aa333 100644 --- a/pubnub-kotlin/pubnub-kotlin-api/src/commonTest/kotlin/com/pubnub/api/util/TimetokenUtilsTest.kt +++ b/pubnub-kotlin/pubnub-kotlin-api/src/commonTest/kotlin/com/pubnub/api/util/TimetokenUtilsTest.kt @@ -1,9 +1,7 @@ package com.pubnub.api.util +import com.pubnub.api.utils.Instant import com.pubnub.api.utils.TimetokenUtil -import kotlinx.datetime.Instant -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -19,9 +17,6 @@ class TimetokenUtilsTest { val timetokenResult = TimetokenUtil.instantToTimetoken(instant) // then - val localDateTimeInUTC = instant.toLocalDateTime(TimeZone.UTC) - assertEquals("2024-09-30", localDateTimeInUTC.date.toString()) - assertEquals("11:24:20.623211800", localDateTimeInUTC.time.toString()) assertEquals(timetoken, timetokenResult) } @@ -35,9 +30,7 @@ class TimetokenUtilsTest { // then val instant: Instant = TimetokenUtil.timetokenToInstant(timetoken) - val localDateTimeInUTC = instant.toLocalDateTime(TimeZone.UTC) - assertEquals("2024-10-02", localDateTimeInUTC.date.toString()) - assertEquals("11:02:15.316", localDateTimeInUTC.time.toString()) + assertEquals(unixTime, instant.toEpochMilliseconds()) } @Test @@ -59,8 +52,6 @@ class TimetokenUtilsTest { // then val instant = Instant.fromEpochMilliseconds(unixTime) - val toLocalDateTime = instant.toLocalDateTime(TimeZone.UTC) - assertEquals("2024-09-30", toLocalDateTime.date.toString()) - assertEquals("11:24:20.623", toLocalDateTime.time.toString()) + assertEquals(timetoken / 10000 * 10000, TimetokenUtil.instantToTimetoken(instant)) } } diff --git a/pubnub-kotlin/pubnub-kotlin-api/src/commonTest/kotlin/com/pubnub/test/integration/MessageCountsTest.kt b/pubnub-kotlin/pubnub-kotlin-api/src/commonTest/kotlin/com/pubnub/test/integration/MessageCountsTest.kt index 3cd4e8d99..a398614cc 100644 --- a/pubnub-kotlin/pubnub-kotlin-api/src/commonTest/kotlin/com/pubnub/test/integration/MessageCountsTest.kt +++ b/pubnub-kotlin/pubnub-kotlin-api/src/commonTest/kotlin/com/pubnub/test/integration/MessageCountsTest.kt @@ -3,9 +3,13 @@ package com.pubnub.test.integration import com.pubnub.test.BaseIntegrationTest import com.pubnub.test.await import com.pubnub.test.randomString +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.time.Duration.Companion.seconds class MessageCountsTest : BaseIntegrationTest() { private val channel = randomString() @@ -35,6 +39,9 @@ class MessageCountsTest : BaseIntegrationTest() { ).await().timetoken } + withContext(Dispatchers.Default) { + delay(2.seconds) + } val counts = pubnub.messageCounts( listOf(channel, otherChannel), listOf(timetokens.first() - 1, timetokensOther.first() - 1) diff --git a/pubnub-kotlin/pubnub-kotlin-api/src/jsMain/kotlin/com/pubnub/api/utils/datetime.js.kt b/pubnub-kotlin/pubnub-kotlin-api/src/jsMain/kotlin/com/pubnub/api/utils/datetime.js.kt new file mode 100644 index 000000000..ee1e7c53e --- /dev/null +++ b/pubnub-kotlin/pubnub-kotlin-api/src/jsMain/kotlin/com/pubnub/api/utils/datetime.js.kt @@ -0,0 +1,73 @@ +package com.pubnub.api.utils + +import kotlin.js.Date +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.nanoseconds +import kotlin.time.Duration.Companion.seconds + +actual class Instant( + actual val epochSeconds: Long, + actual val nanosecondsOfSecond: Int = 0, +) : Comparable { + actual fun toEpochMilliseconds(): Long { + return epochSeconds.seconds.inWholeMilliseconds + nanosecondsOfSecond.nanoseconds.inWholeMilliseconds + } + + actual operator fun plus(duration: Duration): Instant { + val durationWholeSecondsOnly = duration.inWholeSeconds.seconds + val durationNanosOnly = duration - durationWholeSecondsOnly + val sum = add(epochSeconds to nanosecondsOfSecond, duration.inWholeSeconds to durationNanosOnly.inWholeNanoseconds.toInt()) + return Instant(sum.first, sum.second) + } + + actual operator fun minus(duration: Duration): Instant { + return plus(-duration) + } + + actual operator fun minus(other: Instant): Duration { + return epochSeconds.seconds + nanosecondsOfSecond.nanoseconds - other.epochSeconds.seconds - other.nanosecondsOfSecond.nanoseconds + } + + actual override operator fun compareTo(other: Instant): Int { + return epochSeconds.compareTo(other.epochSeconds) + .takeIf { it != 0 } + ?: nanosecondsOfSecond.compareTo(other.nanosecondsOfSecond) + } + + private fun add(secondsAndNanos: SecondsAndNanos, secondsAndNanos2: SecondsAndNanos): Pair { + val nanosSum = secondsAndNanos.nanos + secondsAndNanos2.nanos + val secondsFromNanos = nanosSum.inWholeSeconds.seconds + + val secondsResult = secondsAndNanos.seconds + secondsAndNanos2.seconds + secondsFromNanos + val nanosResult = nanosSum - secondsFromNanos + return secondsResult.inWholeSeconds to nanosResult.inWholeNanoseconds.toInt() + } + + actual companion object { + actual fun fromEpochMilliseconds(epochMilliseconds: Long): Instant { + val wholeSeconds = epochMilliseconds.milliseconds.inWholeSeconds + val nanos = (epochMilliseconds.milliseconds - wholeSeconds.seconds).inWholeNanoseconds + return Instant(wholeSeconds, nanos.toInt()) + } + + actual fun fromEpochSeconds(epochSeconds: Long, nanosecondAdjustment: Int): Instant { + return Instant(epochSeconds, nanosecondAdjustment) + } + } +} + +actual interface Clock { + actual fun now(): Instant + + actual object System : Clock { + actual override fun now(): Instant { + return Instant.fromEpochMilliseconds(Date.now().toLong()) + } + } +} + +typealias SecondsAndNanos = Pair + +val SecondsAndNanos.seconds get() = this.first.seconds +val SecondsAndNanos.nanos get() = this.second.nanoseconds diff --git a/pubnub-kotlin/pubnub-kotlin-api/src/nonJs/kotlin/com/pubnub/api/utils/datetime.nonJs.kt b/pubnub-kotlin/pubnub-kotlin-api/src/nonJs/kotlin/com/pubnub/api/utils/datetime.nonJs.kt new file mode 100644 index 000000000..09094886d --- /dev/null +++ b/pubnub-kotlin/pubnub-kotlin-api/src/nonJs/kotlin/com/pubnub/api/utils/datetime.nonJs.kt @@ -0,0 +1,16 @@ +package com.pubnub.api.utils + +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant + +actual typealias Instant = Instant + +actual interface Clock { + actual fun now(): com.pubnub.api.utils.Instant + + actual object System : com.pubnub.api.utils.Clock { + actual override fun now(): com.pubnub.api.utils.Instant { + return Clock.System.now() + } + } +} diff --git a/pubnub-kotlin/pubnub-kotlin-core-api/build.gradle.kts b/pubnub-kotlin/pubnub-kotlin-core-api/build.gradle.kts index b30a19a37..60f8741b4 100644 --- a/pubnub-kotlin/pubnub-kotlin-core-api/build.gradle.kts +++ b/pubnub-kotlin/pubnub-kotlin-core-api/build.gradle.kts @@ -12,7 +12,6 @@ kotlin { val commonMain by getting { dependencies { implementation(libs.kotlinx.atomicfu) - api(libs.kotlinx.datetime) } }