From cece5809ea750ad34277806fe049a7e9c1cb6a0c Mon Sep 17 00:00:00 2001 From: Artur Babichev Date: Tue, 1 Jul 2025 14:36:37 +0400 Subject: [PATCH] Address review comments --- README.MD | 1 + kronos/build.gradle.kts | 3 ++ .../kotlin/com/softartdev/kronos/Network.kt | 13 ++++++ .../com/softartdev/kronos/WasmNetworkClock.kt | 44 +++++++++++++++++++ sampleApp/build.gradle.kts | 3 ++ .../com/softartdev/kronos/sample/App.wasm.kt | 22 ++++++++++ .../com/softartdev/kronos/sample/Platform.kt | 7 +++ 7 files changed, 93 insertions(+) create mode 100644 kronos/src/wasmJsMain/kotlin/com/softartdev/kronos/Network.kt create mode 100644 kronos/src/wasmJsMain/kotlin/com/softartdev/kronos/WasmNetworkClock.kt create mode 100644 sampleApp/src/wasmJsMain/kotlin/com/softartdev/kronos/sample/App.wasm.kt create mode 100644 sampleApp/src/wasmJsMain/kotlin/com/softartdev/kronos/sample/Platform.kt diff --git a/README.MD b/README.MD index aebf29c..a29b21f 100644 --- a/README.MD +++ b/README.MD @@ -7,6 +7,7 @@ Kotlin Multiplatform library for network time synchronization. It extends the [` - Android - iOS - Desktop JVM (MacOS, Linux, Windows) +- Web ## Usage ### `kotlin.time` Extension The library [extends the main `Clock` interface](https://github.com/softartdev/Kronos-Multiplatform/blob/main/kronos/src/commonMain/kotlin/com/softartdev/kronos/ClockExt.kt) from the standard library. You can use the [`Clock.Network`](https://github.com/softartdev/Kronos-Multiplatform/blob/main/kronos/src/commonMain/kotlin/com/softartdev/kronos/NetworkClock.kt) class to retrieve the current network time, similar to using the built-in [`Clock.System`](https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/src/kotlin/time/Clock.kt#L60) instance. diff --git a/kronos/build.gradle.kts b/kronos/build.gradle.kts index ce3a933..d3a3ced 100644 --- a/kronos/build.gradle.kts +++ b/kronos/build.gradle.kts @@ -10,6 +10,7 @@ version = libs.versions.kronos.get() kotlin { jvmToolchain(libs.versions.jdk.get().toInt()) jvm() + wasmJs() androidTarget { publishLibraryVariants("release", "debug") } @@ -53,6 +54,8 @@ kotlin { implementation(libs.androidx.test) } } + val wasmJsMain by getting + val wasmJsTest by getting val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting diff --git a/kronos/src/wasmJsMain/kotlin/com/softartdev/kronos/Network.kt b/kronos/src/wasmJsMain/kotlin/com/softartdev/kronos/Network.kt new file mode 100644 index 0000000..b7a1d02 --- /dev/null +++ b/kronos/src/wasmJsMain/kotlin/com/softartdev/kronos/Network.kt @@ -0,0 +1,13 @@ +@file:OptIn(ExperimentalTime::class) + +package com.softartdev.kronos + +import kotlin.time.Clock +import kotlin.time.ExperimentalTime + +actual val Clock.Companion.Network: NetworkClock + get() = WasmNetworkClock + +fun NetworkClock.sync() = WasmNetworkClock.sync() +fun NetworkClock.blockingSync(): Boolean = WasmNetworkClock.blockingSync() +suspend fun NetworkClock.awaitSync(): Boolean = WasmNetworkClock.awaitSync() diff --git a/kronos/src/wasmJsMain/kotlin/com/softartdev/kronos/WasmNetworkClock.kt b/kronos/src/wasmJsMain/kotlin/com/softartdev/kronos/WasmNetworkClock.kt new file mode 100644 index 0000000..5ec1787 --- /dev/null +++ b/kronos/src/wasmJsMain/kotlin/com/softartdev/kronos/WasmNetworkClock.kt @@ -0,0 +1,44 @@ +package com.softartdev.kronos + +import kotlinx.browser.window +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.await +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlin.time.Clock + +/** + * Fetches network time using the public WorldTime API and stores the offset + * from the local system clock. The [sync] functions update the offset + * asynchronously or in a blocking fashion. After synchronization the clock + * provides network based timestamps via [getCurrentNtpTimeMs]. + */ +object WasmNetworkClock : NetworkClock { + + private var offsetMs: Long? = null + + fun sync() { + GlobalScope.launch { awaitSync() } + } + + fun blockingSync(): Boolean = runBlocking { awaitSync() } + + suspend fun awaitSync(): Boolean = try { + val response = window.fetch("https://worldtimeapi.org/api/ip").await() + val json = response.json().await() as dynamic + val dateStr = json.datetime as String + val networkTimeMs = js("Date.parse(dateStr)").unsafeCast().toLong() + val systemTimeMs = js("Date.now()").unsafeCast().toLong() + offsetMs = networkTimeMs - systemTimeMs + true + } catch (t: Throwable) { + console.error("Failed to sync time", t) + false + } + + override fun getCurrentNtpTimeMs(): Long? { + val offset = offsetMs ?: return null + val systemTimeMs = js("Date.now()").unsafeCast().toLong() + return systemTimeMs + offset + } +} diff --git a/sampleApp/build.gradle.kts b/sampleApp/build.gradle.kts index c8f5505..843db35 100644 --- a/sampleApp/build.gradle.kts +++ b/sampleApp/build.gradle.kts @@ -16,6 +16,7 @@ kotlin { jvmToolchain(libs.versions.jdk.get().toInt()) jvm("desktop") androidTarget() + wasmJs() iosX64() iosArm64() iosSimulatorArm64() @@ -62,6 +63,8 @@ kotlin { implementation(compose.preview) } } + val wasmJsMain by getting + val wasmJsTest by getting val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting diff --git a/sampleApp/src/wasmJsMain/kotlin/com/softartdev/kronos/sample/App.wasm.kt b/sampleApp/src/wasmJsMain/kotlin/com/softartdev/kronos/sample/App.wasm.kt new file mode 100644 index 0000000..fbebed9 --- /dev/null +++ b/sampleApp/src/wasmJsMain/kotlin/com/softartdev/kronos/sample/App.wasm.kt @@ -0,0 +1,22 @@ +@file:OptIn(ExperimentalTime::class) + +package com.softartdev.kronos.sample + +import kotlinx.browser.window +import kotlin.time.Clock +import kotlin.time.ExperimentalTime + +internal actual fun openUrl(url: String?) { + url ?: return + window.open(url, "_blank") +} + +internal actual fun clickSync() = Clock.Network.sync() + +internal actual fun clickBlockingSync() { + Clock.Network.blockingSync() +} + +internal actual suspend fun clickAwaitSync() { + Clock.Network.awaitSync() +} diff --git a/sampleApp/src/wasmJsMain/kotlin/com/softartdev/kronos/sample/Platform.kt b/sampleApp/src/wasmJsMain/kotlin/com/softartdev/kronos/sample/Platform.kt new file mode 100644 index 0000000..487250f --- /dev/null +++ b/sampleApp/src/wasmJsMain/kotlin/com/softartdev/kronos/sample/Platform.kt @@ -0,0 +1,7 @@ +package com.softartdev.kronos.sample + +class WasmJsPlatform : Platform { + override val name: String = "Web" +} + +actual fun getPlatform(): Platform = WasmJsPlatform()