react-native-gizwits-sdk-v5 1.7.8-beta → 1.7.10-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -82,4 +82,7 @@ dependencies {
82
82
  //ktor
83
83
  implementation "io.ktor:ktor-network:$ktor_network_version"
84
84
 
85
+ testImplementation 'junit:junit:4.13.2'
86
+ testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlin_coroutine_version"
87
+
85
88
  }
@@ -0,0 +1,72 @@
1
+ package com.gizwits.reactnativegizwitssdkv5
2
+
3
+ import kotlinx.coroutines.CoroutineScope
4
+ import kotlinx.coroutines.Job
5
+ import kotlinx.coroutines.channels.Channel
6
+ import kotlinx.coroutines.delay
7
+ import kotlinx.coroutines.launch
8
+
9
+ internal class ConflatedEventDispatcher(
10
+ scope: CoroutineScope,
11
+ private val windowMillis: Long,
12
+ private val dispatch: suspend () -> Unit,
13
+ ) {
14
+ private val requests = Channel<Unit>(Channel.CONFLATED)
15
+ private val worker = scope.launch {
16
+ for (request in requests) {
17
+ if (windowMillis > 0) {
18
+ delay(windowMillis)
19
+ }
20
+ while (requests.tryReceive().isSuccess) {
21
+ // Keep only the latest-state notification for this window.
22
+ }
23
+ dispatch()
24
+ }
25
+ }
26
+
27
+ init {
28
+ require(windowMillis >= 0) { "windowMillis must not be negative" }
29
+ }
30
+
31
+ internal fun request(): Boolean = requests.trySend(Unit).isSuccess
32
+
33
+ internal fun cancel() {
34
+ requests.close()
35
+ worker.cancel()
36
+ }
37
+ }
38
+
39
+ internal class KeyedSubscriptionRegistry<K, V>(
40
+ private val keySelector: (V) -> K,
41
+ private val subscribe: (V) -> Job,
42
+ ) {
43
+ private data class Subscription<V>(val value: V, val job: Job)
44
+
45
+ private val subscriptions = linkedMapOf<K, Subscription<V>>()
46
+
47
+ @Synchronized
48
+ internal fun update(values: Collection<V>) {
49
+ val valuesByKey = values.associateBy(keySelector)
50
+
51
+ (subscriptions.keys - valuesByKey.keys).forEach { key ->
52
+ subscriptions.remove(key)?.job?.cancel()
53
+ }
54
+
55
+ valuesByKey.forEach { (key, value) ->
56
+ val existing = subscriptions[key]
57
+ if (existing == null || !existing.job.isActive || existing.value !== value) {
58
+ existing?.job?.cancel()
59
+ subscriptions[key] = Subscription(value, subscribe(value))
60
+ }
61
+ }
62
+ }
63
+
64
+ @Synchronized
65
+ internal fun cancelAll() {
66
+ subscriptions.values.forEach { it.job.cancel() }
67
+ subscriptions.clear()
68
+ }
69
+
70
+ @Synchronized
71
+ internal fun keys(): Set<K> = subscriptions.keys.toSet()
72
+ }
@@ -188,6 +188,17 @@ class RNGizDeviceManagerModule(reactContext: ReactApplicationContext) : ReactCon
188
188
 
189
189
  override fun getName() = "RNGizDeviceManagerModule"
190
190
 
