diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ba367bda8..a7bdfbe47 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ atomicfu = "0.29.0" coroutines = "1.10.2" jna = "5.18.1" jvm-toolchain = "17" -kotlin = "2.2.20" +kotlin = "2.2.21" tuulbox = "8.1.0" [libraries] @@ -16,6 +16,7 @@ equalsverifier = { module = "nl.jqno.equalsverifier:equalsverifier", version = " jna = { module = "net.java.dev.jna:jna", version.ref = "jna" } khronicle = { module = "com.juul.khronicle:khronicle-core", version = "0.6.0" } kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +kotlinx-browser = { module = "org.jetbrains.kotlinx:kotlinx-browser", version = "0.5.0" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } @@ -26,6 +27,7 @@ tomlkt = { module = "net.peanuuutz.tomlkt:tomlkt", version = "0.5.0" } tuulbox-collections = { module = "com.juul.tuulbox:collections", version.ref = "tuulbox" } tuulbox-coroutines = { module = "com.juul.tuulbox:coroutines", version.ref = "tuulbox" } wrappers-bom = { module = "org.jetbrains.kotlin-wrappers:kotlin-wrappers-bom", version = "2025.12.6" } +wrappers-browser = { module = "org.jetbrains.kotlin-wrappers:kotlin-browser" } wrappers-web = { module = "org.jetbrains.kotlin-wrappers:kotlin-web" } [plugins] diff --git a/kable-core/build.gradle.kts b/kable-core/build.gradle.kts index 23214df03..36987adce 100644 --- a/kable-core/build.gradle.kts +++ b/kable-core/build.gradle.kts @@ -20,10 +20,12 @@ kotlin { macosArm64() macosX64() jvm() + wasmJs().browser() sourceSets { all { languageSettings { + optIn("kotlin.js.ExperimentalWasmJsInterop") optIn("kotlin.uuid.ExperimentalUuidApi") } } @@ -31,7 +33,6 @@ kotlin { commonMain.dependencies { api(libs.kotlinx.coroutines.core) api(libs.kotlinx.io) - implementation(libs.tuulbox.collections) } commonTest.dependencies { @@ -59,7 +60,9 @@ kotlin { implementation(libs.robolectric) } - jsMain.dependencies { + webMain.dependencies { + api(libs.kotlinx.browser) + api(libs.wrappers.browser) api(libs.wrappers.web) api(project.dependencies.platform(libs.wrappers.bom)) } diff --git a/kable-core/src/commonTest/kotlin/SharedRepeatableActionTests.kt b/kable-core/src/commonTest/kotlin/SharedRepeatableActionTests.kt index 8f013e48a..a86281bc6 100644 --- a/kable-core/src/commonTest/kotlin/SharedRepeatableActionTests.kt +++ b/kable-core/src/commonTest/kotlin/SharedRepeatableActionTests.kt @@ -1,6 +1,5 @@ package com.juul.kable -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart.UNDISPATCHED import kotlinx.coroutines.Job @@ -22,6 +21,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.test.runTest import kotlinx.coroutines.yield +import kotlin.coroutines.cancellation.CancellationException import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith diff --git a/kable-core/src/jsMain/kotlin/Bluetooth.kt b/kable-core/src/jsMain/kotlin/Bluetooth.kt deleted file mode 100644 index 2cd2df01f..000000000 --- a/kable-core/src/jsMain/kotlin/Bluetooth.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.juul.kable - -import com.juul.kable.external.Bluetooth -import com.juul.kable.external.bluetooth -import js.errors.ReferenceError -import kotlinx.browser.window - -/** - * @return [Bluetooth] object or `null` if bluetooth is [unavailable](https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth#browser_compatibility). - */ -internal fun bluetoothOrNull(): Bluetooth? { - val navigator = try { - window.navigator - } catch (e: ReferenceError) { - // ReferenceError: window is not defined - return null - } - return navigator.bluetooth.takeIf { it !== undefined } -} - -/** - * @throws IllegalStateException If bluetooth is [unavailable](https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth#browser_compatibility). - */ -internal fun bluetoothOrThrow(): Bluetooth { - val navigator = try { - window.navigator - } catch (e: ReferenceError) { - // ReferenceError: window is not defined - throw IllegalStateException("Bluetooth unavailable", e) - } - val bluetooth = navigator.bluetooth - if (bluetooth === undefined) { - error("Bluetooth unavailable") - } - return bluetooth -} diff --git a/kable-core/src/jsMain/kotlin/bluetooth/WatchingAdvertisementsSupport.kt b/kable-core/src/jsMain/kotlin/bluetooth/WatchingAdvertisementsSupport.kt deleted file mode 100644 index 286dac905..000000000 --- a/kable-core/src/jsMain/kotlin/bluetooth/WatchingAdvertisementsSupport.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.juul.kable.bluetooth - -internal val canWatchAdvertisements by lazy { - js("BluetoothDevice.prototype.watchAdvertisements") != null -} - -internal val canUnwatchAdvertisements by lazy { - js("BluetoothDevice.prototype.unwatchAdvertisements") != null -} - -internal val isWatchingAdvertisementsSupported = canWatchAdvertisements && canUnwatchAdvertisements diff --git a/kable-core/src/jsMain/kotlin/external/Navigator.kt b/kable-core/src/jsMain/kotlin/external/Navigator.kt deleted file mode 100644 index 94ed13160..000000000 --- a/kable-core/src/jsMain/kotlin/external/Navigator.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.juul.kable.external - -import org.w3c.dom.Navigator - -/** Reference to [Bluetooth] instance or [undefined] if bluetooth is unavailable. */ -internal val Navigator.bluetooth: Bluetooth - get() = asDynamic().bluetooth.unsafeCast() diff --git a/kable-core/src/jsMain/kotlin/interop/Await.js.kt b/kable-core/src/jsMain/kotlin/interop/Await.js.kt new file mode 100644 index 000000000..c3fbce6aa --- /dev/null +++ b/kable-core/src/jsMain/kotlin/interop/Await.js.kt @@ -0,0 +1,6 @@ +package com.juul.kable.interop + +import kotlin.js.Promise +import kotlinx.coroutines.await as kotlinxAwait + +internal actual suspend fun Promise.await(): T = kotlinxAwait() diff --git a/kable-core/src/jsMain/kotlin/logs/SystemLogEngine.kt b/kable-core/src/jsMain/kotlin/logs/SystemLogEngine.kt deleted file mode 100644 index 0019147f3..000000000 --- a/kable-core/src/jsMain/kotlin/logs/SystemLogEngine.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.juul.kable.logs - -public actual object SystemLogEngine : LogEngine { - - actual override fun verbose(throwable: Throwable?, tag: String, message: String) { - debug(throwable, tag, message) - } - - actual override fun debug(throwable: Throwable?, tag: String, message: String) { - if (throwable == null) { - console.asDynamic().debug("[%s] %s", tag, message) - } else { - console.asDynamic().debug("[%s] %s\n%o", tag, message, throwable) - } - } - - actual override fun info(throwable: Throwable?, tag: String, message: String) { - if (throwable == null) { - console.info("[%s] %s", tag, message) - } else { - console.info("[%s] %s\n%o", tag, message, throwable) - } - } - - actual override fun warn(throwable: Throwable?, tag: String, message: String) { - if (throwable == null) { - console.warn("[%s] %s", tag, message) - } else { - console.warn("[%s] %s\n%o", tag, message, throwable) - } - } - - actual override fun error(throwable: Throwable?, tag: String, message: String) { - if (throwable == null) { - console.error("[%s] %s", tag, message) - } else { - console.error("[%s] %s\n%o", tag, message, throwable) - } - } - - actual override fun assert(throwable: Throwable?, tag: String, message: String) { - if (throwable == null) { - console.asDynamic().assert(false, "[%s] %s", tag, message) - } else { - console.asDynamic().assert(false, "[%s] %s\n%o", tag, message, throwable) - } - } -} diff --git a/kable-core/src/jsTest/kotlin/Environment.kt b/kable-core/src/jsTest/kotlin/Environment.kt deleted file mode 100644 index 938859d58..000000000 --- a/kable-core/src/jsTest/kotlin/Environment.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.juul.kable - -val isBrowser: Boolean - get() = js("typeof window !== 'undefined'") - .unsafeCast() - -val isNode: Boolean - get() = js("typeof process !== 'undefined' && process.versions && process.versions.node") - .unsafeCast() diff --git a/kable-core/src/wasmJsMain/kotlin/interop/Await.wasmJs.kt b/kable-core/src/wasmJsMain/kotlin/interop/Await.wasmJs.kt new file mode 100644 index 000000000..c3fbce6aa --- /dev/null +++ b/kable-core/src/wasmJsMain/kotlin/interop/Await.wasmJs.kt @@ -0,0 +1,6 @@ +package com.juul.kable.interop + +import kotlin.js.Promise +import kotlinx.coroutines.await as kotlinxAwait + +internal actual suspend fun Promise.await(): T = kotlinxAwait() diff --git a/kable-core/src/webMain/kotlin/Bluetooth.kt b/kable-core/src/webMain/kotlin/Bluetooth.kt new file mode 100644 index 000000000..1b26c3157 --- /dev/null +++ b/kable-core/src/webMain/kotlin/Bluetooth.kt @@ -0,0 +1,27 @@ +package com.juul.kable + +import com.juul.kable.external.Bluetooth +import com.juul.kable.external.getBluetooth +import web.navigator.Navigator +import kotlin.js.js +import kotlin.js.undefined + +private val navigator: Navigator = + js("window.navigator") + +/** + * @return [Bluetooth] object or `null` if bluetooth is [unavailable](https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth#browser_compatibility). + */ +internal fun bluetoothOrNull(): Bluetooth? = + getBluetooth(navigator).takeIf { it !== undefined } + +/** + * @throws IllegalStateException If bluetooth is [unavailable](https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth#browser_compatibility). + */ +internal fun bluetoothOrThrow(): Bluetooth { + val bluetooth = getBluetooth(navigator) + if (bluetooth === undefined) { + error("Bluetooth unavailable") + } + return bluetooth +} diff --git a/kable-core/src/jsMain/kotlin/BluetoothAdvertisingEventWebBluetoothAdvertisement.kt b/kable-core/src/webMain/kotlin/BluetoothAdvertisingEventWebBluetoothAdvertisement.kt similarity index 73% rename from kable-core/src/jsMain/kotlin/BluetoothAdvertisingEventWebBluetoothAdvertisement.kt rename to kable-core/src/webMain/kotlin/BluetoothAdvertisingEventWebBluetoothAdvertisement.kt index b48ebae7b..f32bd93ae 100644 --- a/kable-core/src/jsMain/kotlin/BluetoothAdvertisingEventWebBluetoothAdvertisement.kt +++ b/kable-core/src/webMain/kotlin/BluetoothAdvertisingEventWebBluetoothAdvertisement.kt @@ -2,8 +2,14 @@ package com.juul.kable import com.juul.kable.external.BluetoothAdvertisingEvent import com.juul.kable.external.BluetoothDevice -import com.juul.kable.external.iterable +import js.array.component1 +import js.array.component2 +import js.iterable.iterator import org.khronos.webgl.DataView +import kotlin.js.toInt +import kotlin.js.toJsNumber +import kotlin.js.toJsString +import kotlin.js.toList import kotlin.uuid.Uuid internal class BluetoothAdvertisingEventWebBluetoothAdvertisement( @@ -32,7 +38,7 @@ internal class BluetoothAdvertisingEventWebBluetoothAdvertisement( get() = advertisement.txPower override val uuids: List - get() = advertisement.uuids.map { it.toUuid() } + get() = advertisement.uuids.toList().map { it.toString().toUuid() } override fun serviceData(uuid: Uuid): ByteArray? = serviceDataAsDataView(uuid)?.buffer?.toByteArray() @@ -41,22 +47,19 @@ internal class BluetoothAdvertisingEventWebBluetoothAdvertisement( manufacturerDataAsDataView(companyIdentifierCode)?.buffer?.toByteArray() override fun serviceDataAsDataView(uuid: Uuid): DataView? = - advertisement.serviceData.asDynamic().get(uuid.toString()) as? DataView + advertisement.serviceData.get(uuid.toString().toJsString()) override fun manufacturerDataAsDataView(companyIdentifierCode: Int): DataView? = - advertisement.manufacturerData.asDynamic().get(companyIdentifierCode.toString()) as? DataView + advertisement.manufacturerData.get(companyIdentifierCode.toJsNumber()) override val manufacturerData: ManufacturerData? - get() = advertisement.manufacturerData.entries().iterable().firstOrNull()?.let { entry -> - ManufacturerData( - entry[0] as Int, - (entry[1] as DataView).buffer.toByteArray(), - ) + get() = Iterable { advertisement.manufacturerData.entries().iterator() }.firstOrNull()?.let { (key, value) -> + ManufacturerData(key.toInt(), value.buffer.toByteArray()) } override fun equals(other: Any?): Boolean { if (this === other) return true - if (other == null || this::class.js != other::class.js) return false + if (other == null || this::class != other::class) return false other as BluetoothAdvertisingEventWebBluetoothAdvertisement return advertisement == other.advertisement } diff --git a/kable-core/src/jsMain/kotlin/BluetoothAvailability.kt b/kable-core/src/webMain/kotlin/BluetoothAvailability.kt similarity index 83% rename from kable-core/src/jsMain/kotlin/BluetoothAvailability.kt rename to kable-core/src/webMain/kotlin/BluetoothAvailability.kt index 2cd42f799..9f5808e35 100644 --- a/kable-core/src/jsMain/kotlin/BluetoothAvailability.kt +++ b/kable-core/src/webMain/kotlin/BluetoothAvailability.kt @@ -4,13 +4,15 @@ import com.juul.kable.Bluetooth.Availability.Available import com.juul.kable.Bluetooth.Availability.Unavailable import com.juul.kable.Reason.BluetoothUndefined import com.juul.kable.external.BluetoothAvailabilityChanged -import kotlinx.coroutines.await +import com.juul.kable.interop.await import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.onStart -import org.w3c.dom.events.Event +import web.events.EventType +import web.events.addEventListener +import web.events.removeEventListener @Deprecated( message = "`Bluetooth.availability` has inconsistent behavior across platforms. " + @@ -27,14 +29,14 @@ public actual enum class Reason { BluetoothUndefined, } -private const val AVAILABILITY_CHANGED = "availabilitychanged" +private val AVAILABILITY_CHANGED = EventType("availabilitychanged") internal actual val bluetoothAvailability: Flow = bluetoothOrNull()?.let { bluetooth -> callbackFlow { // https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth/onavailabilitychanged - val listener: (Event) -> Unit = { event -> - val isAvailable = event.unsafeCast().value + val listener: (BluetoothAvailabilityChanged) -> Unit = { event -> + val isAvailable = event.value trySend(if (isAvailable) Available else Unavailable(reason = null)) } diff --git a/kable-core/src/jsMain/kotlin/BluetoothDeviceWebBluetoothPeripheral.kt b/kable-core/src/webMain/kotlin/BluetoothDeviceWebBluetoothPeripheral.kt similarity index 92% rename from kable-core/src/jsMain/kotlin/BluetoothDeviceWebBluetoothPeripheral.kt rename to kable-core/src/webMain/kotlin/BluetoothDeviceWebBluetoothPeripheral.kt index 0fd232998..42e38e716 100644 --- a/kable-core/src/jsMain/kotlin/BluetoothDeviceWebBluetoothPeripheral.kt +++ b/kable-core/src/webMain/kotlin/BluetoothDeviceWebBluetoothPeripheral.kt @@ -9,14 +9,15 @@ import com.juul.kable.external.BluetoothAdvertisingEvent import com.juul.kable.external.BluetoothDevice import com.juul.kable.external.BluetoothRemoteGATTServer import com.juul.kable.external.string +import com.juul.kable.interop.await import com.juul.kable.logs.Logger import com.juul.kable.logs.Logging import com.juul.kable.logs.Logging.DataProcessor.Operation import com.juul.kable.logs.detail +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart.UNDISPATCHED import kotlinx.coroutines.async -import kotlinx.coroutines.await import kotlinx.coroutines.cancel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow @@ -26,13 +27,15 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.suspendCancellableCoroutine import org.khronos.webgl.DataView -import org.w3c.dom.events.EventListener -import kotlin.coroutines.cancellation.CancellationException +import org.khronos.webgl.toInt8Array +import web.events.EventType +import web.events.addEventListener +import web.events.removeEventListener import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.time.Duration -private const val ADVERTISEMENT_RECEIVED = "advertisementreceived" +private val ADVERTISEMENT_RECEIVED = EventType("advertisementreceived") private const val DEFAULT_ATT_MTU = 23 private const val ATT_MTU_HEADER_SIZE = 3 @@ -113,9 +116,8 @@ internal class BluetoothDeviceWebBluetoothPeripheral( connectAction.awaitConnect() override suspend fun disconnect() { - connectAction.cancelAndJoin( - CancellationException(NotConnectedException("Disconnect requested")), - ) + val cause = NotConnectedException("Disconnect requested") + connectAction.cancelAndJoin(CancellationException(cause.toString(), cause)) } override suspend fun maximumWriteValueLengthForType(writeType: WriteType): Int = @@ -161,8 +163,8 @@ internal class BluetoothDeviceWebBluetoothPeripheral( } private suspend fun receiveRssiEvent() = suspendCancellableCoroutine { continuation -> - val listener = EventListener { event -> - val rssi = event.unsafeCast().rssi + val listener = { event: BluetoothAdvertisingEvent -> + val rssi = event.rssi if (rssi != null) { continuation.resume(rssi) } else { @@ -174,14 +176,14 @@ internal class BluetoothDeviceWebBluetoothPeripheral( logger.verbose { message = "addEventListener" - detail("event", ADVERTISEMENT_RECEIVED) + detail("event", ADVERTISEMENT_RECEIVED.toString()) } bluetoothDevice.addEventListener(ADVERTISEMENT_RECEIVED, listener) continuation.invokeOnCancellation { logger.verbose { message = "removeEventListener" - detail("event", ADVERTISEMENT_RECEIVED) + detail("event", ADVERTISEMENT_RECEIVED.toString()) } bluetoothDevice.removeEventListener(ADVERTISEMENT_RECEIVED, listener) } @@ -202,8 +204,8 @@ internal class BluetoothDeviceWebBluetoothPeripheral( val platformCharacteristic = servicesOrThrow().obtain(characteristic, writeType.properties) connectionOrThrow().execute { when (writeType) { - WithResponse -> platformCharacteristic.writeValueWithResponse(data) - WithoutResponse -> platformCharacteristic.writeValueWithoutResponse(data) + WithResponse -> platformCharacteristic.writeValueWithResponse(data.toInt8Array()) + WithoutResponse -> platformCharacteristic.writeValueWithoutResponse(data.toInt8Array()) } } } @@ -241,7 +243,7 @@ internal class BluetoothDeviceWebBluetoothPeripheral( val platformDescriptor = servicesOrThrow().obtain(descriptor) connectionOrThrow().execute { - platformDescriptor.writeValue(data) + platformDescriptor.writeValue(data.toInt8Array()) } } diff --git a/kable-core/src/jsMain/kotlin/BluetoothLEScanOptions.kt b/kable-core/src/webMain/kotlin/BluetoothLEScanOptions.kt similarity index 82% rename from kable-core/src/jsMain/kotlin/BluetoothLEScanOptions.kt rename to kable-core/src/webMain/kotlin/BluetoothLEScanOptions.kt index 17754b3e1..8ac30db6e 100644 --- a/kable-core/src/jsMain/kotlin/BluetoothLEScanOptions.kt +++ b/kable-core/src/webMain/kotlin/BluetoothLEScanOptions.kt @@ -5,6 +5,9 @@ import com.juul.kable.external.BluetoothLEScanOptions import com.juul.kable.external.BluetoothManufacturerDataFilterInit import com.juul.kable.external.BluetoothServiceDataFilterInit import js.objects.unsafeJso +import org.khronos.webgl.toInt8Array +import kotlin.js.JsArray +import kotlin.js.toJsArray import kotlin.uuid.Uuid /** Convert list of public API type to Web Bluetooth (JavaScript) type. */ @@ -16,9 +19,9 @@ internal fun List.toBluetoothLEScanOptions(): BluetoothLEScanOp } } -internal fun List.toBluetoothLEScanFilterInit(): Array = +internal fun List.toBluetoothLEScanFilterInit(): JsArray = map(FilterPredicate::toBluetoothLEScanFilterInit) - .toTypedArray() + .toJsArray() private fun FilterPredicate.toBluetoothLEScanFilterInit(): BluetoothLEScanFilterInit = unsafeJso { filters @@ -27,7 +30,7 @@ private fun FilterPredicate.toBluetoothLEScanFilterInit(): BluetoothLEScanFilter ?.map(Filter.Service::uuid) ?.map(Uuid::toBluetoothServiceUUID) ?.toTypedArray() - ?.let { services = it } + ?.let { services = it.toJsArray() } filters .filterIsInstance() .firstOrNull() @@ -41,23 +44,23 @@ private fun FilterPredicate.toBluetoothLEScanFilterInit(): BluetoothLEScanFilter .takeIf(Collection::isNotEmpty) ?.map(::toBluetoothManufacturerDataFilterInit) ?.toTypedArray() - ?.let { manufacturerData = it } + ?.let { manufacturerData = it.toJsArray() } filters .filterIsInstance() .takeIf(Collection::isNotEmpty) ?.map(::toBluetoothServiceDataFilterInit) ?.toTypedArray() - ?.let { serviceData = it } + ?.let { serviceData = it.toJsArray() } } private fun toBluetoothManufacturerDataFilterInit(filter: Filter.ManufacturerData) = unsafeJso { companyIdentifier = filter.id if (filter.data != null) { - dataPrefix = filter.data + dataPrefix = filter.data.toInt8Array() } if (filter.dataMask != null) { - mask = filter.dataMask + mask = filter.dataMask.toInt8Array() } } @@ -65,9 +68,9 @@ private fun toBluetoothServiceDataFilterInit(filter: Filter.ServiceData) = unsafeJso { service = filter.uuid.toBluetoothServiceUUID() if (filter.data != null) { - dataPrefix = filter.data + dataPrefix = filter.data.toInt8Array() } if (filter.dataMask != null) { - mask = filter.dataMask + mask = filter.dataMask.toInt8Array() } } diff --git a/kable-core/src/jsMain/kotlin/BluetoothWebBluetoothScanner.kt b/kable-core/src/webMain/kotlin/BluetoothWebBluetoothScanner.kt similarity index 80% rename from kable-core/src/jsMain/kotlin/BluetoothWebBluetoothScanner.kt rename to kable-core/src/webMain/kotlin/BluetoothWebBluetoothScanner.kt index 427d0c1c2..549ac5c84 100644 --- a/kable-core/src/jsMain/kotlin/BluetoothWebBluetoothScanner.kt +++ b/kable-core/src/webMain/kotlin/BluetoothWebBluetoothScanner.kt @@ -1,21 +1,25 @@ package com.juul.kable import com.juul.kable.external.BluetoothAdvertisingEvent +import com.juul.kable.interop.await import com.juul.kable.logs.Logger import com.juul.kable.logs.Logging -import js.errors.JsError import js.errors.TypeError -import kotlinx.coroutines.await +import js.json.stringify import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.getOrElse import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow -import org.w3c.dom.events.Event import web.errors.DOMException import web.errors.SecurityError +import web.events.EventType +import web.events.addEventListener +import web.events.removeEventListener +import kotlin.js.JsException +import kotlin.js.thrownValue -private const val ADVERTISEMENT_RECEIVED_EVENT = "advertisementreceived" +private val ADVERTISEMENT_RECEIVED_EVENT = EventType("advertisementreceived") /** * Only available on Chrome 79+ with "Experimental Web Platform features" enabled via: @@ -51,19 +55,19 @@ internal class BluetoothWebBluetoothScanner( val requestLEScan = try { bluetooth.requestLEScan(options) - } catch (e: TypeError) { - // Example failure when executing `requestLEScan(..)` with Chrome's "Experimental Web Platform features" turned off: - // > TypeError: navigator.bluetooth.requestLEScan is not a function - throw IllegalStateException("Scanning not supported", e) - } catch (e: JsError) { + } catch (e: JsException) { + if (e.thrownValue is TypeError) { + // Example failure when executing `requestLEScan(..)` with Chrome's "Experimental Web Platform features" turned off: + // > TypeError: navigator.bluetooth.requestLEScan is not a function + throw IllegalStateException("Scanning not supported", e) + } ensureActive() throw InternalError("Failed to request scan", e) } logger.verbose { message = "Adding scan listener" } - val listener: (Event) -> Unit = { - val event = it.unsafeCast() - val advertisement = BluetoothAdvertisingEventWebBluetoothAdvertisement(event) + val listener: (BluetoothAdvertisingEvent) -> Unit = { + val advertisement = BluetoothAdvertisingEventWebBluetoothAdvertisement(it) trySend(advertisement).getOrElse { logger.warn { message = "Unable to deliver advertisement event due to failure in flow or premature closing." } } @@ -73,14 +77,15 @@ internal class BluetoothWebBluetoothScanner( logger.info { message = "Starting scan" } val scan = try { requestLEScan.await() - } catch (e: JsError) { + } catch (e: JsException) { logger.verbose { message = "Removing scan listener" } bluetooth.removeEventListener(ADVERTISEMENT_RECEIVED_EVENT, listener) ensureActive() // The Web Bluetooth API can only be used in a secure context. // https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#security_considerations - if (e is DOMException && e.name == DOMException.SecurityError) { + val thrownValue = e.thrownValue + if (thrownValue is DOMException && thrownValue.name == DOMException.SecurityError) { throw IllegalStateException("Operation is not permitted in this context due to security concerns", e) } @@ -92,7 +97,7 @@ internal class BluetoothWebBluetoothScanner( // that we can fix any issues). logger.error { detail("filters", filters.toString()) - detail("options", JSON.stringify(options)) + detail("options", stringify(options)) message = e.toString() } throw InternalError("Failed to start scan", e) diff --git a/kable-core/src/jsMain/kotlin/Bytes.kt b/kable-core/src/webMain/kotlin/Bytes.kt similarity index 74% rename from kable-core/src/jsMain/kotlin/Bytes.kt rename to kable-core/src/webMain/kotlin/Bytes.kt index 83766b522..4d84fd2ed 100644 --- a/kable-core/src/jsMain/kotlin/Bytes.kt +++ b/kable-core/src/webMain/kotlin/Bytes.kt @@ -2,5 +2,6 @@ package com.juul.kable import org.khronos.webgl.ArrayBuffer import org.khronos.webgl.Int8Array +import org.khronos.webgl.toByteArray -internal fun ArrayBuffer.toByteArray(): ByteArray = Int8Array(this).unsafeCast() +internal fun ArrayBuffer.toByteArray(): ByteArray = Int8Array(this).toByteArray() diff --git a/kable-core/src/jsMain/kotlin/Connection.kt b/kable-core/src/webMain/kotlin/Connection.kt similarity index 90% rename from kable-core/src/jsMain/kotlin/Connection.kt rename to kable-core/src/webMain/kotlin/Connection.kt index 58c008c12..f7b51176e 100644 --- a/kable-core/src/jsMain/kotlin/Connection.kt +++ b/kable-core/src/webMain/kotlin/Connection.kt @@ -6,18 +6,17 @@ import com.juul.kable.coroutines.childSupervisor import com.juul.kable.external.BluetoothDevice import com.juul.kable.external.BluetoothRemoteGATTCharacteristic import com.juul.kable.external.BluetoothRemoteGATTServer +import com.juul.kable.interop.await import com.juul.kable.logs.Logger import com.juul.kable.logs.Logging import com.juul.kable.logs.Logging.DataProcessor.Operation import com.juul.kable.logs.detail -import js.errors.JsError import kotlinx.coroutines.CompletableJob import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart.ATOMIC import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.await import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.MutableSharedFlow @@ -32,17 +31,25 @@ import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import kotlinx.io.IOException import org.khronos.webgl.DataView -import org.w3c.dom.events.Event import web.errors.DOMException +import web.events.Event +import web.events.EventType +import web.events.addEventListener +import web.events.removeEventListener import kotlin.coroutines.CoroutineContext import kotlin.coroutines.coroutineContext +import kotlin.js.JsAny +import kotlin.js.JsException import kotlin.js.Promise +import kotlin.js.thrownValue +import kotlin.js.toList +import kotlin.js.unsafeCast import kotlin.time.Duration private typealias ObservationListener = (Event) -> Unit -private const val GATT_SERVER_DISCONNECTED = "gattserverdisconnected" -private const val CHARACTERISTIC_VALUE_CHANGED = "characteristicvaluechanged" +private val GATT_SERVER_DISCONNECTED = EventType("gattserverdisconnected") +private val CHARACTERISTIC_VALUE_CHANGED = EventType("characteristicvaluechanged") internal class Connection( parentContext: CoroutineContext, @@ -68,7 +75,7 @@ internal class Connection( private val logger = Logger(logging, tag = "Kable/Connection", identifier = bluetoothDevice.id) private val disconnectedListener: (Event) -> Unit = { - logger.debug { message = GATT_SERVER_DISCONNECTED } + logger.debug { message = GATT_SERVER_DISCONNECTED.toString() } state.value = Disconnected() } @@ -100,6 +107,7 @@ internal class Connection( logger.verbose { message = "Discovering services" } state.value = Connecting.Services discoveredServices.value = execute(BluetoothRemoteGATTServer::getPrimaryServices) + .toList() .map { it.toDiscoveredService(logger) } } @@ -121,18 +129,18 @@ internal class Connection( logger.verbose { message = "addEventListener" detail(characteristic) - detail("event", CHARACTERISTIC_VALUE_CHANGED) + detail("event", CHARACTERISTIC_VALUE_CHANGED.toString()) } addEventListener(CHARACTERISTIC_VALUE_CHANGED, listener) try { execute { startNotifications() } - } catch (e: JsError) { + } catch (e: JsException) { removeCharacteristicValueChangedListener(listener) observationListeners.remove(platformCharacteristic) coroutineContext.ensureActive() - throw when (e) { + throw when (e.thrownValue) { is DOMException -> IOException("Failed to start notification", e) else -> InternalError("Unexpected start notification failure", e) } @@ -151,9 +159,9 @@ internal class Connection( platformCharacteristic.apply { try { execute { stopNotifications() } - } catch (e: JsError) { + } catch (e: JsException) { coroutineContext.ensureActive() - when (e) { + when (e.thrownValue) { // DOMException: Failed to execute 'stopNotifications' on 'BluetoothRemoteGATTCharacteristic': // Characteristic with UUID [...] is no longer valid. Remember to retrieve the characteristic // again after reconnecting. @@ -174,7 +182,7 @@ internal class Connection( private val guard = Mutex() - suspend fun execute( + suspend fun execute( action: BluetoothRemoteGATTServer.() -> Promise, ): T = guard.withLock { unwrapCancellationExceptions { @@ -233,10 +241,10 @@ internal class Connection( } private fun PlatformCharacteristic.createObservationListener(): ObservationListener = { event -> - val target = event.target.unsafeCast() - val data = target.value!! + val target = event.target?.unsafeCast() + val data = target?.value!! logger.debug { - message = CHARACTERISTIC_VALUE_CHANGED + message = CHARACTERISTIC_VALUE_CHANGED.toString() detail(this@createObservationListener) detail(data, Operation.Change) } @@ -270,7 +278,7 @@ internal class Connection( logger.verbose { message = "removeEventListener" detail(this@removeCharacteristicValueChangedListener) - detail("event", CHARACTERISTIC_VALUE_CHANGED) + detail("event", CHARACTERISTIC_VALUE_CHANGED.toString()) } removeEventListener(CHARACTERISTIC_VALUE_CHANGED, listener) } diff --git a/kable-core/src/jsMain/kotlin/FilterSet.kt b/kable-core/src/webMain/kotlin/FilterSet.kt similarity index 100% rename from kable-core/src/jsMain/kotlin/FilterSet.kt rename to kable-core/src/webMain/kotlin/FilterSet.kt diff --git a/kable-core/src/jsMain/kotlin/Identifier.kt b/kable-core/src/webMain/kotlin/Identifier.kt similarity index 100% rename from kable-core/src/jsMain/kotlin/Identifier.kt rename to kable-core/src/webMain/kotlin/Identifier.kt diff --git a/kable-core/src/jsMain/kotlin/JsPeripheral.kt b/kable-core/src/webMain/kotlin/JsPeripheral.kt similarity index 100% rename from kable-core/src/jsMain/kotlin/JsPeripheral.kt rename to kable-core/src/webMain/kotlin/JsPeripheral.kt diff --git a/kable-core/src/jsMain/kotlin/Observations.kt b/kable-core/src/webMain/kotlin/Observations.kt similarity index 100% rename from kable-core/src/jsMain/kotlin/Observations.kt rename to kable-core/src/webMain/kotlin/Observations.kt diff --git a/kable-core/src/jsMain/kotlin/Options.deprecated.kt b/kable-core/src/webMain/kotlin/Options.deprecated.kt similarity index 100% rename from kable-core/src/jsMain/kotlin/Options.deprecated.kt rename to kable-core/src/webMain/kotlin/Options.deprecated.kt diff --git a/kable-core/src/jsMain/kotlin/Options.kt b/kable-core/src/webMain/kotlin/Options.kt similarity index 92% rename from kable-core/src/jsMain/kotlin/Options.kt rename to kable-core/src/webMain/kotlin/Options.kt index 9ef55da3b..2dbc3cf39 100644 --- a/kable-core/src/jsMain/kotlin/Options.kt +++ b/kable-core/src/webMain/kotlin/Options.kt @@ -1,6 +1,8 @@ package com.juul.kable import com.juul.kable.external.RequestDeviceOptions +import com.juul.kable.interop.isEmpty +import com.juul.kable.interop.isNotEmpty import js.objects.unsafeJso import kotlin.uuid.Uuid diff --git a/kable-core/src/jsMain/kotlin/OptionsBuilder.kt b/kable-core/src/webMain/kotlin/OptionsBuilder.kt similarity index 100% rename from kable-core/src/jsMain/kotlin/OptionsBuilder.kt rename to kable-core/src/webMain/kotlin/OptionsBuilder.kt diff --git a/kable-core/src/jsMain/kotlin/Peripheral.deprecated.kt b/kable-core/src/webMain/kotlin/Peripheral.deprecated.kt similarity index 100% rename from kable-core/src/jsMain/kotlin/Peripheral.deprecated.kt rename to kable-core/src/webMain/kotlin/Peripheral.deprecated.kt diff --git a/kable-core/src/jsMain/kotlin/Peripheral.kt b/kable-core/src/webMain/kotlin/Peripheral.kt similarity index 84% rename from kable-core/src/jsMain/kotlin/Peripheral.kt rename to kable-core/src/webMain/kotlin/Peripheral.kt index cd7ce9a39..b8c958230 100644 --- a/kable-core/src/jsMain/kotlin/Peripheral.kt +++ b/kable-core/src/webMain/kotlin/Peripheral.kt @@ -1,12 +1,14 @@ package com.juul.kable import com.juul.kable.external.BluetoothDevice -import js.errors.JsError -import kotlinx.coroutines.await +import com.juul.kable.interop.await import kotlinx.coroutines.ensureActive import web.errors.DOMException import web.errors.SecurityError import kotlin.coroutines.coroutineContext +import kotlin.js.JsException +import kotlin.js.thrownValue +import kotlin.js.toList public actual fun Peripheral( advertisement: Advertisement, @@ -23,13 +25,14 @@ public suspend fun Peripheral( ): WebBluetoothPeripheral? { val bluetooth = bluetoothOrThrow() val devices = try { - bluetooth.getDevices().await() - } catch (e: JsError) { + bluetooth.getDevices().await().toList() + } catch (e: JsException) { coroutineContext.ensureActive() + val thrownValue = e.thrownValue throw when { // The Web Bluetooth API can only be used in a secure context. // https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#security_considerations - e is DOMException && e.name == DOMException.SecurityError -> + thrownValue is DOMException && thrownValue.name == DOMException.SecurityError -> IllegalStateException("Operation is not permitted in this context due to security concerns", e) else -> InternalError("Failed to invoke getDevices request", e) diff --git a/kable-core/src/jsMain/kotlin/PeripheralBuilder.kt b/kable-core/src/webMain/kotlin/PeripheralBuilder.kt similarity index 100% rename from kable-core/src/jsMain/kotlin/PeripheralBuilder.kt rename to kable-core/src/webMain/kotlin/PeripheralBuilder.kt diff --git a/kable-core/src/jsMain/kotlin/PlatformAdvertisement.kt b/kable-core/src/webMain/kotlin/PlatformAdvertisement.kt similarity index 100% rename from kable-core/src/jsMain/kotlin/PlatformAdvertisement.kt rename to kable-core/src/webMain/kotlin/PlatformAdvertisement.kt diff --git a/kable-core/src/jsMain/kotlin/Profile.kt b/kable-core/src/webMain/kotlin/Profile.kt similarity index 97% rename from kable-core/src/jsMain/kotlin/Profile.kt rename to kable-core/src/webMain/kotlin/Profile.kt index bb40c50d6..3d45d64e7 100644 --- a/kable-core/src/jsMain/kotlin/Profile.kt +++ b/kable-core/src/webMain/kotlin/Profile.kt @@ -5,9 +5,10 @@ import com.juul.kable.external.BluetoothCharacteristicProperties import com.juul.kable.external.BluetoothRemoteGATTCharacteristic import com.juul.kable.external.BluetoothRemoteGATTDescriptor import com.juul.kable.external.BluetoothRemoteGATTService +import com.juul.kable.interop.await import com.juul.kable.logs.Logger import com.juul.kable.logs.detail -import kotlinx.coroutines.await +import kotlin.js.toList @Suppress("ACTUAL_WITHOUT_EXPECT") // https://youtrack.jetbrains.com/issue/KT-37316 internal actual typealias PlatformService = BluetoothRemoteGATTService @@ -81,6 +82,7 @@ internal actual class PlatformDiscoveredDescriptor internal constructor( internal suspend fun PlatformService.toDiscoveredService(logger: Logger): PlatformDiscoveredService { val characteristics = getCharacteristics() .await() + .toList() .map { characteristic -> characteristic.toDiscoveredCharacteristic(logger) } @@ -101,7 +103,8 @@ private suspend fun BluetoothRemoteGATTCharacteristic.toDiscoveredCharacteristic detail(this@toDiscoveredCharacteristic) } } - .getOrDefault(emptyArray()) + .map { it.toList() } + .getOrDefault(emptyList()) val platformDescriptors = descriptors.map(::PlatformDiscoveredDescriptor) return PlatformDiscoveredCharacteristic( diff --git a/kable-core/src/jsMain/kotlin/RequestPeripheral.kt b/kable-core/src/webMain/kotlin/RequestPeripheral.kt similarity index 84% rename from kable-core/src/jsMain/kotlin/RequestPeripheral.kt rename to kable-core/src/webMain/kotlin/RequestPeripheral.kt index 8d61eb94a..57c94a216 100644 --- a/kable-core/src/jsMain/kotlin/RequestPeripheral.kt +++ b/kable-core/src/webMain/kotlin/RequestPeripheral.kt @@ -1,14 +1,16 @@ package com.juul.kable +import com.juul.kable.interop.await import com.juul.kable.logs.Logger -import js.errors.JsError import js.errors.TypeError -import kotlinx.coroutines.await +import js.json.stringify import kotlinx.coroutines.ensureActive import web.errors.DOMException import web.errors.NotFoundError import web.errors.SecurityError import kotlin.coroutines.coroutineContext +import kotlin.js.JsException +import kotlin.js.thrownValue /** * Obtains a nearby [Peripheral] via device picker. Returns `null` if dialog is cancelled (e.g. user @@ -32,9 +34,10 @@ public suspend fun requestPeripheral( val requestDeviceOptions = options.toRequestDeviceOptions() val requestDevice = try { bluetooth.requestDevice(requestDeviceOptions) - } catch (e: JsError) { + } catch (e: JsException) { + val thrownValue = e.thrownValue coroutineContext.ensureActive() - throw when (e) { + throw when (thrownValue) { is TypeError -> IllegalStateException("Requesting a device is not supported", e) else -> InternalError("Failed to invoke device request", e) } @@ -46,18 +49,19 @@ public suspend fun requestPeripheral( return try { requestDevice.await() - } catch (e: JsError) { + } catch (e: JsException) { coroutineContext.ensureActive() + val thrownValue = e.thrownValue when { // User cancelled picker dialog by either clicking outside dialog, or clicking cancel button. - e is DOMException && e.name == DOMException.NotFoundError -> null + thrownValue is DOMException && thrownValue.name == DOMException.NotFoundError -> null // The Web Bluetooth API can only be used in a secure context. // https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#security_considerations - e is DOMException && e.name == DOMException.SecurityError -> + thrownValue is DOMException && thrownValue.name == DOMException.SecurityError -> throw IllegalStateException("Operation is not permitted in this context due to security concerns", e) - e is TypeError -> { + thrownValue is TypeError -> { // Example failure when executing `requestDevice(unsafeJso {})`: // > TypeError: Failed to execute 'requestDevice' on 'Bluetooth': Either 'filters' // > should be present or 'acceptAllAdvertisements' should be true, but not both. @@ -67,7 +71,7 @@ public suspend fun requestPeripheral( val logger = Logger(builder.logging, tag = "Kable/requestDevice", identifier = null) logger.error { detail("options", options.toString()) - detail("processed", JSON.stringify(requestDeviceOptions)) + detail("processed", stringify(requestDeviceOptions)) message = e.toString() } throw InternalError("Type error when requesting device", e) diff --git a/kable-core/src/jsMain/kotlin/ScannerBuilder.kt b/kable-core/src/webMain/kotlin/ScannerBuilder.kt similarity index 100% rename from kable-core/src/jsMain/kotlin/ScannerBuilder.kt rename to kable-core/src/webMain/kotlin/ScannerBuilder.kt diff --git a/kable-core/src/jsMain/kotlin/Uuid.kt b/kable-core/src/webMain/kotlin/Uuid.kt similarity index 75% rename from kable-core/src/jsMain/kotlin/Uuid.kt rename to kable-core/src/webMain/kotlin/Uuid.kt index e42d5a703..d08c87b5a 100644 --- a/kable-core/src/jsMain/kotlin/Uuid.kt +++ b/kable-core/src/webMain/kotlin/Uuid.kt @@ -2,6 +2,10 @@ package com.juul.kable import com.juul.kable.external.BluetoothServiceUUID import com.juul.kable.external.BluetoothUUID +import kotlin.js.JsArray +import kotlin.js.toJsArray +import kotlin.js.toJsNumber +import kotlin.js.toJsString import kotlin.uuid.Uuid // Number of characters in a 16-bit UUID alias in string hex representation @@ -20,15 +24,15 @@ internal typealias UUID = String internal fun UUID.toUuid(): Uuid = Uuid.parse( when (length) { - UUID_ALIAS_STRING_LENGTH -> BluetoothUUID.canonicalUUID(toInt(16)) + UUID_ALIAS_STRING_LENGTH -> BluetoothUUID.canonicalUUID(toInt(16).toJsNumber()) else -> this }, ) -internal fun List.toBluetoothServiceUUID(): Array = +internal fun List.toBluetoothServiceUUID(): JsArray = map(Uuid::toBluetoothServiceUUID) - .toTypedArray() + .toJsArray() // Note: Web Bluetooth requires that UUIDs be provided as lowercase strings. internal fun Uuid.toBluetoothServiceUUID(): BluetoothServiceUUID = - toString().lowercase() + toString().lowercase().toJsString() diff --git a/kable-core/src/jsMain/kotlin/WebBluetoothAdvertisement.kt b/kable-core/src/webMain/kotlin/WebBluetoothAdvertisement.kt similarity index 100% rename from kable-core/src/jsMain/kotlin/WebBluetoothAdvertisement.kt rename to kable-core/src/webMain/kotlin/WebBluetoothAdvertisement.kt diff --git a/kable-core/src/jsMain/kotlin/WebBluetoothPeripheral.kt b/kable-core/src/webMain/kotlin/WebBluetoothPeripheral.kt similarity index 100% rename from kable-core/src/jsMain/kotlin/WebBluetoothPeripheral.kt rename to kable-core/src/webMain/kotlin/WebBluetoothPeripheral.kt diff --git a/kable-core/src/jsMain/kotlin/WebBluetoothScanner.kt b/kable-core/src/webMain/kotlin/WebBluetoothScanner.kt similarity index 100% rename from kable-core/src/jsMain/kotlin/WebBluetoothScanner.kt rename to kable-core/src/webMain/kotlin/WebBluetoothScanner.kt diff --git a/kable-core/src/jsMain/kotlin/bluetooth/IsSupported.kt b/kable-core/src/webMain/kotlin/bluetooth/IsSupported.kt similarity index 69% rename from kable-core/src/jsMain/kotlin/bluetooth/IsSupported.kt rename to kable-core/src/webMain/kotlin/bluetooth/IsSupported.kt index ee6985f28..a0b26afd0 100644 --- a/kable-core/src/jsMain/kotlin/bluetooth/IsSupported.kt +++ b/kable-core/src/webMain/kotlin/bluetooth/IsSupported.kt @@ -2,21 +2,23 @@ package com.juul.kable.bluetooth import com.juul.kable.InternalError import com.juul.kable.bluetoothOrNull -import js.errors.JsError +import com.juul.kable.interop.await import js.errors.TypeError -import kotlinx.coroutines.await +import kotlin.js.JsException +import kotlin.js.thrownValue internal actual suspend fun isSupported(): Boolean { val bluetooth = bluetoothOrNull() ?: return false val promise = try { bluetooth.getAvailability() - } catch (e: TypeError) { + } catch (e: JsException) { // > TypeError: navigator.bluetooth.getAvailability is not a function - return false + if (e.thrownValue is TypeError) return false + throw e } return try { promise.await() - } catch (e: JsError) { + } catch (e: JsException) { throw InternalError("Failed to get bluetooth availability", e) } } diff --git a/kable-core/src/webMain/kotlin/bluetooth/WatchingAdvertisementsSupport.kt b/kable-core/src/webMain/kotlin/bluetooth/WatchingAdvertisementsSupport.kt new file mode 100644 index 000000000..43a002f99 --- /dev/null +++ b/kable-core/src/webMain/kotlin/bluetooth/WatchingAdvertisementsSupport.kt @@ -0,0 +1,21 @@ +package com.juul.kable.bluetooth + +import kotlin.js.JsAny +import kotlin.js.js +import kotlin.js.undefined + +private val watchAdvertisements: JsAny? = + js("BluetoothDevice.prototype.watchAdvertisements") + +private val unwatchAdvertisements: JsAny? = + js("BluetoothDevice.prototype.unwatchAdvertisements") + +internal val canWatchAdvertisements by lazy { + watchAdvertisements != null && watchAdvertisements != undefined +} + +internal val canUnwatchAdvertisements by lazy { + unwatchAdvertisements != null && unwatchAdvertisements != undefined +} + +internal val isWatchingAdvertisementsSupported = canWatchAdvertisements && canUnwatchAdvertisements diff --git a/kable-core/src/jsMain/kotlin/external/Bluetooth.kt b/kable-core/src/webMain/kotlin/external/Bluetooth.kt similarity index 65% rename from kable-core/src/jsMain/kotlin/external/Bluetooth.kt rename to kable-core/src/webMain/kotlin/external/Bluetooth.kt index e2d260d4e..5f6e4ef1f 100644 --- a/kable-core/src/jsMain/kotlin/external/Bluetooth.kt +++ b/kable-core/src/webMain/kotlin/external/Bluetooth.kt @@ -1,12 +1,14 @@ package com.juul.kable.external -import org.w3c.dom.events.EventTarget +import web.events.EventTarget +import kotlin.js.JsArray +import kotlin.js.JsBoolean import kotlin.js.Promise /** https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth */ internal abstract external class Bluetooth : EventTarget { - fun getAvailability(): Promise + fun getAvailability(): Promise fun requestDevice(options: RequestDeviceOptions): Promise fun requestLEScan(options: BluetoothLEScanOptions): Promise - fun getDevices(): Promise> + fun getDevices(): Promise> } diff --git a/kable-core/src/jsMain/kotlin/external/BluetoothAdvertisingEvent.kt b/kable-core/src/webMain/kotlin/external/BluetoothAdvertisingEvent.kt similarity index 58% rename from kable-core/src/jsMain/kotlin/external/BluetoothAdvertisingEvent.kt rename to kable-core/src/webMain/kotlin/external/BluetoothAdvertisingEvent.kt index fd98448d1..7ba50dce8 100644 --- a/kable-core/src/jsMain/kotlin/external/BluetoothAdvertisingEvent.kt +++ b/kable-core/src/webMain/kotlin/external/BluetoothAdvertisingEvent.kt @@ -1,17 +1,20 @@ package com.juul.kable.external -import org.w3c.dom.events.Event +import js.collections.JsMap +import org.khronos.webgl.DataView +import web.events.Event +import kotlin.js.JsArray +import kotlin.js.JsNumber +import kotlin.js.JsString -internal interface BluetoothManufacturerDataMap { - fun entries(): JsIterator> -} +internal typealias BluetoothManufacturerDataMap = JsMap /** * https://webbluetoothcg.github.io/web-bluetooth/#bluetoothadvertisingevent */ internal abstract external class BluetoothAdvertisingEvent : Event { val device: BluetoothDevice - val uuids: Array + val uuids: JsArray val name: String? val rssi: Int? val txPower: Int? diff --git a/kable-core/src/jsMain/kotlin/external/BluetoothAvailabilityChanged.kt b/kable-core/src/webMain/kotlin/external/BluetoothAvailabilityChanged.kt similarity index 86% rename from kable-core/src/jsMain/kotlin/external/BluetoothAvailabilityChanged.kt rename to kable-core/src/webMain/kotlin/external/BluetoothAvailabilityChanged.kt index eeae30410..2384a5806 100644 --- a/kable-core/src/jsMain/kotlin/external/BluetoothAvailabilityChanged.kt +++ b/kable-core/src/webMain/kotlin/external/BluetoothAvailabilityChanged.kt @@ -1,6 +1,6 @@ package com.juul.kable.external -import org.w3c.dom.events.Event +import web.events.Event /** https://webbluetoothcg.github.io/web-bluetooth/#availability */ internal external class BluetoothAvailabilityChanged : Event { diff --git a/kable-core/src/jsMain/kotlin/external/BluetoothCharacteristicProperties.kt b/kable-core/src/webMain/kotlin/external/BluetoothCharacteristicProperties.kt similarity index 100% rename from kable-core/src/jsMain/kotlin/external/BluetoothCharacteristicProperties.kt rename to kable-core/src/webMain/kotlin/external/BluetoothCharacteristicProperties.kt diff --git a/kable-core/src/jsMain/kotlin/external/BluetoothCharacteristicUUID.kt b/kable-core/src/webMain/kotlin/external/BluetoothCharacteristicUUID.kt similarity index 100% rename from kable-core/src/jsMain/kotlin/external/BluetoothCharacteristicUUID.kt rename to kable-core/src/webMain/kotlin/external/BluetoothCharacteristicUUID.kt diff --git a/kable-core/src/jsMain/kotlin/external/BluetoothDataFilterInit.kt b/kable-core/src/webMain/kotlin/external/BluetoothDataFilterInit.kt similarity index 77% rename from kable-core/src/jsMain/kotlin/external/BluetoothDataFilterInit.kt rename to kable-core/src/webMain/kotlin/external/BluetoothDataFilterInit.kt index bb1b19220..401326f1a 100644 --- a/kable-core/src/jsMain/kotlin/external/BluetoothDataFilterInit.kt +++ b/kable-core/src/webMain/kotlin/external/BluetoothDataFilterInit.kt @@ -1,5 +1,7 @@ package com.juul.kable.external +import kotlin.js.JsAny + /** * ``` * dictionary BluetoothDataFilterInit { @@ -10,7 +12,7 @@ package com.juul.kable.external * * https://webbluetoothcg.github.io/web-bluetooth/#device-discovery */ -internal external interface BluetoothDataFilterInit { +internal external interface BluetoothDataFilterInit : JsAny { var dataPrefix: BufferSource? var mask: BufferSource? } diff --git a/kable-core/src/jsMain/kotlin/external/BluetoothDescriptorUUID.kt b/kable-core/src/webMain/kotlin/external/BluetoothDescriptorUUID.kt similarity index 100% rename from kable-core/src/jsMain/kotlin/external/BluetoothDescriptorUUID.kt rename to kable-core/src/webMain/kotlin/external/BluetoothDescriptorUUID.kt diff --git a/kable-core/src/jsMain/kotlin/external/BluetoothDevice.kt b/kable-core/src/webMain/kotlin/external/BluetoothDevice.kt similarity index 90% rename from kable-core/src/jsMain/kotlin/external/BluetoothDevice.kt rename to kable-core/src/webMain/kotlin/external/BluetoothDevice.kt index ff50fbdc4..829a803d2 100644 --- a/kable-core/src/jsMain/kotlin/external/BluetoothDevice.kt +++ b/kable-core/src/webMain/kotlin/external/BluetoothDevice.kt @@ -1,6 +1,6 @@ package com.juul.kable.external -import org.w3c.dom.events.EventTarget +import web.events.EventTarget import kotlin.js.Promise /** @@ -26,8 +26,8 @@ internal abstract external class BluetoothDevice : EventTarget { // Experimental advertisement features // https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetoothdevice-watchadvertisements // Requires chrome://flags/#enable-experimental-web-platform-features - fun watchAdvertisements(): Promise - fun unwatchAdvertisements(): Promise + fun watchAdvertisements(): Promise + fun unwatchAdvertisements(): Promise val watchingAdvertisements: Boolean } diff --git a/kable-core/src/jsMain/kotlin/external/BluetoothLEScanFilterInit.kt b/kable-core/src/webMain/kotlin/external/BluetoothLEScanFilterInit.kt similarity index 60% rename from kable-core/src/jsMain/kotlin/external/BluetoothLEScanFilterInit.kt rename to kable-core/src/webMain/kotlin/external/BluetoothLEScanFilterInit.kt index ccc5e84bb..22352a69e 100644 --- a/kable-core/src/jsMain/kotlin/external/BluetoothLEScanFilterInit.kt +++ b/kable-core/src/webMain/kotlin/external/BluetoothLEScanFilterInit.kt @@ -1,5 +1,8 @@ package com.juul.kable.external +import kotlin.js.JsAny +import kotlin.js.JsArray + /** * ``` * dictionary BluetoothLEScanFilterInit { @@ -13,10 +16,10 @@ package com.juul.kable.external * * https://webbluetoothcg.github.io/web-bluetooth/#device-discovery */ -internal external interface BluetoothLEScanFilterInit { - var services: Array? +internal external interface BluetoothLEScanFilterInit : JsAny { + var services: JsArray? var name: String? var namePrefix: String? - var manufacturerData: Array? - var serviceData: Array? + var manufacturerData: JsArray? + var serviceData: JsArray? } diff --git a/kable-core/src/jsMain/kotlin/external/BluetoothLEScanOptions.kt b/kable-core/src/webMain/kotlin/external/BluetoothLEScanOptions.kt similarity index 70% rename from kable-core/src/jsMain/kotlin/external/BluetoothLEScanOptions.kt rename to kable-core/src/webMain/kotlin/external/BluetoothLEScanOptions.kt index 645510e33..a70693dd2 100644 --- a/kable-core/src/jsMain/kotlin/external/BluetoothLEScanOptions.kt +++ b/kable-core/src/webMain/kotlin/external/BluetoothLEScanOptions.kt @@ -1,5 +1,8 @@ package com.juul.kable.external +import kotlin.js.JsAny +import kotlin.js.JsArray + /** * ``` * dictionary BluetoothLEScanOptions { @@ -11,8 +14,8 @@ package com.juul.kable.external * * https://webbluetoothcg.github.io/web-bluetooth/scanning.html#scanning */ -internal external interface BluetoothLEScanOptions { - var filters: Array? +internal external interface BluetoothLEScanOptions : JsAny { + var filters: JsArray? var keepRepeatedDevices: Boolean? var acceptAllAdvertisements: Boolean? } diff --git a/kable-core/src/jsMain/kotlin/external/BluetoothManufacturerDataFilterInit.kt b/kable-core/src/webMain/kotlin/external/BluetoothManufacturerDataFilterInit.kt similarity index 100% rename from kable-core/src/jsMain/kotlin/external/BluetoothManufacturerDataFilterInit.kt rename to kable-core/src/webMain/kotlin/external/BluetoothManufacturerDataFilterInit.kt diff --git a/kable-core/src/jsMain/kotlin/external/BluetoothRemoteGATTCharacteristic.kt b/kable-core/src/webMain/kotlin/external/BluetoothRemoteGATTCharacteristic.kt similarity index 86% rename from kable-core/src/jsMain/kotlin/external/BluetoothRemoteGATTCharacteristic.kt rename to kable-core/src/webMain/kotlin/external/BluetoothRemoteGATTCharacteristic.kt index 6bec05f63..d4ebaf7ac 100644 --- a/kable-core/src/jsMain/kotlin/external/BluetoothRemoteGATTCharacteristic.kt +++ b/kable-core/src/webMain/kotlin/external/BluetoothRemoteGATTCharacteristic.kt @@ -2,7 +2,8 @@ package com.juul.kable.external import com.juul.kable.UUID import org.khronos.webgl.DataView -import org.w3c.dom.events.EventTarget +import web.events.EventTarget +import kotlin.js.JsArray import kotlin.js.Promise /** @@ -17,12 +18,12 @@ internal external class BluetoothRemoteGATTCharacteristic : EventTarget { val value: DataView? fun getDescriptor(descriptor: BluetoothDescriptorUUID): Promise - fun getDescriptors(): Promise> + fun getDescriptors(): Promise> fun readValue(): Promise - fun writeValueWithResponse(value: BufferSource): Promise - fun writeValueWithoutResponse(value: BufferSource): Promise + fun writeValueWithResponse(value: BufferSource): Promise + fun writeValueWithoutResponse(value: BufferSource): Promise /** * > All notifications become inactive when a device is disconnected. A site that wants to keep diff --git a/kable-core/src/jsMain/kotlin/external/BluetoothRemoteGATTDescriptor.kt b/kable-core/src/webMain/kotlin/external/BluetoothRemoteGATTDescriptor.kt similarity index 83% rename from kable-core/src/jsMain/kotlin/external/BluetoothRemoteGATTDescriptor.kt rename to kable-core/src/webMain/kotlin/external/BluetoothRemoteGATTDescriptor.kt index 67c373cea..67579cddf 100644 --- a/kable-core/src/jsMain/kotlin/external/BluetoothRemoteGATTDescriptor.kt +++ b/kable-core/src/webMain/kotlin/external/BluetoothRemoteGATTDescriptor.kt @@ -2,7 +2,7 @@ package com.juul.kable.external import com.juul.kable.UUID import org.khronos.webgl.DataView -import org.w3c.dom.events.EventTarget +import web.events.EventTarget import kotlin.js.Promise /** @@ -13,5 +13,5 @@ internal external class BluetoothRemoteGATTDescriptor : EventTarget { val uuid: UUID val characteristic: BluetoothRemoteGATTCharacteristic fun readValue(): Promise - fun writeValue(value: BufferSource): Promise + fun writeValue(value: BufferSource): Promise } diff --git a/kable-core/src/jsMain/kotlin/external/BluetoothRemoteGATTServer.kt b/kable-core/src/webMain/kotlin/external/BluetoothRemoteGATTServer.kt similarity index 69% rename from kable-core/src/jsMain/kotlin/external/BluetoothRemoteGATTServer.kt rename to kable-core/src/webMain/kotlin/external/BluetoothRemoteGATTServer.kt index 4256b4416..9e12b9740 100644 --- a/kable-core/src/jsMain/kotlin/external/BluetoothRemoteGATTServer.kt +++ b/kable-core/src/webMain/kotlin/external/BluetoothRemoteGATTServer.kt @@ -1,12 +1,14 @@ package com.juul.kable.external +import kotlin.js.JsAny +import kotlin.js.JsArray import kotlin.js.Promise /** * https://developer.mozilla.org/en-US/docs/Web/API/BluetoothRemoteGATTServer * https://webbluetoothcg.github.io/web-bluetooth/#bluetoothgattremoteserver-interface */ -internal external interface BluetoothRemoteGATTServer { +internal external interface BluetoothRemoteGATTServer : JsAny { val device: BluetoothDevice val connected: Boolean @@ -14,11 +16,11 @@ internal external interface BluetoothRemoteGATTServer { fun connect(): Promise fun disconnect(): Unit - fun getPrimaryServices(): Promise> + fun getPrimaryServices(): Promise> fun getPrimaryServices( service: BluetoothServiceUUID, - ): Promise> + ): Promise> } internal fun BluetoothRemoteGATTServer.string() = diff --git a/kable-core/src/jsMain/kotlin/external/BluetoothRemoteGATTService.kt b/kable-core/src/webMain/kotlin/external/BluetoothRemoteGATTService.kt similarity index 71% rename from kable-core/src/jsMain/kotlin/external/BluetoothRemoteGATTService.kt rename to kable-core/src/webMain/kotlin/external/BluetoothRemoteGATTService.kt index 18ba5e474..786bd074f 100644 --- a/kable-core/src/jsMain/kotlin/external/BluetoothRemoteGATTService.kt +++ b/kable-core/src/webMain/kotlin/external/BluetoothRemoteGATTService.kt @@ -1,7 +1,8 @@ package com.juul.kable.external import com.juul.kable.UUID -import org.w3c.dom.events.EventTarget +import web.events.EventTarget +import kotlin.js.JsArray import kotlin.js.Promise /** @@ -12,5 +13,5 @@ internal external class BluetoothRemoteGATTService : EventTarget { val uuid: UUID - fun getCharacteristics(): Promise> + fun getCharacteristics(): Promise> } diff --git a/kable-core/src/jsMain/kotlin/external/BluetoothScan.kt b/kable-core/src/webMain/kotlin/external/BluetoothScan.kt similarity index 64% rename from kable-core/src/jsMain/kotlin/external/BluetoothScan.kt rename to kable-core/src/webMain/kotlin/external/BluetoothScan.kt index daf96b6e9..031a8dba7 100644 --- a/kable-core/src/jsMain/kotlin/external/BluetoothScan.kt +++ b/kable-core/src/webMain/kotlin/external/BluetoothScan.kt @@ -1,8 +1,10 @@ package com.juul.kable.external +import kotlin.js.JsAny + /** * https://webbluetoothcg.github.io/web-bluetooth/scanning.html#bluetoothlescan */ -internal external interface BluetoothScan { +internal external interface BluetoothScan : JsAny { fun stop() } diff --git a/kable-core/src/jsMain/kotlin/external/BluetoothServiceDataFilterInit.kt b/kable-core/src/webMain/kotlin/external/BluetoothServiceDataFilterInit.kt similarity index 100% rename from kable-core/src/jsMain/kotlin/external/BluetoothServiceDataFilterInit.kt rename to kable-core/src/webMain/kotlin/external/BluetoothServiceDataFilterInit.kt diff --git a/kable-core/src/jsMain/kotlin/external/BluetoothServiceDataMap.kt b/kable-core/src/webMain/kotlin/external/BluetoothServiceDataMap.kt similarity index 100% rename from kable-core/src/jsMain/kotlin/external/BluetoothServiceDataMap.kt rename to kable-core/src/webMain/kotlin/external/BluetoothServiceDataMap.kt diff --git a/kable-core/src/jsMain/kotlin/external/BluetoothServiceUUID.kt b/kable-core/src/webMain/kotlin/external/BluetoothServiceUUID.kt similarity index 82% rename from kable-core/src/jsMain/kotlin/external/BluetoothServiceUUID.kt rename to kable-core/src/webMain/kotlin/external/BluetoothServiceUUID.kt index 8300fb05d..85fda7751 100644 --- a/kable-core/src/jsMain/kotlin/external/BluetoothServiceUUID.kt +++ b/kable-core/src/webMain/kotlin/external/BluetoothServiceUUID.kt @@ -1,9 +1,11 @@ package com.juul.kable.external +import kotlin.js.JsString + /** * According to [Web Bluetooth](https://webbluetoothcg.github.io/web-bluetooth/#typedefdef-bluetoothserviceuuid): * * > BluetoothServiceUUID represents 16- and 32-bit UUID aliases, valid UUIDs, and names defined in * > [BLUETOOTH-ASSIGNED-SERVICES](https://webbluetoothcg.github.io/web-bluetooth/#biblio-bluetooth-assigned-services). */ -internal typealias BluetoothServiceUUID = String +internal typealias BluetoothServiceUUID = JsString diff --git a/kable-core/src/jsMain/kotlin/external/BluetoothUUID.kt b/kable-core/src/webMain/kotlin/external/BluetoothUUID.kt similarity index 86% rename from kable-core/src/jsMain/kotlin/external/BluetoothUUID.kt rename to kable-core/src/webMain/kotlin/external/BluetoothUUID.kt index 4924f1ec1..686fd2b2d 100644 --- a/kable-core/src/jsMain/kotlin/external/BluetoothUUID.kt +++ b/kable-core/src/webMain/kotlin/external/BluetoothUUID.kt @@ -1,6 +1,7 @@ package com.juul.kable.external import com.juul.kable.UUID +import kotlin.js.JsAny /** * According to [Web Bluetooth](https://webbluetoothcg.github.io/web-bluetooth/#uuids): @@ -12,6 +13,6 @@ import com.juul.kable.UUID */ internal abstract external class BluetoothUUID { internal companion object { - internal fun canonicalUUID(alias: dynamic): UUID + internal fun canonicalUUID(alias: JsAny?): UUID } } diff --git a/kable-core/src/jsMain/kotlin/external/BufferSource.kt b/kable-core/src/webMain/kotlin/external/BufferSource.kt similarity index 76% rename from kable-core/src/jsMain/kotlin/external/BufferSource.kt rename to kable-core/src/webMain/kotlin/external/BufferSource.kt index 6ec4268c4..da3f79f88 100644 --- a/kable-core/src/jsMain/kotlin/external/BufferSource.kt +++ b/kable-core/src/webMain/kotlin/external/BufferSource.kt @@ -1,5 +1,7 @@ package com.juul.kable.external +import org.khronos.webgl.Int8Array + /** * Per Web IDL:. * @@ -10,11 +12,8 @@ package com.juul.kable.external * Float32Array or Float64Array or DataView) ArrayBufferView; * ``` * - * [kotlin.ByteArray] are mapped to JavaScript `Int8Array`; therefore we can use [ByteArray] where external Javascript - * expects a [BufferSource]. - * * - [BufferSource](https://heycam.github.io/webidl/#BufferSource) * - [ArrayBufferView](https://heycam.github.io/webidl/#ArrayBufferView) * - [Representing Kotlin types in JavaScript](https://kotlinlang.org/docs/reference/js-to-kotlin-interop.html#representing-kotlin-types-in-javascript) */ -internal typealias BufferSource = ByteArray +internal typealias BufferSource = Int8Array diff --git a/kable-core/src/jsMain/kotlin/external/JsIterator.kt b/kable-core/src/webMain/kotlin/external/JsIterator.kt similarity index 73% rename from kable-core/src/jsMain/kotlin/external/JsIterator.kt rename to kable-core/src/webMain/kotlin/external/JsIterator.kt index 324e25b0a..078eb2acd 100644 --- a/kable-core/src/jsMain/kotlin/external/JsIterator.kt +++ b/kable-core/src/webMain/kotlin/external/JsIterator.kt @@ -1,15 +1,17 @@ package com.juul.kable.external -internal external interface JsIterator { +import kotlin.js.JsAny + +internal external interface JsIterator : JsAny { fun next(): JsIteratorResult } -internal external interface JsIteratorResult { +internal external interface JsIteratorResult : JsAny { val done: Boolean val value: T? } -internal fun JsIterator.iterable(): Iterable { +internal fun JsIterator.iterable(): Iterable { return object : Iterable { override fun iterator(): Iterator = object : Iterator { diff --git a/kable-core/src/webMain/kotlin/external/Navigator.kt b/kable-core/src/webMain/kotlin/external/Navigator.kt new file mode 100644 index 000000000..6a27c482c --- /dev/null +++ b/kable-core/src/webMain/kotlin/external/Navigator.kt @@ -0,0 +1,8 @@ +package com.juul.kable.external + +import web.navigator.Navigator +import kotlin.js.js + +/** Reference to [Bluetooth] instance or [undefined] if bluetooth is unavailable. */ +internal fun getBluetooth(navigator: Navigator): Bluetooth = + js("navigator.bluetooth") diff --git a/kable-core/src/jsMain/kotlin/external/RequestDeviceOptions.kt b/kable-core/src/webMain/kotlin/external/RequestDeviceOptions.kt similarity index 58% rename from kable-core/src/jsMain/kotlin/external/RequestDeviceOptions.kt rename to kable-core/src/webMain/kotlin/external/RequestDeviceOptions.kt index 8ed1a3205..ab7c34a7a 100644 --- a/kable-core/src/jsMain/kotlin/external/RequestDeviceOptions.kt +++ b/kable-core/src/webMain/kotlin/external/RequestDeviceOptions.kt @@ -1,5 +1,9 @@ package com.juul.kable.external +import org.khronos.webgl.Int8Array +import kotlin.js.JsAny +import kotlin.js.JsArray + /** * ``` * dictionary RequestDeviceOptions { @@ -12,9 +16,9 @@ package com.juul.kable.external * * https://webbluetoothcg.github.io/web-bluetooth/#device-discovery */ -internal external interface RequestDeviceOptions { - var filters: Array? - var optionalServices: Array? - var optionalManufacturerData: ByteArray? +internal external interface RequestDeviceOptions : JsAny { + var filters: JsArray? + var optionalServices: JsArray? + var optionalManufacturerData: Int8Array? var acceptAllDevices: Boolean? } diff --git a/kable-core/src/webMain/kotlin/interop/Await.kt b/kable-core/src/webMain/kotlin/interop/Await.kt new file mode 100644 index 000000000..bddd5cc58 --- /dev/null +++ b/kable-core/src/webMain/kotlin/interop/Await.kt @@ -0,0 +1,24 @@ +package com.juul.kable.interop + +import kotlin.js.JsAny +import kotlin.js.JsBoolean +import kotlin.js.JsNumber +import kotlin.js.JsString +import kotlin.js.Promise +import kotlin.js.toBoolean +import kotlin.js.toDouble + +/** Wrapper around `kotlinx.coroutines.await` which exists in both js and wasm, but has no expect-actual. */ +internal expect suspend fun Promise.await(): T + +/** Syntax sugar for [await] that makes using native Kotlin types a little less verbose. */ +internal suspend fun Promise.await(): Boolean = (await() as JsBoolean).toBoolean() + +/** Syntax sugar for [await] that makes using native Kotlin types a little less verbose. */ +internal suspend fun Promise.await(): Double = (await() as JsNumber).toDouble() + +/** Syntax sugar for [await] that makes using native Kotlin types a little less verbose. */ +internal suspend fun Promise.await(): String = (await() as JsString).toString() + +/** Syntax sugar for [await] that makes using native Kotlin types a little less verbose. */ +internal suspend fun Promise.await(): Unit = await() diff --git a/kable-core/src/webMain/kotlin/interop/JsArray.kt b/kable-core/src/webMain/kotlin/interop/JsArray.kt new file mode 100644 index 000000000..d15b44397 --- /dev/null +++ b/kable-core/src/webMain/kotlin/interop/JsArray.kt @@ -0,0 +1,7 @@ +package com.juul.kable.interop + +import kotlin.js.JsArray +import kotlin.js.length + +internal fun JsArray<*>.isEmpty() = length == 0 +internal fun JsArray<*>.isNotEmpty() = !isEmpty() diff --git a/kable-core/src/jsMain/kotlin/logs/LogMessage.kt b/kable-core/src/webMain/kotlin/logs/LogMessage.kt similarity index 100% rename from kable-core/src/jsMain/kotlin/logs/LogMessage.kt rename to kable-core/src/webMain/kotlin/logs/LogMessage.kt diff --git a/kable-core/src/webMain/kotlin/logs/SystemLogEngine.kt b/kable-core/src/webMain/kotlin/logs/SystemLogEngine.kt new file mode 100644 index 000000000..163485743 --- /dev/null +++ b/kable-core/src/webMain/kotlin/logs/SystemLogEngine.kt @@ -0,0 +1,75 @@ +package com.juul.kable.logs + +import js.errors.JsErrorLike +import js.errors.toJsErrorLike +import kotlin.js.JsAny +import kotlin.js.js + +private val console: Console = js("console") + +/** + * The actual console interface is far more flexible than this (effectively just a `vararg JsAny?`), + * but calling it like that requires calling `toJsString()` all over. This interface is narrowly + * typed to exactly fit our use case so the compiler can do the type conversions for us. + */ +private external interface Console : JsAny { + fun debug(format: String, tag: String, message: String) + fun debug(format: String, tag: String, message: String, error: JsErrorLike?) + fun log(format: String, tag: String, message: String) + fun log(format: String, tag: String, message: String, error: JsErrorLike?) + fun info(format: String, tag: String, message: String) + fun info(format: String, tag: String, message: String, error: JsErrorLike?) + fun warn(format: String, tag: String, message: String) + fun warn(format: String, tag: String, message: String, error: JsErrorLike?) + fun error(format: String, tag: String, message: String) + fun error(format: String, tag: String, message: String, error: JsErrorLike?) + fun assert(assertion: Boolean, format: String, tag: String, message: String) + fun assert(assertion: Boolean, format: String, tag: String, message: String, error: JsErrorLike?) +} + +public actual object SystemLogEngine : LogEngine { + + actual override fun verbose(throwable: Throwable?, tag: String, message: String) { + debug(throwable, tag, message) + } + + actual override fun debug(throwable: Throwable?, tag: String, message: String) { + if (throwable == null) { + console.debug("[%s] %s", tag, message) + } else { + console.debug("[%s] %s\n%o", tag, message, throwable.toJsErrorLike()) + } + } + + actual override fun info(throwable: Throwable?, tag: String, message: String) { + if (throwable == null) { + console.info("[%s] %s", tag, message) + } else { + console.info("[%s] %s\n%o", tag, message, throwable.toJsErrorLike()) + } + } + + actual override fun warn(throwable: Throwable?, tag: String, message: String) { + if (throwable == null) { + console.warn("[%s] %s", tag, message) + } else { + console.warn("[%s] %s\n%o", tag, message, throwable.toJsErrorLike()) + } + } + + actual override fun error(throwable: Throwable?, tag: String, message: String) { + if (throwable == null) { + console.error("[%s] %s", tag, message) + } else { + console.error("[%s] %s\n%o", tag, message, throwable.toJsErrorLike()) + } + } + + actual override fun assert(throwable: Throwable?, tag: String, message: String) { + if (throwable == null) { + console.assert(false, "[%s] %s", tag, message) + } else { + console.assert(false, "[%s] %s\n%o", tag, message, throwable.toJsErrorLike()) + } + } +} diff --git a/kable-core/src/jsTest/kotlin/BluetoothJsTests.kt b/kable-core/src/webTest/kotlin/BluetoothJsTests.kt similarity index 86% rename from kable-core/src/jsTest/kotlin/BluetoothJsTests.kt rename to kable-core/src/webTest/kotlin/BluetoothJsTests.kt index f592688e4..cd6635949 100644 --- a/kable-core/src/jsTest/kotlin/BluetoothJsTests.kt +++ b/kable-core/src/webTest/kotlin/BluetoothJsTests.kt @@ -2,6 +2,7 @@ package com.juul.kable import com.juul.kable.external.Bluetooth import kotlinx.coroutines.test.runTest +import kotlin.js.toBoolean import kotlin.test.Test import kotlin.test.assertFailsWith import kotlin.test.assertIs @@ -10,7 +11,7 @@ class BluetoothJsTests { @Test fun bluetoothOrThrow_browserUnitTest_returnsBluetooth() = runTest { - if (isBrowser) { + if (isBrowser.toBoolean()) { assertIs(bluetoothOrThrow()) } } @@ -18,7 +19,7 @@ class BluetoothJsTests { // In Node.js unit tests, bluetooth is unavailable. @Test fun bluetoothOrThrow_nodeJsUnitTest_throwsIllegalStateException() = runTest { - if (isNode) { + if (isNode.toBoolean()) { assertFailsWith { bluetoothOrThrow() } diff --git a/kable-core/src/webTest/kotlin/Environment.kt b/kable-core/src/webTest/kotlin/Environment.kt new file mode 100644 index 000000000..07ba5c2ba --- /dev/null +++ b/kable-core/src/webTest/kotlin/Environment.kt @@ -0,0 +1,8 @@ +package com.juul.kable + +import kotlin.js.JsBoolean +import kotlin.js.js + +val isBrowser: JsBoolean = js("typeof window !== 'undefined'") + +val isNode: JsBoolean = js("typeof process !== 'undefined' && process.versions && process.versions.node") diff --git a/kable-core/src/jsTest/kotlin/RequestPeripheralTests.kt b/kable-core/src/webTest/kotlin/RequestPeripheralTests.kt similarity index 100% rename from kable-core/src/jsTest/kotlin/RequestPeripheralTests.kt rename to kable-core/src/webTest/kotlin/RequestPeripheralTests.kt diff --git a/kable-log-engine-khronicle/build.gradle.kts b/kable-log-engine-khronicle/build.gradle.kts index 562d971c8..a8fed1d6f 100644 --- a/kable-log-engine-khronicle/build.gradle.kts +++ b/kable-log-engine-khronicle/build.gradle.kts @@ -15,6 +15,7 @@ kotlin { macosArm64() macosX64() jvm() + wasmJs().browser() sourceSets { commonMain.dependencies {