@dawidzawada/bonjour-zeroconf 1.2.0 → 2.0.0

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.
Files changed (37) hide show
  1. package/README.md +25 -0
  2. package/android/src/main/java/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourZeroconf+AddressResolver.kt +32 -20
  3. package/android/src/main/java/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourZeroconf+Listeners.kt +4 -5
  4. package/android/src/main/java/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourZeroconf.kt +18 -16
  5. package/android/src/main/java/com/margelo/nitro/dawidzawada/bonjourzeroconf/Loggy.kt +16 -0
  6. package/ios/BonjourZeroconf+AddressResolver.swift +32 -32
  7. package/ios/BonjourZeroconf+Listeners.swift +5 -11
  8. package/ios/BonjourZeroconf.swift +66 -71
  9. package/ios/ListenerStore.swift +43 -0
  10. package/ios/Utils/Loggy.swift +21 -4
  11. package/lib/module/BonjourScanner.js +34 -0
  12. package/lib/module/BonjourScanner.js.map +1 -0
  13. package/lib/module/index.js +3 -3
  14. package/lib/module/index.js.map +1 -1
  15. package/lib/module/permissions.ios.js.map +1 -1
  16. package/lib/typescript/src/BonjourScanner.d.ts +20 -0
  17. package/lib/typescript/src/BonjourScanner.d.ts.map +1 -0
  18. package/lib/typescript/src/index.d.ts +3 -3
  19. package/lib/typescript/src/index.d.ts.map +1 -1
  20. package/lib/typescript/src/permissions.ios.d.ts +1 -1
  21. package/lib/typescript/src/permissions.ios.d.ts.map +1 -1
  22. package/lib/typescript/src/specs/BonjourZeroconf.nitro.d.ts +1 -0
  23. package/lib/typescript/src/specs/BonjourZeroconf.nitro.d.ts.map +1 -1
  24. package/nitrogen/generated/android/c++/JHybridBonjourZeroconfSpec.cpp +11 -2
  25. package/nitrogen/generated/android/c++/JHybridBonjourZeroconfSpec.hpp +2 -0
  26. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/HybridBonjourZeroconfSpec.kt +6 -0
  27. package/nitrogen/generated/ios/BonjourZeroconf-Swift-Cxx-Bridge.hpp +15 -15
  28. package/nitrogen/generated/ios/c++/HybridBonjourZeroconfSpecSwift.hpp +8 -1
  29. package/nitrogen/generated/ios/swift/HybridBonjourZeroconfSpec.swift +1 -0
  30. package/nitrogen/generated/ios/swift/HybridBonjourZeroconfSpec_cxx.swift +24 -0
  31. package/nitrogen/generated/shared/c++/HybridBonjourZeroconfSpec.cpp +2 -0
  32. package/nitrogen/generated/shared/c++/HybridBonjourZeroconfSpec.hpp +3 -1
  33. package/package.json +1 -1
  34. package/src/BonjourScanner.ts +59 -0
  35. package/src/index.ts +4 -4
  36. package/src/permissions.ios.ts +4 -1
  37. package/src/specs/BonjourZeroconf.nitro.ts +1 -0
package/README.md CHANGED
@@ -17,6 +17,7 @@ Discover devices and services on your local network using native Bonjour (iOS) a
17
17
  - 📡 **Cross-platform** – iOS (Bonjour) and Android (NSD)
18
18
  - 📱 **Managing iOS permissions** - no need for extra libraries or custom code, just use `requestLocalNetworkPermission` or `useLocalNetworkPermission` before scanning!
19
19
  - 🔄 **Real-time updates** – listen to scan results, state changes, and errors
20
+ - 🔭 **Multiple scanners** – run several independent scanners simultaneously for different service types
20
21
  - 🧩 **Expo compatible** - (config plugin coming soon)
21
22
 
22
23
  ## 📦 Installation