191
+ /**
192
+ * NativeEventEmitter requires these lifecycle methods on Android. Device
193
+ * events are produced by SDK operations, so there is no native listener
194
+ * resource to allocate or release here.
195
+ */
196
+ @ReactMethod
197
+ fun addListener(eventName: String) = Unit
198
+
199
+ @ReactMethod
200
+ fun removeListeners(count: Int) = Unit
201
+
191
202
  override fun onCatalystInstanceDestroy() {
192
203
  super.onCatalystInstanceDestroy()
193
204
  moduleScope.cancel() // 取消所有协程
@@ -28,20 +28,18 @@ import com.google.gson.annotations.SerializedName
28
28
  import kotlinx.coroutines.CoroutineScope
29
29
  import kotlinx.coroutines.Dispatchers
30
30
  import kotlinx.coroutines.ExperimentalCoroutinesApi
31
- import kotlinx.coroutines.async
32
- import kotlinx.coroutines.coroutineScope
31
+ import kotlinx.coroutines.SupervisorJob
32
+ import kotlinx.coroutines.cancel
33
33
  import kotlinx.coroutines.flow.Flow
34
- import kotlinx.coroutines.flow.collectLatest
34
+ import kotlinx.coroutines.flow.collect
35
35
  import kotlinx.coroutines.flow.combine
36
- import kotlinx.coroutines.flow.debounce
37
- import kotlinx.coroutines.flow.emptyFlow
38
- import kotlinx.coroutines.flow.first
39
- import kotlinx.coroutines.flow.flatMapConcat
40
- import kotlinx.coroutines.flow.last
41
- import kotlinx.coroutines.flow.launchIn
36
+ import kotlinx.coroutines.flow.distinctUntilChanged
37
+ import kotlinx.coroutines.flow.drop
38
+ import kotlinx.coroutines.flow.filterNotNull
42
39
  import kotlinx.coroutines.flow.mapLatest
43
40
  import kotlinx.coroutines.flow.take
44
41
  import kotlinx.coroutines.launch
42
+ import kotlinx.coroutines.supervisorScope
45
43
  import org.json.JSONArray
46
44
  import org.json.JSONObject
47
45
 
@@ -62,15 +60,24 @@ data class GizFeedback(
62
60
 
63
61
  class RNGizSDKManagerModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
64
62
 
63
+ private val moduleJob = SupervisorJob()
64
+ private val moduleScope = CoroutineScope(moduleJob + Dispatchers.IO)
65
+ @Volatile
66
+ private var latestDeviceList = emptyList<GizDevice>()
65
67
  private var mReactContext: ReactContext? = null
68
+ private val deviceListEventDispatcher = ConflatedEventDispatcher(
69
+ scope = moduleScope,
70
+ windowMillis = 500,
71
+ dispatch = ::emitDeviceListChange,
72
+ )
73
+ private val deviceSubscriptions = KeyedSubscriptionRegistry<String, GizDevice>(
74
+ keySelector = GizDevice::id,
75
+ subscribe = ::subscribeToDevice,
76
+ )
66
77
 
67
- private var isSubscriptDevices: List<String> = listOf();
68
78
  init {
69
79
  mReactContext = reactContext
70
80
  }
71
- companion object {
72
- internal var devices = listOf<GizDevice>()
73
- }
74
81
  enum class EventName(val value: String) {
75
82
  DeviceDataListener("DeviceDataListener"),
76
83
  DeviceListListener("DeviceListListener"),
@@ -107,6 +114,18 @@ class RNGizSDKManagerModule(reactContext: ReactApplicationContext) : ReactContex
107
114
  }
108
115
 
109
116
  override fun getName() = "RNGizSDKManagerModule"
117
+
118
+ /**
119
+ * NativeEventEmitter requires these lifecycle methods on Android. SDK
120
+ * subscriptions are owned by this module's lifecycle rather than by each JS
121
+ * listener, so the methods intentionally do not start or stop subscriptions.
122
+ */
123
+ @ReactMethod
124
+ fun addListener(eventName: String) = Unit
125
+
126
+ @ReactMethod
127
+ fun removeListeners(count: Int) = Unit
128
+
110
129
  private val deviceListState: Flow<List<GizDevice>> =
111
130
  combineLatest(
112
131
  GizSDKManager.subscribeBoundDeviceList(),
@@ -120,7 +139,7 @@ class RNGizSDKManagerModule(reactContext: ReactApplicationContext) : ReactContex
120
139
 
121
140
  override fun initialize() {
122
141
  super.initialize()
123
- CoroutineScope(Dispatchers.IO).launch {
142
+ moduleScope.launch {
124
143
  GizSDKManager.subscribeBindEvent().collect {
125
144
  val data = JSONObject()
126
145
  data.put("did", it.first)
@@ -129,159 +148,141 @@ class RNGizSDKManagerModule(reactContext: ReactApplicationContext) : ReactContex
129
148
  }
130
149
  }
131
150
 
132
- // 监听设备对象
133
- deviceListState
134
- .debounce(500)
135
- .flatMapConcat { deviceList ->
136
- deviceList.map { dev ->
137
-
138
- if (!isSubscriptDevices.contains(dev.mac)) {
139
- CoroutineScope(Dispatchers.IO).launch {
140
- async {
141
- dev.bleCapability.subscribeModuleProfile().debounce(500).collect {
142
- if (it != null) {
143
- emitDeviceListChange()
144
- }
145
- }
146
- }
147
- async {
148
- combine(
149
- dev.bleCapability.subscribeStatus(),
150
- dev.bleCapability.subscribeIsLogin(),
151
- ) { status, isLogin ->
152
- Pair(status, isLogin)
153
- }.collect {
154
- val data = JSONObject()
155
- data.put("state", it.first.toInt())
156
- data.put("isLogin", it.second)
157
- data.put("type", "BLE")
158
- data.put("device", dev.toJsonObject())
159
- sendEvent(EventName.DeviceStateListener.name, GizRNCallbackManager.jsonObject2WriteableMap(data))
160
- }
161
- }
162
- async {
163
- dev.bleCapability.subscribeDp().collect{
164
- val data = JSONObject()
165
- data.put("data", JSONObject(it.toString()))
166
- data.put("type", "BLE")
167
- data.put("device", dev.toJsonObject())
168
- sendEvent(EventName.DeviceDataListener.name, GizRNCallbackManager.jsonObject2WriteableMap(data))
169
- }
170
- }
171
- async {
172
- dev.lanCapability.subscribeModuleProfile().debounce(500).collectLatest{
173
- if (it != null) {
174
- emitDeviceListChange()
175
- }
176
- }
177
- }
178
-
179
- async {
180
- combineLatest(
181
- dev.lanCapability.subscribeStatus(),
182
- dev.lanCapability.subscribeIsLogin(),
183
- ) { status, isLogin ->
184
- Pair(status, isLogin)
185
- }.collectLatest {
186
- val data = JSONObject()
187
- data.put("state", it.first.toInt())
188
- data.put("isLogin", it.second)
189
- data.put("type", "LAN")
190
- data.put("device", dev.toJsonObject())
191
- sendEvent(EventName.DeviceStateListener.name, GizRNCallbackManager.jsonObject2WriteableMap(data))
192
- }
193
- }
194
-
195
- async {
196
- dev.lanCapability.subscribeDp().collect{
197
- val data = JSONObject()
198
- data.put("data", JSONObject(it.toString()))
199
- data.put("type", "LAN")
200
- data.put("device", dev.toJsonObject())
201
- sendEvent(EventName.DeviceDataListener.name, GizRNCallbackManager.jsonObject2WriteableMap(data))
202
- }
203
- }
204
-
205
- async {
206
- combineLatest(
207
- dev.mqttCapability.subscribeStatus(),
208
- dev.mqttCapability.subscribeIsLogin(),
209
- ) { status, isLogin ->
210
- Pair(status, isLogin)
211
- }.collectLatest {
212
- val data = JSONObject()
213
- data.put("state", it.first.toInt())
214
- data.put("isLogin", it.second)
215
- data.put("type", "MQTT")
216
- data.put("device", dev.toJsonObject())
217
- sendEvent(EventName.DeviceStateListener.name, GizRNCallbackManager.jsonObject2WriteableMap(data))
218
- }
219
- }
220
-
221
- // 不会主动发生改表
222
- // async {
223
- // dev.mqttCapability.subscribeModuleProfile().debounce(500).collectLatest{
224
- // if (it != null) {
225
- // emitDeviceListChange()
226
- // }
227
- // }
228
- // }
229
-
230
- async {
231
- dev.mqttCapability.subscribeDp().collect{
232
- val data = JSONObject()
233
- data.put("data", JSONObject(it.toString()))
234
- data.put("type", "MQTT")
235
- data.put("device", dev.toJsonObject())
236
- sendEvent(EventName.DeviceDataListener.name, GizRNCallbackManager.jsonObject2WriteableMap(data))
237
- }
238
- }
239
- }
151
+ moduleScope.launch {
152
+ deviceListState
153
+ .distinctUntilChanged { previous, current ->
154
+ previous.size == current.size && previous.indices.all { index ->
155
+ previous[index].id == current[index].id && previous[index] === current[index]
240
156
  }
241
157
  }
242
-
243
- /*
244
- 更新当前已经订阅的设备
245
- 如果设备消失,又出现,就可以正常的重新订阅
246
- */
247
- isSubscriptDevices = deviceList.map{
248
- it.mac
158
+ .collect { deviceList ->
159
+ latestDeviceList = deviceList
160
+ deviceSubscriptions.update(deviceList)
161
+ requestDeviceListChange()
249
162
  }
163
+ }
164
+ }
250
165
 
251
- emptyFlow<Unit>()
166
+ private fun subscribeToDevice(device: GizDevice) = moduleScope.launch {
167
+ supervisorScope {
168
+ launch {
169
+ device.bleCapability.subscribeModuleProfile()
170
+ .drop(1)
171
+ .filterNotNull()
172
+ .collect { requestDeviceListChange() }
252
173
  }
253
- .launchIn(CoroutineScope(Dispatchers.IO))
254
-
255
- // 推送设备列表变化
256
- CoroutineScope(Dispatchers.IO).launch {
257
- deviceListState.debounce(500).collectLatest { deviceList ->
258
- devices = deviceList
259
- emitDeviceListChange()
174
+ launch {
175
+ combine(
176
+ device.bleCapability.subscribeStatus(),
177
+ device.bleCapability.subscribeIsLogin(),
178
+ ) { status, isLogin -> Pair(status, isLogin) }
179
+ .collect { (status, isLogin) ->
180
+ val data = JSONObject()
181
+ data.put("state", status.toInt())
182
+ data.put("isLogin", isLogin)
183
+ data.put("type", "BLE")
184
+ data.put("device", device.toJsonObject())
185
+ sendEvent(EventName.DeviceStateListener.name, GizRNCallbackManager.jsonObject2WriteableMap(data))
186
+ }
187
+ }
188
+ launch {
189
+ device.bleCapability.subscribeDp().collect { payload ->
190
+ val data = JSONObject()
191
+ data.put("data", JSONObject(payload.toString()))
192
+ data.put("type", "BLE")
193
+ data.put("device", device.toJsonObject())
194
+ sendEvent(EventName.DeviceDataListener.name, GizRNCallbackManager.jsonObject2WriteableMap(data))
195
+ }
196
+ }
197
+ launch {
198
+ device.lanCapability.subscribeModuleProfile()
199
+ .drop(1)
200
+ .filterNotNull()
201
+ .collect { requestDeviceListChange() }
202
+ }
203
+ launch {
204
+ combineLatest(
205
+ device.lanCapability.subscribeStatus(),
206
+ device.lanCapability.subscribeIsLogin(),
207
+ ) { status, isLogin -> Pair(status, isLogin) }
208
+ .collect { (status, isLogin) ->
209
+ val data = JSONObject()
210
+ data.put("state", status.toInt())
211
+ data.put("isLogin", isLogin)
212
+ data.put("type", "LAN")
213
+ data.put("device", device.toJsonObject())
214
+ sendEvent(EventName.DeviceStateListener.name, GizRNCallbackManager.jsonObject2WriteableMap(data))
215
+ }
216
+ }
217
+ launch {
218
+ device.lanCapability.subscribeDp().collect { payload ->
219
+ val data = JSONObject()
220
+ data.put("data", JSONObject(payload.toString()))
221
+ data.put("type", "LAN")
222
+ data.put("device", device.toJsonObject())
223
+ sendEvent(EventName.DeviceDataListener.name, GizRNCallbackManager.jsonObject2WriteableMap(data))
224
+ }
225
+ }
226
+ launch {
227
+ combineLatest(
228
+ device.mqttCapability.subscribeStatus(),
229
+ device.mqttCapability.subscribeIsLogin(),
230
+ ) { status, isLogin -> Pair(status, isLogin) }
231
+ .collect { (status, isLogin) ->
232
+ val data = JSONObject()
233
+ data.put("state", status.toInt())
234
+ data.put("isLogin", isLogin)
235
+ data.put("type", "MQTT")
236
+ data.put("device", device.toJsonObject())
237
+ sendEvent(EventName.DeviceStateListener.name, GizRNCallbackManager.jsonObject2WriteableMap(data))
238
+ }
239
+ }
240
+ launch {
241
+ device.mqttCapability.subscribeDp().collect { payload ->
242
+ val data = JSONObject()
243
+ data.put("data", JSONObject(payload.toString()))
244
+ data.put("type", "MQTT")
245
+ data.put("device", device.toJsonObject())
246
+ sendEvent(EventName.DeviceDataListener.name, GizRNCallbackManager.jsonObject2WriteableMap(data))
247
+ }
260
248
  }
261
249
  }
262
250
  }
263
251
 
252
+ private fun requestDeviceListChange() {
253
+ deviceListEventDispatcher.request()
254
+ }
255
+
264
256
  /**
265
257
  * RN的场景下
266
258
  * module profile 变更 相当于设备列表变更
267
259
  */
268
- suspend fun emitDeviceListChange() {
269
- val deviceList = deviceListState.first()
260
+ private suspend fun emitDeviceListChange() {
270
261
  // 缓存变更
271
262
  val deviceListJson = JSONArray()
272
- deviceList.forEach{ item ->
263
+ latestDeviceList.forEach{ item ->
273
264
  deviceListJson.put(item.toJsonObject())
274
265
  }
275
266
  sendEvent(EventName.DeviceListListener.name, GizRNCallbackManager.jsonArray2WriteableArray(deviceListJson))
276
267
  }
277
268
 
269
+ override fun invalidate() {
270
+ deviceSubscriptions.cancelAll()
271
+ deviceListEventDispatcher.cancel()
272
+ moduleScope.cancel()
273
+ mReactContext = null
274
+ super.invalidate()
275
+ }
276
+
278
277
  fun sendEvent(name:String, data: WritableArray?) {
279
- mReactContext!!
278
+ val reactContext = mReactContext ?: return
279
+ reactContext
280
280
  .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
281
281
  .emit(name, data)
282
282
  }
283
283
  fun sendEvent(name:String, data: WritableMap?) {
284
- mReactContext!!
284
+ val reactContext = mReactContext ?: return
285
+ reactContext
285
286
  .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
286
287
  .emit(name, data)
287
288
  }
@@ -326,6 +327,7 @@ class RNGizSDKManagerModule(reactContext: ReactApplicationContext) : ReactContex
326
327
  // 查询 绑定设备 返回设备列表
327
328
  GizSDKManager.queryBoundDevices()
328
329
  deviceListState.take(1).collect {
330
+ latestDeviceList = it
329
331
  val jsonData = JSONArray()
330
332
  it.forEach{device ->
331
333
  jsonData.put(device.toJsonObject())
@@ -337,7 +339,7 @@ class RNGizSDKManagerModule(reactContext: ReactApplicationContext) : ReactContex
337
339
  writableMap.putInt("error", 0)
338
340
  writableMap.putString("message", "")
339
341
  result.invoke(null, writableMap)
340
- emitDeviceListChange()
342
+ requestDeviceListChange()
341
343
  }
342
344
  }
343
345
  }
@@ -0,0 +1,110 @@
1
+ package com.gizwits.reactnativegizwitssdkv5
2
+
3
+ import kotlinx.coroutines.Job
4
+ import kotlinx.coroutines.ExperimentalCoroutinesApi
5
+ import kotlinx.coroutines.test.advanceTimeBy
6
+ import kotlinx.coroutines.test.runCurrent
7
+ import kotlinx.coroutines.test.runTest
8
+ import org.junit.Assert.assertEquals
9
+ import org.junit.Assert.assertFalse
10
+ import org.junit.Assert.assertTrue
11
+ import org.junit.Test
12
+
13
+ @OptIn(ExperimentalCoroutinesApi::class)
14
+ class DeviceListEventCoordinatorTest {
15
+ @Test
16
+ fun requestBeforeWorkerStartsIsDelivered() = runTest {
17
+ var dispatchCount = 0
18
+ val dispatcher = ConflatedEventDispatcher(this, 500) { dispatchCount++ }
19
+
20
+ assertTrue(dispatcher.request())
21
+ advanceTimeBy(500)
22
+ runCurrent()
23
+
24
+ assertEquals(1, dispatchCount)
25
+ dispatcher.cancel()
26
+ }
27
+
28
+ @Test
29
+ fun burstRequestsDeliverOnlyTheLatestSnapshot() = runTest {
30
+ var latestSnapshot = 0
31
+ val deliveredSnapshots = mutableListOf<Int>()
32
+ val dispatcher = ConflatedEventDispatcher(this, 500) {
33
+ deliveredSnapshots += latestSnapshot
34
+ }
35
+
36
+ dispatcher.request()
37
+ runCurrent()
38
+ repeat(5) { value ->
39
+ latestSnapshot = value + 1
40
+ advanceTimeBy(75)
41
+ dispatcher.request()
42
+ }
43
+ advanceTimeBy(125)
44
+ runCurrent()
45
+
46
+ assertEquals(listOf(5), deliveredSnapshots)
47
+ dispatcher.cancel()
48
+ }
49
+
50
+ @Test
51
+ fun continuousRequestsStillDispatchAtEveryWindow() = runTest {
52
+ var dispatchCount = 0
53
+ val dispatcher = ConflatedEventDispatcher(this, 500) { dispatchCount++ }
54
+
55
+ dispatcher.request()
56
+ runCurrent()
57
+ repeat(15) {
58
+ advanceTimeBy(100)
59
+ dispatcher.request()
60
+ runCurrent()
61
+ }
62
+
63
+ assertTrue(dispatchCount >= 2)
64
+ dispatcher.cancel()
65
+ }
66
+
67
+ @Test
68
+ fun registryCancelsRemovedDevicesWithoutDuplicatingRetainedSubscriptions() {
69
+ val createdJobs = mutableMapOf<String, MutableList<Job>>()
70
+ val registry = KeyedSubscriptionRegistry<String, String>(
71
+ keySelector = { it },
72
+ subscribe = { key -> Job().also { createdJobs.getOrPut(key) { mutableListOf() }.add(it) } },
73
+ )
74
+
75
+ registry.update(listOf("a", "b"))
76
+ val firstAJob = createdJobs.getValue("a").single()
77
+ val firstBJob = createdJobs.getValue("b").single()
78
+ registry.update(listOf("b"))
79
+ registry.update(listOf("a", "b"))
80
+
81
+ assertFalse(firstAJob.isActive)
82
+ assertTrue(firstBJob.isActive)
83
+ assertEquals(2, createdJobs.getValue("a").size)
84
+ assertEquals(1, createdJobs.getValue("b").size)
85
+ assertEquals(setOf("a", "b"), registry.keys())
86
+
87
+ registry.cancelAll()
88
+ assertTrue(createdJobs.values.flatten().none { it.isActive })
89
+ assertTrue(registry.keys().isEmpty())
90
+ }
91
+
92
+ @Test
93
+ fun registryReplacesSubscriptionWhenValueInstanceChangesForTheSameKey() {
94
+ data class Device(val id: String)
95
+
96
+ val jobs = mutableListOf<Job>()
97
+ val registry = KeyedSubscriptionRegistry<String, Device>(
98
+ keySelector = Device::id,
99
+ subscribe = { Job().also(jobs::add) },
100
+ )
101
+
102
+ registry.update(listOf(Device("a")))
103
+ registry.update(listOf(Device("a")))
104
+
105
+ assertEquals(2, jobs.size)
106
+ assertFalse(jobs.first().isActive)
107
+ assertTrue(jobs.last().isActive)
108
+ registry.cancelAll()
109
+ }
110
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-gizwits-sdk-v5",
3
- "version": "1.7.8-beta",
3
+ "version": "1.7.10-beta",
4
4
  "description": "Gizwits",
5
5
  "homepage": "https://github.com/demchenkoalex/react-native-gizwits-sdk-v5#readme",
6
6
  "main": "lib/index.js",