@inthepocket/react-native-service-discovery 0.2.4 → 0.2.6

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.
@@ -3,6 +3,8 @@ package com.servicediscovery
3
3
  import android.content.Context
4
4
  import android.net.nsd.NsdManager
5
5
  import android.net.nsd.NsdServiceInfo
6
+ import android.os.Handler
7
+ import android.os.Looper
6
8
  import android.util.Log
7
9
  import com.facebook.react.bridge.Promise
8
10
  import com.facebook.react.bridge.ReactApplicationContext
@@ -12,6 +14,7 @@ import com.facebook.react.bridge.WritableNativeArray
12
14
  import com.facebook.react.bridge.WritableNativeMap
13
15
  import com.facebook.react.modules.core.DeviceEventManagerModule
14
16
  import java.nio.charset.StandardCharsets
17
+ import java.util.LinkedList
15
18
 
16
19
 
17
20
  class ServiceDiscoveryModule internal constructor(context: ReactApplicationContext) :
@@ -24,6 +27,21 @@ class ServiceDiscoveryModule internal constructor(context: ReactApplicationConte
24
27
  // Keep track of resolved services, because onServiceLost doesn't pass a resolved service
25
28
  private val resolvedServices: MutableMap<String, NsdServiceInfo> = HashMap()
26
29
 
30
+ // Resolve queue to handle Android's single-resolve limitation
31
+ private data class ResolveRequest(val service: NsdServiceInfo, var retryCount: Int = 0)
32
+ private val resolveQueue: LinkedList<ResolveRequest> = LinkedList()
33
+ private var isResolving: Boolean = false
34
+ private val mainHandler = Handler(Looper.getMainLooper())
35
+ private val pendingServices: MutableSet<String> = HashSet()
36
+
37
+ companion object {
38
+ private const val SERVICE_FOUND = "serviceFound"
39
+ private const val SERVICE_LOST = "serviceLost"
40
+ private const val MAX_RETRY_COUNT = 3
41
+ private const val RETRY_DELAY_MS = 100L
42
+ const val NAME = "ServiceDiscovery"
43
+ }
44
+
27
45
  override fun getName(): String {
28
46
  return NAME
29
47
  }
@@ -42,6 +60,86 @@ class ServiceDiscoveryModule internal constructor(context: ReactApplicationConte
42
60
  override fun removeListeners(count: Double) {
43
61
  }
44
62
 
63
+ private fun queueServiceForResolve(service: NsdServiceInfo) {
64
+ val serviceKey = "${service.serviceName}-${service.serviceType}"
65
+ synchronized(resolveQueue) {
66
+ // Skip if already pending or resolved
67
+ if (pendingServices.contains(serviceKey) || resolvedServices.containsKey(service.serviceName)) {
68
+ return
69
+ }
70
+ pendingServices.add(serviceKey)
71
+ resolveQueue.add(ResolveRequest(service))
72
+ Log.d(NAME, "Queued service for resolve: ${service.serviceName} (queue size: ${resolveQueue.size})")
73
+ }
74
+ processResolveQueue()
75
+ }
76
+
77
+ private fun processResolveQueue() {
78
+ synchronized(resolveQueue) {
79
+ if (isResolving || resolveQueue.isEmpty()) {
80
+ return
81
+ }
82
+ isResolving = true
83
+ }
84
+
85
+ val request = synchronized(resolveQueue) { resolveQueue.poll() } ?: run {
86
+ synchronized(resolveQueue) { isResolving = false }
87
+ return
88
+ }
89
+
90
+ Log.d(NAME, "Resolving service: ${request.service.serviceName} (attempt ${request.retryCount + 1})")
91
+
92
+ try {
93
+ nsdManager.resolveService(request.service, object : NsdManager.ResolveListener {
94
+ override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
95
+ try {
96
+ val serviceKey = "${serviceInfo.serviceName}-${serviceInfo.serviceType}"
97
+ synchronized(resolveQueue) {
98
+ pendingServices.remove(serviceKey)
99
+ }
100
+ resolvedServices[serviceInfo.serviceName] = serviceInfo
101
+ sendEvent(SERVICE_FOUND, serviceToMap(serviceInfo))
102
+ Log.d(NAME, "Resolved service: ${serviceInfo.serviceName}")
103
+ } catch (e: Exception) {
104
+ Log.e(NAME, "Error handling resolved service", e)
105
+ } finally {
106
+ synchronized(resolveQueue) { isResolving = false }
107
+ // Small delay before processing next to avoid overwhelming NsdManager
108
+ mainHandler.postDelayed({ processResolveQueue() }, 50)
109
+ }
110
+ }
111
+
112
+ override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
113
+ Log.e(NAME, "Resolve failed for ${serviceInfo.serviceName}: errorCode=$errorCode")
114
+
115
+ val serviceKey = "${serviceInfo.serviceName}-${serviceInfo.serviceType}"
116
+
117
+ // Retry on FAILURE_ALREADY_ACTIVE (3) or other transient errors
118
+ if (request.retryCount < MAX_RETRY_COUNT) {
119
+ request.retryCount++
120
+ synchronized(resolveQueue) {
121
+ resolveQueue.add(request) // Re-queue for retry
122
+ }
123
+ Log.d(NAME, "Re-queued ${serviceInfo.serviceName} for retry (attempt ${request.retryCount})")
124
+ } else {
125
+ synchronized(resolveQueue) {
126
+ pendingServices.remove(serviceKey)
127
+ }
128
+ Log.e(NAME, "Giving up on ${serviceInfo.serviceName} after ${MAX_RETRY_COUNT} retries")
129
+ }
130
+
131
+ synchronized(resolveQueue) { isResolving = false }
132
+ // Longer delay after failure before retrying
133
+ mainHandler.postDelayed({ processResolveQueue() }, RETRY_DELAY_MS)
134
+ }
135
+ })
136
+ } catch (e: Exception) {
137
+ Log.e(NAME, "Exception starting resolve", e)
138
+ synchronized(resolveQueue) { isResolving = false }
139
+ mainHandler.postDelayed({ processResolveQueue() }, RETRY_DELAY_MS)
140
+ }
141
+ }
142
+
45
143
  @ReactMethod
