Skip to content

Commit 57edad1

Browse files
authored
Prevent concurrent I/O operations on JavaScript (#98)
1 parent c2fb43d commit 57edad1

2 files changed

Lines changed: 70 additions & 35 deletions

File tree

core/src/jsMain/kotlin/Observers.kt

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import kotlinx.coroutines.await
55
import kotlinx.coroutines.flow.MutableSharedFlow
66
import kotlinx.coroutines.flow.collect
77
import kotlinx.coroutines.flow.flow
8+
import kotlinx.coroutines.sync.withLock
89
import org.khronos.webgl.DataView
910
import org.w3c.dom.events.Event
1011

@@ -41,7 +42,9 @@ internal class Observers(
4142
if (++observation.count == 1) {
4243
bluetoothRemoteGATTCharacteristic.apply {
4344
addEventListener(CHARACTERISTIC_VALUE_CHANGED, observation.listener)
44-
startNotifications().await()
45+
peripheral.ioLock.withLock {
46+
startNotifications().await()
47+
}
4548
}
4649
}
4750

@@ -51,24 +54,42 @@ internal class Observers(
5154
emit(it.data)
5255
}
5356
}
54-
} finally {
55-
if (--observation.count < 1) {
56-
bluetoothRemoteGATTCharacteristic.apply {
57-
/* Throws `DOMException` if connection is closed:
58-
*
59-
* DOMException: Failed to execute 'stopNotifications' on 'BluetoothRemoteGATTCharacteristic':
60-
* Characteristic with UUID [...] is no longer valid. Remember to retrieve the characteristic
61-
* again after reconnecting.
62-
*
63-
* Wrapped in `runCatching` to silently ignore failure, as notification will already be
64-
* invalidated due to the connection being closed.
65-
*/
66-
runCatching { stopNotifications().await() }
67-
68-
removeEventListener(CHARACTERISTIC_VALUE_CHANGED, observation.listener)
57+
} catch (t: Throwable) {
58+
// Unnecessary `catch` block as workaround for KT-37279 (needed until we switch to IR compiler).
59+
// https://youtrack.jetbrains.com/issue/KT-37279
60+
// Once KT-37279 is fixed, this `catch` should be replaced with `finally`.
61+
// See previous logic (before workaround) in:
62+
// https://github.com/JuulLabs/kable/blob/151e54d255bf5595c67023927084d083e6180706/core/src/jsMain/kotlin/Observers.kt#L48-L72
63+
observation.teardown(bluetoothRemoteGATTCharacteristic, characteristic)
64+
return@flow
65+
}
66+
observation.teardown(bluetoothRemoteGATTCharacteristic, characteristic)
67+
}
68+
69+
private suspend fun Observation.teardown(
70+
bluetoothRemoteGATTCharacteristic: BluetoothRemoteGATTCharacteristic,
71+
characteristic: Characteristic
72+
) {
73+
if (--count < 1) {
74+
bluetoothRemoteGATTCharacteristic.apply {
75+
/* Throws `DOMException` if connection is closed:
76+
*
77+
* DOMException: Failed to execute 'stopNotifications' on 'BluetoothRemoteGATTCharacteristic':
78+
* Characteristic with UUID [...] is no longer valid. Remember to retrieve the characteristic
79+
* again after reconnecting.
80+
*
81+
* Wrapped in `runCatching` to silently ignore failure, as notification will already be
82+
* invalidated due to the connection being closed.
83+
*/
84+
runCatching {
85+
peripheral.ioLock.withLock {
86+
stopNotifications().await()
87+
}
6988
}
70-
observers.remove(characteristic)
89+
90+
removeEventListener(CHARACTERISTIC_VALUE_CHANGED, listener)
7191
}
92+
observers.remove(characteristic)
7293
}
7394
}
7495

@@ -89,7 +110,9 @@ internal class Observers(
89110
platformCharacteristic
90111
.bluetoothRemoteGATTCharacteristic
91112
.apply {
92-
startNotifications().await()
113+
peripheral.ioLock.withLock {
114+
startNotifications().await()
115+
}
93116
addEventListener(CHARACTERISTIC_VALUE_CHANGED, platformCharacteristic.createListener())
94117
}
95118
}

core/src/jsMain/kotlin/Peripheral.kt

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import kotlinx.coroutines.flow.filterNotNull
2121
import kotlinx.coroutines.flow.map
2222
import kotlinx.coroutines.job
2323
import kotlinx.coroutines.suspendCancellableCoroutine
24+
import kotlinx.coroutines.sync.Mutex
25+
import kotlinx.coroutines.sync.withLock
2426
import org.khronos.webgl.DataView
2527
import kotlin.coroutines.CoroutineContext
2628
import org.w3c.dom.events.Event as JsEvent
@@ -47,6 +49,8 @@ public class JsPeripheral internal constructor(
4749

4850
private val scope = CoroutineScope(parentCoroutineContext + job)
4951

52+
internal val ioLock = Mutex()
53+
5054
private val _state = MutableStateFlow<State?>(null)
5155
public override val state: Flow<State> = _state.filterNotNull()
5256

@@ -136,9 +140,9 @@ public class JsPeripheral internal constructor(
136140
}
137141

138142
private suspend fun discoverServices(): List<PlatformService> {
139-
val services = gatt.getPrimaryServices()
140-
.await()
141-
.map { it.toPlatformService() }
143+
val services = ioLock.withLock {
144+
gatt.getPrimaryServices().await()
145+
}.map { it.toPlatformService() }
142146
_platformServices = services
143147
return services
144148
}
@@ -148,19 +152,23 @@ public class JsPeripheral internal constructor(
148152
data: ByteArray,
149153
writeType: WriteType,
150154
) {
151-
bluetoothRemoteGATTCharacteristicFrom(characteristic).run {
155+
val jsCharacteristic = bluetoothRemoteGATTCharacteristicFrom(characteristic)
156+
ioLock.withLock {
152157
when (writeType) {
153-
WithResponse -> writeValueWithResponse(data)
154-
WithoutResponse -> writeValueWithoutResponse(data)
155-
}
156-
}.await()
158+
WithResponse -> jsCharacteristic.writeValueWithResponse(data)
159+
WithoutResponse -> jsCharacteristic.writeValueWithoutResponse(data)
160+
}.await()
161+
}
157162
}
158163

159164
public suspend fun readAsDataView(
160165
characteristic: Characteristic
161-
): DataView = bluetoothRemoteGATTCharacteristicFrom(characteristic)
162-
.readValue()
163-
.await()
166+
): DataView {
167+
val jsCharacteristic = bluetoothRemoteGATTCharacteristicFrom(characteristic)
168+
return ioLock.withLock {
169+
jsCharacteristic.readValue().await()
170+
}
171+
}
164172

165173
public override suspend fun read(
166174
characteristic: Characteristic
@@ -172,16 +180,20 @@ public class JsPeripheral internal constructor(
172180
descriptor: Descriptor,
173181
data: ByteArray
174182
) {
175-
bluetoothRemoteGATTDescriptorFrom(descriptor)
176-
.writeValue(data)
177-
.await()
183+
val jsDescriptor = bluetoothRemoteGATTDescriptorFrom(descriptor)
184+
ioLock.withLock {
185+
jsDescriptor.writeValue(data).await()
186+
}
178187
}
179188

180189
public suspend fun readAsDataView(
181190
descriptor: Descriptor
182-
): DataView = bluetoothRemoteGATTDescriptorFrom(descriptor)
183-
.readValue()
184-
.await()
191+
): DataView {
192+
val jsDescriptor = bluetoothRemoteGATTDescriptorFrom(descriptor)
193+
return ioLock.withLock {
194+
jsDescriptor.readValue().await()
195+
}
196+
}
185197

186198
public override suspend fun read(
187199
descriptor: Descriptor

0 commit comments

Comments
 (0)