Skip to content

Commit 687bc7e

Browse files
authored
Merge pull request #4 from inthepocket/feature/android-sequential-resolve-queue
fix(android): add resolve queue to handle NsdManager single-resolve limitation
2 parents 74c8440 + 645b9e5 commit 687bc7e

File tree

3 files changed

+121
-35
lines changed

3 files changed

+121
-35
lines changed

.github/actions/setup/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ runs:
1212
- name: Setup Ruby
1313
uses: ruby/setup-ruby@v1
1414
with:
15-
ruby-version: '3.2'
15+
ruby-version: '3.3'
1616
bundler-cache: false
1717

1818
- name: Cache dependencies

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ jobs:
104104
yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}"
105105
106106
build-ios:
107-
runs-on: macos-14
107+
runs-on: macos-15
108108
env:
109109
TURBO_CACHE_DIR: .turbo/ios
110110
steps:
@@ -137,7 +137,7 @@ jobs:
137137
with:
138138
path: |
139139
**/ios/Pods
140-
key: ${{ runner.os }}-cocoapods-${{ hashFiles('example/ios/Podfile.lock') }}
140+
key: ${{ runner.os }}-cocoapods-${{ hashFiles('example/ios/Podfile.lock', 'yarn.lock', 'package.json') }}
141141
restore-keys: |
142142
${{ runner.os }}-cocoapods-
143143

android/src/main/java/com/servicediscovery/ServiceDiscoveryModule.kt

Lines changed: 118 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package com.servicediscovery
33
import android.content.Context
44
import android.net.nsd.NsdManager
55
import android.net.nsd.NsdServiceInfo
6+
import android.os.Handler
7+
import android.os.Looper
68
import android.util.Log
79
import com.facebook.react.bridge.Promise
810
import com.facebook.react.bridge.ReactApplicationContext
@@ -12,6 +14,7 @@ import com.facebook.react.bridge.WritableNativeArray
1214
import com.facebook.react.bridge.WritableNativeMap
1315
import com.facebook.react.modules.core.DeviceEventManagerModule
1416
import java.nio.charset.StandardCharsets
17+
import java.util.LinkedList
1518

1619

1720
class ServiceDiscoveryModule internal constructor(context: ReactApplicationContext) :
@@ -24,6 +27,21 @@ class ServiceDiscoveryModule internal constructor(context: ReactApplicationConte
2427
// Keep track of resolved services, because onServiceLost doesn't pass a resolved service
2528
private val resolvedServices: MutableMap<String, NsdServiceInfo> = HashMap()
2629

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+
2745
override fun getName(): String {
2846
return NAME
2947
}
@@ -42,6 +60,86 @@ class ServiceDiscoveryModule internal constructor(context: ReactApplicationConte
4260
override fun removeListeners(count: Double) {
4361
}
4462

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+
45143
@ReactMethod
46144
override fun startSearch(type: String, promise: Promise) {
47145
try {
@@ -55,38 +153,22 @@ class ServiceDiscoveryModule internal constructor(context: ReactApplicationConte
55153
val discoveryListener: NsdManager.DiscoveryListener =
56154
object : NsdManager.DiscoveryListener {
57155
override fun onDiscoveryStarted(regType: String) {
58-
// Service discovery started
156+
Log.d(NAME, "Discovery started for $regType")
59157
}
60158

61159
override fun onDiscoveryStopped(regType: String) {
62-
// Service discovery stopped
160+
Log.d(NAME, "Discovery stopped for $regType")
63161
}
64162

65163
override fun onServiceFound(service: NsdServiceInfo) {
66164
try {
67165
// Service discovery success
68166
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)
87169
}
88170
} catch (e: Exception) {
89-
Log.e(NAME, "Error resolving service", e)
171+
Log.e(NAME, "Error queuing service for resolve", e)
90172
}
91173
}
92174

@@ -101,24 +183,27 @@ class ServiceDiscoveryModule internal constructor(context: ReactApplicationConte
101183
sendEvent(SERVICE_LOST, serviceToMap(serviceInfo))
102184
}
103185
} catch (e: Exception) {
104-
Log.e(NAME, "Error resolving service", e)
186+
Log.e(NAME, "Error handling service lost", e)
105187
}
106188
}
107189

108190
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
191+
Log.e(NAME, "Discovery start failed: $errorCode")
109192
tryStopServiceDiscovery(this)
110193
}
111194

112195
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
196+
Log.e(NAME, "Discovery stop failed: $errorCode")
113197
tryStopServiceDiscovery(this)
114198
}
115199
}
116200
discoveryListeners[serviceType] = discoveryListener
201+
117202
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryListener)
118203
promise.resolve(null)
119204
return
120205
} catch (e: Exception) {
121-
Log.e(NAME, "Error resolving service", e)
206+
Log.e(NAME, "Error starting search", e)
122207
promise.reject(e)
123208
}
124209
}
@@ -127,16 +212,23 @@ class ServiceDiscoveryModule internal constructor(context: ReactApplicationConte
127212
override fun stopSearch(type: String, promise: Promise) {
128213
try {
129214
resolvedServices.clear()
215+
synchronized(resolveQueue) {
216+
resolveQueue.clear()
217+
pendingServices.clear()
218+
isResolving = false
219+
}
220+
130221
val serviceType = getServiceType(type)
131222
val discoveryListener = discoveryListeners.remove(serviceType)
132223
if (discoveryListener != null) {
133224
tryStopServiceDiscovery(discoveryListener)
134225
}
226+
227+
promise.resolve(null)
135228
} catch (e: Exception) {
136-
Log.e(NAME, "Error resolving service", e)
229+
Log.e(NAME, "Error stopping search", e)
137230
promise.reject(e)
138231
}
139-
140232
}
141233

142234
private fun tryStopServiceDiscovery(listener: NsdManager.DiscoveryListener) {
@@ -178,14 +270,8 @@ class ServiceDiscoveryModule internal constructor(context: ReactApplicationConte
178270
}
179271
service.putMap("txt", txt)
180272
} catch (e: Exception) {
181-
Log.e(NAME, "Error stopping search", e)
273+
Log.e(NAME, "Error converting service to map", e)
182274
}
183275
return service
184276
}
185-
186-
companion object {
187-
private const val SERVICE_FOUND = "serviceFound"
188-
private const val SERVICE_LOST = "serviceLost"
189-
const val NAME = "ServiceDiscovery"
190-
}
191277
}

0 commit comments

Comments
 (0)