46
144
  override fun startSearch(type: String, promise: Promise) {
47
145
  try {
@@ -55,38 +153,22 @@ class ServiceDiscoveryModule internal constructor(context: ReactApplicationConte
55
153
  val discoveryListener: NsdManager.DiscoveryListener =
56
154
  object : NsdManager.DiscoveryListener {
57
155
  override fun onDiscoveryStarted(regType: String) {
58
- // Service discovery started
156
+ Log.d(NAME, "Discovery started for $regType")
59
157
  }
60
158
 
61
159
  override fun onDiscoveryStopped(regType: String) {
62
- // Service discovery stopped
160
+ Log.d(NAME, "Discovery stopped for $regType")
63
161
  }
64
162
 
65
163
  override fun onServiceFound(service: NsdServiceInfo) {
66
164
  try {
67
165
  // Service discovery success
68
166
  if (service.serviceType == serviceType) {
69
- // Resolve the service with ad-hoc listener
70
- nsdManager.resolveService(service, object : NsdManager.ResolveListener {
71
- override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
72
- try {
73
- resolvedServices[serviceInfo.serviceName] = serviceInfo
74
- sendEvent(SERVICE_FOUND, serviceToMap(serviceInfo))
75
- } catch (e: Exception) {
76
- Log.e(NAME, "Error resolving service", e)
77
- }
78
- }
79
-
80
- override fun onResolveFailed(
81
- serviceInfo: NsdServiceInfo,
82
- errorCode: Int
83
- ) {
84
- Log.e(NAME, "Resolve failed: $errorCode")
85
- }
86
- })
167
+ // Queue the service for resolution instead of resolving immediately
168
+ queueServiceForResolve(service)
87
169
  }
88
170
  } catch (e: Exception) {
89
- Log.e(NAME, "Error resolving service", e)
171
+ Log.e(NAME, "Error queuing service for resolve", e)
90
172
  }
91
173
  }
92
174
 
@@ -101,24 +183,27 @@ class ServiceDiscoveryModule internal constructor(context: ReactApplicationConte
101
183
  sendEvent(SERVICE_LOST, serviceToMap(serviceInfo))
102
184
  }
103
185
  } catch (e: Exception) {
104
- Log.e(NAME, "Error resolving service", e)
186
+ Log.e(NAME, "Error handling service lost", e)
105
187
  }
106
188
  }
107
189
 
108
190
  override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
191
+ Log.e(NAME, "Discovery start failed: $errorCode")
109
192
  tryStopServiceDiscovery(this)
110
193
  }