@@ -125,10 +126,33 @@ function App() {
125
126
 
126
127
  ---
127
128
 
129
+ ## 🔭 Multiple Scanners
130
+
131
+ For convenience, the library exports a `Scanner` singleton that covers most use cases. Each scanner can only scan one service type at a time, so if you need to discover multiple service types simultaneously, use the `BonjourScanner` class to create as many independent scanners as you need.
132
+
133
+ ```tsx
134
+ import { BonjourScanner } from '@dawidzawada/bonjour-zeroconf';
135
+
136
+ const printerScanner = new BonjourScanner({ id: 'printers' });
137
+ const httpScanner = new BonjourScanner({ id: 'http' });
138
+
139
+ // Both run in parallel, scanning different service types
140
+ printerScanner.scan('_printer._tcp', 'local');
141
+ httpScanner.scan('_http._tcp', 'local');
142
+ ```
143
+
144
+ The optional `id` is appended to log messages, making it easy to distinguish between scanners during debugging.
145
+
146
+ ---
147
+
128
148
  ## 📖 API Reference
129
149
 
130
150
  ### **Scanner**
131
151
 
152
+ A pre-created `BonjourScanner` singleton exported for convenience. Use it when you only need to scan one service type at a time.
153
+
154
+ ### **BonjourScanner**
155
+
132
156
  #### `scan(type: string, domain: string, options?: ScanOptions)`
133
157
 
134
158
  Start scanning for services.
@@ -273,6 +297,7 @@ interface ScanOptions {
273
297
  enum BonjourFail {
274
298
  DISCOVERY_FAILED = 'DISCOVERY_FAILED',
275
299
  RESOLVE_FAILED = 'RESOLVE_FAILED',
300
+ EXTRACTION_FAILED = 'EXTRACTION_FAILED',
276
301
  }
277
302
  ```
278
303
 
@@ -2,12 +2,14 @@ package com.margelo.nitro.dawidzawada.bonjourzeroconf
2
2
 
3
3
  import android.net.nsd.NsdManager
4
4
  import android.net.nsd.NsdServiceInfo
5
- import android.util.Log
6
5
  import androidx.annotation.RequiresApi
7
- import com.margelo.nitro.dawidzawada.bonjourzeroconf.BonjourZeroconf.Companion.TAG
6
+ import com.margelo.nitro.dawidzawada.bonjourzeroconf.BonjourZeroconf.Companion.legacyResolveMutex
7
+ import com.margelo.nitro.dawidzawada.bonjourzeroconf.BonjourZeroconf.Companion.loggy
8
8
  import kotlinx.coroutines.suspendCancellableCoroutine
9
9
  import kotlinx.coroutines.sync.withLock
10
10
  import kotlinx.coroutines.withTimeoutOrNull
11
+ import java.net.Inet6Address
12
+ import java.net.NetworkInterface
11
13
  import java.util.concurrent.Executors
12
14
 
13
15
  suspend fun BonjourZeroconf.resolveService(service: NsdServiceInfo, serviceKey: String, timeout: Long) {
@@ -40,19 +42,19 @@ suspend fun BonjourZeroconf.resolveServiceNew(service: NsdServiceInfo, serviceKe
40
42
  try {
41
43
  manager.unregisterServiceInfoCallback(this)
42
44
  } catch (e: Exception) {
43
- Log.e(TAG, "Error unregistering", e)
45
+ loggy.e("Error unregistering", id, e)
44
46
  }
45
47
  }
46
48
 
47
49
  override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {
48
- Log.e(TAG, "Registration failed: ${service.serviceName}, error: $errorCode")
50
+ loggy.e("Registration failed: ${service.serviceName}, error: $errorCode", id)
49
51
  notifyScanFailListeners(BonjourFail.RESOLVE_FAILED)
50
52
  unregisterCallback()
51
53
  continuation.resume(null) {}
52
54
  }
53
55
 
54
56
  override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
55
- Log.d(TAG, "Service updated: ${serviceInfo.serviceName}")
57
+ loggy.d("Service updated: ${serviceInfo.serviceName}", id)
56
58
  unregisterCallback()
57
59
 
58
60
  if (!_isScanning) {
@@ -64,11 +66,11 @@ suspend fun BonjourZeroconf.resolveServiceNew(service: NsdServiceInfo, serviceKe
64
66
  }
65
67
 
66
68
  override fun onServiceLost() {
67
- Log.d(TAG, "Service lost during resolution: ${service.serviceName}")
69
+ loggy.d("Service lost during resolution: ${service.serviceName}", id)
68
70
  }
69
71
 
70
72
  override fun onServiceInfoCallbackUnregistered() {
71
- Log.d(TAG, "Callback unregistered: ${service.serviceName}")
73
+ loggy.d("Callback unregistered: ${service.serviceName}", id)
72
74
  executor.shutdown()
73
75
  }
74
76
  }
@@ -80,13 +82,13 @@ suspend fun BonjourZeroconf.resolveServiceNew(service: NsdServiceInfo, serviceKe
80
82
  try {
81
83
  manager.unregisterServiceInfoCallback(callback)
82
84
  } catch (e: Exception) {
83
- Log.e(TAG, "Error unregistering on cancellation", e)
85
+ loggy.e("Error unregistering on cancellation", id, e)
84
86
  executor.shutdown()
85
87
  }
86
88
  }
87
89
  } catch (e: Exception) {
88
90
  notifyScanFailListeners(BonjourFail.RESOLVE_FAILED)
89
- Log.e(TAG, "Exception registering callback", e)
91
+ loggy.e("Exception registering callback", id, e)
90
92
  executor.shutdown()
91
93
  continuation.resume(null) {}
92
94
  }
@@ -98,11 +100,11 @@ suspend fun BonjourZeroconf.resolveServiceNew(service: NsdServiceInfo, serviceKe
98
100
  serviceCache[serviceKey] = scanResult
99
101
  notifyScanResultsListeners()
100
102
  }
101
- } ?: Log.w(TAG, "Failed to resolve service: $serviceKey")
103
+ } ?: loggy.w("Failed to resolve service: $serviceKey", id)
102
104
 
103
105
  } catch (e: Exception) {
104
106
  notifyScanFailListeners(BonjourFail.RESOLVE_FAILED)
105
- Log.e(TAG, "Error during service resolution", e)
107
+ loggy.e("Error during service resolution", id, e)
106
108
  }
107
109
  }
108
110
 
@@ -114,14 +116,14 @@ suspend fun BonjourZeroconf.resolveServiceLegacy(service: NsdServiceInfo, servic
114
116
  suspendCancellableCoroutine { continuation ->
115
117
  val resolveListener = object : NsdManager.ResolveListener {
116
118
  override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
117
- Log.e(TAG, "Resolve failed: ${serviceInfo.serviceName}, error: $errorCode")
119
+ loggy.e("Resolve failed: ${serviceInfo.serviceName}, error: $errorCode", id)
118
120
 
119
121
  notifyScanFailListeners(BonjourFail.RESOLVE_FAILED)
120
122
  continuation.resume(null) {}
121
123
  }
122
124
 
123
125
  override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
124
- Log.d(TAG, "Service resolved: ${serviceInfo.serviceName}")
126
+ loggy.d("Service resolved: ${serviceInfo.serviceName}", id)
125
127
 
126
128
  if (!_isScanning) {
127
129
  continuation.resume(null) {}
@@ -142,7 +144,7 @@ suspend fun BonjourZeroconf.resolveServiceLegacy(service: NsdServiceInfo, servic
142
144
  manager.resolveService(service, resolveListener)
143
145
  } catch (e: Exception) {
144
146
  notifyScanFailListeners(BonjourFail.RESOLVE_FAILED)
145
- Log.e(TAG, "Exception resolving service", e)
147
+ loggy.e("Exception resolving service", id, e)
146
148
  continuation.resume(null) {}
147
149
  }
148
150
  }
@@ -154,10 +156,10 @@ suspend fun BonjourZeroconf.resolveServiceLegacy(service: NsdServiceInfo, servic
154
156
  serviceCache[serviceKey] = scanResult
155
157
  notifyScanResultsListeners()
156
158
  }
157
- } ?: Log.w(TAG, "Failed to resolve service: $serviceKey")
159
+ } ?: loggy.w("Failed to resolve service: $serviceKey", id)
158
160
 
159
161
  } catch (e: Exception) {
160
- Log.e(TAG, "Error during service resolution", e)
162
+ loggy.e("Error during service resolution", id, e)
161
163
  }
162
164
  }
163
165
 
@@ -168,7 +170,7 @@ private fun BonjourZeroconf.extractScanResult(serviceInfo: NsdServiceInfo): Scan
168
170
 
169
171
  val (ipv4, ipv6) = when {
170
172
  host.address.size == 4 -> host.hostAddress to null
171
- host.address.size == 16 -> null to formatIPv6Address(host.address)
173
+ host.address.size == 16 -> null to formatIPv6Address(host.address, host as? Inet6Address)
172
174
  else -> null to null
173
175
  }
174
176
 
@@ -181,16 +183,26 @@ private fun BonjourZeroconf.extractScanResult(serviceInfo: NsdServiceInfo): Scan
181
183
  )
182
184
  } catch (e: Exception) {
183
185
  notifyScanFailListeners(BonjourFail.EXTRACTION_FAILED)
184
- Log.e(TAG, "Failed to extract scan result", e)
186
+ loggy.e("Failed to extract scan result", id, e)
185
187
  null
186
188
  }
187
189
  }
188
190
 
189
- private fun formatIPv6Address(bytes: ByteArray): String {
191
+ private fun formatIPv6Address(bytes: ByteArray, inet6Address: Inet6Address? = null): String {
190
192
  require(bytes.size == 16) { "IPv6 address must be 16 bytes" }
191
193
 
192
- return (0 until 16 step 2).joinToString(":") { i ->
194
+ val formatted = (0 until 16 step 2).joinToString(":") { i ->
193
195
  val segment = ((bytes[i].toInt() and 0xFF) shl 8) or (bytes[i + 1].toInt() and 0xFF)
194
196
  segment.toString(16)
195
197
  }
198
+
199
+ if (formatted.startsWith("fe80:") && inet6Address != null) {
200
+ val interfaceName = inet6Address.scopedInterface?.name
201
+ ?: NetworkInterface.getByIndex(inet6Address.scopeId)?.name
202
+ if (interfaceName != null) {
203
+ return "$formatted%$interfaceName"
204
+ }
205
+ }
206
+
207
+ return formatted
196
208
  }
@@ -1,7 +1,6 @@
1
1
  package com.margelo.nitro.dawidzawada.bonjourzeroconf
2
2
 
3
- import android.util.Log
4
- import com.margelo.nitro.dawidzawada.bonjourzeroconf.BonjourZeroconf.Companion.TAG
3
+ import com.margelo.nitro.dawidzawada.bonjourzeroconf.BonjourZeroconf.Companion.loggy
5
4
  import kotlinx.coroutines.Dispatchers
6
5
  import kotlinx.coroutines.launch
7
6
 
@@ -12,7 +11,7 @@ fun BonjourZeroconf.notifyScanResultsListeners() {
12
11
  try {
13
12
  listener(results)
14
13
  } catch (e: Exception) {
15
- Log.e(TAG, "Error notifying scan results listener", e)
14
+ loggy.e("Error notifying scan results listener", id, e)
16
15
  }
17
16
  }
18
17
  }
@@ -25,7 +24,7 @@ fun BonjourZeroconf.updateScanningState(newState: Boolean) {
25
24
  try {
26
25
  listener(newState)
27
26
  } catch (e: Exception) {
28
- Log.e(TAG, "Error notifying scan state listener", e)
27
+ loggy.e("Error notifying scan state listener", id, e)
29
28
  }
30
29
  }
31
30
  }
@@ -38,7 +37,7 @@ fun BonjourZeroconf.notifyScanFailListeners(fail: BonjourFail) {
38
37
  try {
39
38
  listener(fail)
40
39
  } catch (e: Exception) {
41
- Log.e(TAG, "Error notifying scan fail listener", e)
40
+ loggy.e("Error notifying scan fail listener", id, e)
42
41
  }
43
42
  }
44
43
  }
@@ -3,7 +3,6 @@ package com.margelo.nitro.dawidzawada.bonjourzeroconf
3
3
  import android.content.Context
4
4
  import android.net.nsd.NsdManager
5
5
  import android.net.nsd.NsdServiceInfo
6
- import android.util.Log
7
6
  import com.facebook.proguard.annotations.DoNotStrip
8
7
  import java.util.UUID
9
8
  import java.util.concurrent.ConcurrentHashMap
@@ -20,10 +19,13 @@ import kotlinx.coroutines.sync.Mutex
20
19
  class BonjourZeroconf : HybridBonjourZeroconfSpec() {
21
20
 
22
21
  companion object {
23
- internal const val TAG = "BonjourZeroconf"
24
22
  internal const val DEFAULT_RESOLVE_TIMEOUT_MS = 10_000L
23
+ internal val legacyResolveMutex = Mutex()
24
+ internal val loggy = Loggy(BonjourZeroconf::class)
25
25
  }
26
26
 
27
+ override var id: String? = null
28
+
27
29
  @Volatile
28
30
  internal var _isScanning = false
29
31
 
@@ -36,7 +38,6 @@ class BonjourZeroconf : HybridBonjourZeroconfSpec() {
36
38
  internal val serviceCache = ConcurrentHashMap<String, ScanResult>()
37
39
  internal val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
38
40
  internal val resolveScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
39
- internal val legacyResolveMutex = Mutex()
40
41
 
41
42
  override val isScanning: Boolean
42
43
  get() = _isScanning
@@ -59,12 +60,12 @@ class BonjourZeroconf : HybridBonjourZeroconfSpec() {
59
60
 
60
61
  currentDiscoveryListener = createDiscoveryListener(resolveTimeout).also { listener ->
61
62
  try {
62
- Log.i(TAG, "Starting scan for type: $type")
63
+ loggy.i("Starting scan for type: $type", id)
63
64
  updateScanningState(true)
64
65
  nsdManager?.discoverServices(type, NsdManager.PROTOCOL_DNS_SD, listener)
65
66
  ?: throw IllegalStateException("NsdManager is not initialized")
66
67
  } catch (e: Exception) {
67
- Log.e(TAG, "Failed to start discovery", e)
68
+ loggy.e("Failed to start discovery", id, e)
68
69
  updateScanningState(false)
69
70
  throw RuntimeException("Failed to start service discovery: ${e.message}", e)
70
71
  }
@@ -96,9 +97,9 @@ class BonjourZeroconf : HybridBonjourZeroconfSpec() {
96
97
  currentDiscoveryListener?.let { listener ->
97
98
  try {
98
99
  nsdManager?.stopServiceDiscovery(listener)
99
- Log.i(TAG, "Stopped service discovery")
100
+ loggy.i("Stopped service discovery", id)
100
101
  } catch (e: Exception) {
101
- Log.e(TAG, "Error stopping discovery", e)
102
+ loggy.e("Error stopping discovery", id, e)
102
103
  }
103
104
  }
104
105
 
@@ -116,7 +117,7 @@ class BonjourZeroconf : HybridBonjourZeroconfSpec() {
116
117
  try {
117
118
  onResult(currentResults)
118
119
  } catch (e: Exception) {
119
- Log.e(TAG, "Scan results listener error", e)
120
+ loggy.e("Scan results listener error", throwable = e)
120
121
  }
121
122
  }
122
123
  }
@@ -134,7 +135,7 @@ class BonjourZeroconf : HybridBonjourZeroconfSpec() {
134
135
  try {
135
136
  onChange(_isScanning)
136
137
  } catch (e: Exception) {
137
- Log.e(TAG, "Scan state listener error", e)
138
+ loggy.e("Scan state listener error", throwable = e)
138
139
  }
139
140
  }
140
141
 
@@ -155,12 +156,12 @@ class BonjourZeroconf : HybridBonjourZeroconfSpec() {
155
156
  private fun createDiscoveryListener(resolveTimeout: Long) = object : NsdManager.DiscoveryListener {
156
157
 
157
158
  override fun onDiscoveryStarted(serviceType: String) {
158
- Log.d(TAG, "Discovery started: $serviceType")
159
+ loggy.d("Discovery started: $serviceType", id)
159
160
  }
160
161
 
161
162
  override fun onServiceFound(service: NsdServiceInfo) {
162
163
  val serviceKey = createServiceKey(service)
163
- Log.d(TAG, "Service found: ${service.serviceName} (key: $serviceKey)")
164
+ loggy.d("Service found: ${service.serviceName} (key: $serviceKey)", id)
164
165
 
165
166
  if (serviceCache.containsKey(serviceKey)) {
166
167
  return
@@ -170,14 +171,14 @@ class BonjourZeroconf : HybridBonjourZeroconfSpec() {
170
171
  try {
171
172
  resolveService(service, serviceKey, resolveTimeout)
172
173
  } catch (e: Exception) {
173
- Log.e(TAG, "Error resolving service: $serviceKey", e)
174
+ loggy.e("Error resolving service: $serviceKey", id, e)
174
175
  }
175
176
  }
176
177
  }
177
178
 
178
179
  override fun onServiceLost(service: NsdServiceInfo) {
179
180
  val serviceKey = createServiceKey(service)
180
- Log.d(TAG, "Service lost: ${service.serviceName} (key: $serviceKey)")
181
+ loggy.d("Service lost: ${service.serviceName} (key: $serviceKey)", id)
181
182
 
182
183
  serviceCache.remove(serviceKey)?.let {
183
184
  notifyScanResultsListeners()
@@ -185,19 +186,19 @@ class BonjourZeroconf : HybridBonjourZeroconfSpec() {
185
186
  }
186
187
 
187
188
  override fun onDiscoveryStopped(serviceType: String) {
188
- Log.d(TAG, "Discovery stopped: $serviceType")
189
+ loggy.d("Discovery stopped: $serviceType", id)
189
190
  updateScanningState(false)
190
191
  }
191
192
 
192
193
  override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
193
- Log.e(TAG, "Start discovery failed: $serviceType, error: $errorCode")
194
+ loggy.e("Start discovery failed: $serviceType, error: $errorCode", id)
194
195
  notifyScanFailListeners(BonjourFail.DISCOVERY_FAILED)
195
196
  updateScanningState(false)
196
197
  }
197
198
 
198
199
  override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
199
200
  notifyScanFailListeners(BonjourFail.DISCOVERY_FAILED)
200
- Log.e(TAG, "Stop discovery failed: $serviceType, error: $errorCode")
201
+ loggy.e("Stop discovery failed: $serviceType, error: $errorCode", id)
201
202
  }
202
203
  }
203
204
 
@@ -205,3 +206,4 @@ class BonjourZeroconf : HybridBonjourZeroconfSpec() {
205
206
  return "${service.serviceName}.${service.serviceType}"
206
207
  }
207
208
  }
209
+
@@ -0,0 +1,16 @@
1
+ package com.margelo.nitro.dawidzawada.bonjourzeroconf
2
+
3
+ import android.util.Log
4
+ import kotlin.reflect.KClass
5
+
6
+ internal class Loggy(clazz: KClass<*>) {
7
+ private val tag = clazz.simpleName ?: "Unknown"
8
+
9
+ private fun format(message: String, id: String?): String =
10
+ if (id != null) "($id) $message" else message
11
+
12
+ fun d(message: String, id: String? = null) = Log.d(tag, format(message, id))
13
+ fun i(message: String, id: String? = null) = Log.i(tag, format(message, id))
14
+ fun w(message: String, id: String? = null) = Log.w(tag, format(message, id))
15
+ fun e(message: String, id: String? = null, throwable: Throwable? = null) = Log.e(tag, format(message, id), throwable)
16
+ }
@@ -10,11 +10,11 @@ extension BonjourZeroconf {
10
10
  /// Resolve a service to get its IP address and port using async/await
11
11
  internal func resolveService(result: NWBrowser.Result, name: String, timeout: TimeInterval) async -> ScanResult? {
12
12
  let taskId = UUID()
13
-
13
+
14
14
  do {
15
15
  return try await withCheckedThrowingContinuation { continuation in
16
16
  let connection = NWConnection(to: result.endpoint, using: .tcp)
17
-
17
+
18
18
  resolveLock.lock()
19
19
  guard _isScanning else {
20
20
  resolveLock.unlock()
@@ -23,25 +23,25 @@ extension BonjourZeroconf {
23
23
  }
24
24
  activeConnections[taskId] = connection
25
25
  resolveLock.unlock()
26
-
26
+
27
27
  final class ResumeBox { var hasResumed = false }
28
28
  let box = ResumeBox()
29
-
29
+
30
30
  let timeoutTask = DispatchWorkItem { [weak self] in
31
31
  guard let self = self else { return }
32
32
  guard !box.hasResumed else { return }
33
33
  box.hasResumed = true
34
- Loggy.log(.debug, message: "Timeout resolving \(name)")
34
+ Loggy.log(.debug, id: self.id, message: "Timeout resolving \(name)")
35
35
  continuation.resume(throwing: AddressResolverError.timeout)
36
36
  self.cleanupResolve(connection: connection, taskId: taskId)
37
37
  }
38
-
38
+
39
39
  resolveLock.lock()
40
40
  activeTimeouts[taskId] = timeoutTask
41
41
  resolveLock.unlock()
42
-
42
+
43
43
  networkQueue.asyncAfter(deadline: .now() + timeout, execute: timeoutTask)
44
-
44
+
45
45
  connection.stateUpdateHandler = { [weak self] state in
46
46
  guard let self = self else { return }
47
47
  switch state {
@@ -49,7 +49,7 @@ extension BonjourZeroconf {
49
49
  timeoutTask.cancel()
50
50
  guard !box.hasResumed else { return }
51
51
  box.hasResumed = true
52
-
52
+
53
53
  if let remoteEndpoint = connection.currentPath?.remoteEndpoint,
54
54
  let scanResult = self.extractIPAndPort(from: remoteEndpoint, serviceName: name) {
55
55
  continuation.resume(returning: scanResult)
@@ -57,26 +57,26 @@ extension BonjourZeroconf {
57
57
  continuation.resume(throwing: AddressResolverError.extractionFailed)
58
58
  }
59
59
  self.cleanupResolve(connection: connection, taskId: taskId)
60
-
60
+
61
61
  case .failed(let error):
62
62
  timeoutTask.cancel()
63
63
  guard !box.hasResumed else { return }
64
64
  box.hasResumed = true
65
- Loggy.log(.error, message: "Failed to resolve service \(name): \(error.localizedDescription)")
65
+ Loggy.log(.error, id: self.id, message: "Failed to resolve service \(name): \(error.localizedDescription)")
66
66
  continuation.resume(throwing: error)
67
67
  self.cleanupResolve(connection: connection, taskId: taskId)
68
-
68
+
69
69
  case .waiting(let error):
70
- Loggy.log(.debug, message: "Connection waiting for \(name): \(error.localizedDescription)")
71
-
70
+ Loggy.log(.debug, id: self.id, message: "Connection waiting for \(name): \(error.localizedDescription)")
71
+
72
72
  case .cancelled:
73
73
  break
74
-
74
+
75
75
  default:
76
76
  break
77
77
  }
78
78
  }
79
-
79
+
80
80
  connection.start(queue: networkQueue)
81
81
  }
82
82
  } catch let error as AddressResolverError {
@@ -86,7 +86,7 @@ extension BonjourZeroconf {
86
86
  case .extractionFailed:
87
87
  notifyScanFailListeners(with: BonjourFail.extractionFailed)
88
88
  case .cancelled:
89
- Loggy.log(.debug, message: "Scanning stopped, cancelling address resolution")
89
+ Loggy.log(.debug, id: self.id, message: "Scanning stopped, cancelling address resolution")
90
90
  break
91
91
  }
92
92
  return nil
@@ -94,10 +94,10 @@ extension BonjourZeroconf {
94
94
  return nil
95
95
  }
96
96
  }
97
-
97
+
98
98
  /// Cancels ongoing address resolve process
99
99
  internal func cancelAddressResolving() {
100
- Loggy.log(.debug, message: "Cancelling Address Resolving")
100
+ Loggy.log(.debug, id: id, message: "Cancelling Address Resolving")
101
101
  resolveLock.lock()
102
102
  activeTimeouts.values.forEach { $0.cancel() }
103
103
  activeTimeouts.removeAll()
@@ -114,7 +114,7 @@ extension BonjourZeroconf {
114
114
  activeTimeouts.removeValue(forKey: taskId)
115
115
  resolveLock.unlock()
116
116
  }
117
-
117
+
118
118
  /// Extract IP address and port from an endpoint
119
119
  private func extractIPAndPort(from endpoint: NWEndpoint, serviceName: String) -> ScanResult? {
120
120
  switch endpoint {
@@ -123,17 +123,17 @@ extension BonjourZeroconf {
123
123
  var ipv6: String?
124
124
  var hostname: String?
125
125
  let portNumber = Int(port.rawValue)
126
-
126
+
127
127
  switch host {
128
128
  case .ipv4(let address):
129
129
  ipv4 = address.rawValue.map(String.init).joined(separator: ".")
130
- Loggy.log(.debug, message: "Resolved \(serviceName) -> IPv4: \(ipv4!), Port: \(portNumber)")
131
-
130
+ Loggy.log(.debug, id: id, message: "Resolved \(serviceName) -> IPv4: \(ipv4!), Port: \(portNumber)")
131
+
132
132
  case .ipv6(let address):
133
133
  let formatted = stride(from: 0, to: address.rawValue.count, by: 2).map { i in
134
134
  String(format: "%02x%02x", address.rawValue[i], address.rawValue[i + 1])
135
135
  }.joined(separator: ":")
136
-
136
+
137
137
  if formatted.hasPrefix("fe80:") {
138
138
  if let interface = endpoint.interface?.name {
139
139
  ipv6 = "\(formatted)%\(interface)"
@@ -143,17 +143,17 @@ extension BonjourZeroconf {
143
143
  } else {
144
144
  ipv6 = formatted
145
145
  }
146
- Loggy.log(.debug, message: "Resolved \(serviceName) -> IPv6: \(ipv6!), Port: \(portNumber)")
147
-
146
+ Loggy.log(.debug, id: id, message: "Resolved \(serviceName) -> IPv6: \(ipv6!), Port: \(portNumber)")
147
+
148
148
  case .name(let name, _):
149
149
  hostname = name
150
- Loggy.log(.debug, message: "Resolved \(serviceName) -> Hostname: \(hostname ?? "nil"), Port: \(portNumber)")
151
-
150
+ Loggy.log(.debug, id: id, message: "Resolved \(serviceName) -> Hostname: \(hostname ?? "nil"), Port: \(portNumber)")
151
+
152
152
  @unknown default:
153
- Loggy.log(.debug, message: "Unknown host type for \(serviceName)")
153
+ Loggy.log(.debug, id: id, message: "Unknown host type for \(serviceName)")
154
154
  return nil
155
155
  }
156
-
156
+
157
157
  return ScanResult(
158
158
  name: serviceName,
159
159
  ipv4: ipv4,
@@ -161,9 +161,9 @@ extension BonjourZeroconf {
161
161
  hostname: hostname,
162
162
  port: Double(portNumber)
163
163
  )
164
-
164
+
165
165
  default:
166
- Loggy.log(.warning, message: "Unexpected endpoint format for \(serviceName)")
166
+ Loggy.log(.warning, id: id, message: "Unexpected endpoint format for \(serviceName)")
167
167
  return nil
168
168
  }
169
169
  }
@@ -7,20 +7,14 @@
7
7
 
8
8
  extension BonjourZeroconf {
9
9
  internal func notifyScanResultsListeners(with results: [ScanResult]) {
10
- for listener in scanResultsListeners.values {
11
- listener(results)
12
- }
10
+ Task { await listenerStore.notifyScanResults(with: results) }
13
11
  }
14
-
12
+
15
13
  internal func notifyScanStateListeners(with isScanningState: Bool) {
16
- for listener in scanStateListeners.values {
17
- listener(isScanningState)
18
- }
14
+ Task { await listenerStore.notifyScanState(with: isScanningState) }
19
15
  }
20
-
16
+
21
17
  internal func notifyScanFailListeners(with fail: BonjourFail) {
22
- for listener in scanFailListeners.values {
23
- listener(fail)
24
- }
18
+ Task { await listenerStore.notifyScanFail(with: fail) }
25
19
  }
26
20
  }