111
194
 
112
195
  override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
196
+ Log.e(NAME, "Discovery stop failed: $errorCode")
113
197
  tryStopServiceDiscovery(this)
114
198
  }
115
199
  }
116
200
  discoveryListeners[serviceType] = discoveryListener
201
+
117
202
  nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryListener)
118
203
  promise.resolve(null)
119
204
  return
120
205
  } catch (e: Exception) {
121
- Log.e(NAME, "Error resolving service", e)
206
+ Log.e(NAME, "Error starting search", e)
122
207
  promise.reject(e)
123
208
  }
124
209
  }
@@ -127,16 +212,23 @@ class ServiceDiscoveryModule internal constructor(context: ReactApplicationConte
127
212
  override fun stopSearch(type: String, promise: Promise) {
128
213
  try {
129
214
  resolvedServices.clear()
215
+ synchronized(resolveQueue) {
216
+ resolveQueue.clear()
217
+ pendingServices.clear()
218
+ isResolving = false
219
+ }
220
+
130
221
  val serviceType = getServiceType(type)
131
222
  val discoveryListener = discoveryListeners.remove(serviceType)
132
223
  if (discoveryListener != null) {
133
224
  tryStopServiceDiscovery(discoveryListener)
134
225
  }
226
+
227
+ promise.resolve(null)
135
228
  } catch (e: Exception) {
136
- Log.e(NAME, "Error resolving service", e)
229
+ Log.e(NAME, "Error stopping search", e)
137
230
  promise.reject(e)
138
231
  }
139
-
140
232
  }
141
233
 
142
234
  private fun tryStopServiceDiscovery(listener: NsdManager.DiscoveryListener) {
@@ -178,14 +270,8 @@ class ServiceDiscoveryModule internal constructor(context: ReactApplicationConte
178
270
  }
179
271
  service.putMap("txt", txt)
180
272
  } catch (e: Exception) {
181
- Log.e(NAME, "Error stopping search", e)
273
+ Log.e(NAME, "Error converting service to map", e)
182
274
  }
183
275
  return service
184
276
  }
185
-
186
- companion object {
187
- private const val SERVICE_FOUND = "serviceFound"
188
- private const val SERVICE_LOST = "serviceLost"
189
- const val NAME = "ServiceDiscovery"
190
- }
191
277
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inthepocket/react-native-service-discovery",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "mDNS Network Service Discovery in React Native",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
@@ -61,13 +61,13 @@
61
61
  "registry": "https://registry.npmjs.org/"
62
62
  },
63
63
  "devDependencies": {
64
- "@commitlint/config-conventional": "^19.8.1",
64
+ "@commitlint/config-conventional": "^20.5.0",
65
65
  "@evilmartians/lefthook": "^1.5.0",
66
66
  "@react-native/eslint-config": "^0.73.1",
67
- "@release-it/conventional-changelog": "^9.0.4",
67
+ "@release-it/conventional-changelog": "^10.0.6",
68
68
  "@types/jest": "^29.5.5",
69
69
  "@types/react": "^18.2.44",
70
- "commitlint": "^19.8.1",
70
+ "commitlint": "^20.5.0",
71
71
  "del-cli": "^5.1.0",
72
72
  "eslint": "^8.51.0",
73
73
  "eslint-config-prettier": "^9.0.0",
@@ -77,15 +77,14 @@
77
77
  "react": "18.2.0",
78
78
  "react-native": "0.74.0",
79
79
  "react-native-builder-bob": "^0.23.2",
80
- "release-it": "^17.8.2",
80
+ "release-it": "^19.2.4",
81
81
  "turbo": "^1.10.7",
82
82
  "typescript": "^5.2.2"
83
83
  },
84
84
  "resolutions": {
85
85
  "@types/react": "^18.2.44",
86
- "tmp": "^0.2.4",
87
- "@conventional-changelog/git-client": "^2.0.0",
88
- "@octokit/plugin-paginate-rest": "^13.0.0"
86
+ "handlebars": ">=4.7.9",
87
+ "basic-ftp": ">=5.2.0"
89
88
  },
90
89
  "peerDependencies": {
91
90
  "react": "*",