@dawidzawada/bonjour-zeroconf 1.0.0 → 1.1.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 (66) hide show
  1. package/README.md +274 -10
  2. package/android/src/main/java/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourZeroconf+AddressResolver.kt +14 -0
  3. package/android/src/main/java/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourZeroconf.kt +25 -1
  4. package/ios/AddressResolverError.swift +13 -10
  5. package/ios/BonjourZeroconf+AddressResolver.swift +58 -21
  6. package/ios/BonjourZeroconf.swift +21 -0
  7. package/ios/LocalNetworkAuthorization.swift +0 -1
  8. package/lib/typescript/src/specs/BonjourZeroconf.nitro.d.ts +1 -0
  9. package/lib/typescript/src/specs/BonjourZeroconf.nitro.d.ts.map +1 -1
  10. package/nitrogen/generated/android/c++/JBonjourFail.hpp +1 -1
  11. package/nitrogen/generated/android/c++/JBonjourListener.hpp +1 -1
  12. package/nitrogen/generated/android/c++/JFunc_void.hpp +1 -1
  13. package/nitrogen/generated/android/c++/JFunc_void_BonjourFail.hpp +1 -1
  14. package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +1 -1
  15. package/nitrogen/generated/android/c++/JFunc_void_std__vector_ScanResult_.hpp +1 -1
  16. package/nitrogen/generated/android/c++/JHybridBonjourZeroconfSpec.cpp +35 -8
  17. package/nitrogen/generated/android/c++/JHybridBonjourZeroconfSpec.hpp +2 -1
  18. package/nitrogen/generated/android/c++/JScanOptions.hpp +1 -1
  19. package/nitrogen/generated/android/c++/JScanResult.hpp +1 -1
  20. package/nitrogen/generated/android/dawidzawada_bonjourzeroconf+autolinking.cmake +1 -1
  21. package/nitrogen/generated/android/dawidzawada_bonjourzeroconf+autolinking.gradle +1 -1
  22. package/nitrogen/generated/android/dawidzawada_bonjourzeroconfOnLoad.cpp +1 -1
  23. package/nitrogen/generated/android/dawidzawada_bonjourzeroconfOnLoad.hpp +1 -1
  24. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourFail.kt +1 -1
  25. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourListener.kt +1 -1
  26. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/Func_void.kt +1 -1
  27. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/Func_void_BonjourFail.kt +1 -1
  28. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/Func_void_bool.kt +1 -1
  29. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/Func_void_std__vector_ScanResult_.kt +1 -1
  30. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/HybridBonjourZeroconfSpec.kt +6 -1
  31. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/ScanOptions.kt +1 -1
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/ScanResult.kt +1 -1
  33. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/dawidzawada_bonjourzeroconfOnLoad.kt +1 -1
  34. package/nitrogen/generated/ios/BonjourZeroconf+autolinking.rb +1 -1
  35. package/nitrogen/generated/ios/BonjourZeroconf-Swift-Cxx-Bridge.cpp +17 -17
  36. package/nitrogen/generated/ios/BonjourZeroconf-Swift-Cxx-Bridge.hpp +73 -52
  37. package/nitrogen/generated/ios/BonjourZeroconf-Swift-Cxx-Umbrella.hpp +1 -1
  38. package/nitrogen/generated/ios/BonjourZeroconfAutolinking.mm +1 -1
  39. package/nitrogen/generated/ios/BonjourZeroconfAutolinking.swift +1 -1
  40. package/nitrogen/generated/ios/c++/HybridBonjourZeroconfSpecSwift.cpp +1 -1
  41. package/nitrogen/generated/ios/c++/HybridBonjourZeroconfSpecSwift.hpp +14 -5
  42. package/nitrogen/generated/ios/c++/HybridLocalNetworkPermissionSpecSwift.cpp +1 -1
  43. package/nitrogen/generated/ios/c++/HybridLocalNetworkPermissionSpecSwift.hpp +1 -1
  44. package/nitrogen/generated/ios/swift/BonjourFail.swift +1 -1
  45. package/nitrogen/generated/ios/swift/BonjourListener.swift +1 -1
  46. package/nitrogen/generated/ios/swift/Func_void.swift +1 -1
  47. package/nitrogen/generated/ios/swift/Func_void_BonjourFail.swift +1 -1
  48. package/nitrogen/generated/ios/swift/Func_void_bool.swift +1 -1
  49. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +1 -1
  50. package/nitrogen/generated/ios/swift/Func_void_std__vector_ScanResult_.swift +6 -6
  51. package/nitrogen/generated/ios/swift/HybridBonjourZeroconfSpec.swift +2 -1
  52. package/nitrogen/generated/ios/swift/HybridBonjourZeroconfSpec_cxx.swift +26 -1
  53. package/nitrogen/generated/ios/swift/HybridLocalNetworkPermissionSpec.swift +1 -1
  54. package/nitrogen/generated/ios/swift/HybridLocalNetworkPermissionSpec_cxx.swift +1 -1
  55. package/nitrogen/generated/ios/swift/ScanOptions.swift +1 -1
  56. package/nitrogen/generated/ios/swift/ScanResult.swift +1 -1
  57. package/nitrogen/generated/shared/c++/BonjourFail.hpp +1 -1
  58. package/nitrogen/generated/shared/c++/BonjourListener.hpp +1 -1
  59. package/nitrogen/generated/shared/c++/HybridBonjourZeroconfSpec.cpp +2 -1
  60. package/nitrogen/generated/shared/c++/HybridBonjourZeroconfSpec.hpp +6 -4
  61. package/nitrogen/generated/shared/c++/HybridLocalNetworkPermissionSpec.cpp +1 -1
  62. package/nitrogen/generated/shared/c++/HybridLocalNetworkPermissionSpec.hpp +1 -1
  63. package/nitrogen/generated/shared/c++/ScanOptions.hpp +1 -1
  64. package/nitrogen/generated/shared/c++/ScanResult.hpp +1 -1
  65. package/package.json +18 -2
  66. package/src/specs/BonjourZeroconf.nitro.ts +7 -0
package/README.md CHANGED
@@ -1,23 +1,279 @@
1
- # @dawidzawada/bonjour-zeroconf
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="./docs/banner-dark.png" />
3
+ <source media="(prefers-color-scheme: light)" srcset="./docs/banner-light.png" />
4
+ <img alt="BonjourZeroconf" src="./docs/banner-light.png" />
5
+ </picture>
2
6
 
3
- Zeroconf devices scanner using Bonjour (iOS) and NSD (Android) for React Native & Expo apps. Powered by Nitro Modules.
7
+ # Bonjour Zeroconf 🇫🇷🥖
4
8
 
5
- ## Installation
9
+ **High-performance Zeroconf/mDNS service discovery for React Native**
10
+
11
+ Discover devices and services on your local network using native Bonjour (iOS) and NSD (Android) APIs. Built with [Nitro Modules](https://nitro.margelo.com/) for maximum performance. Designed for both React Native and Expo. 🧑‍🚀
12
+
13
+ ## ✨ Features
14
+
15
+ - 🏎️ **Racecar performance** – powered by Nitro Modules
16
+ - 🛡️ **Type-safe** – thanks to Nitro & Nitrogen
17
+ - 📡 **Cross-platform** – iOS (Bonjour) and Android (NSD)
18
+ - 📱 **Managing iOS permissions** - no need for extra libraries or custom code, just use `requestLocalNetworkPermission` or `useLocalNetworkPermission` before scanning!
19
+ - 🔄 **Real-time updates** – listen to scan results, state changes, and errors
20
+ - 🧩 **Expo compatible** - (config plugin coming soon)
21
+
22
+ ## 📦 Installation
6
23
 
7
24
  ```sh
8
25
  npm install @dawidzawada/bonjour-zeroconf react-native-nitro-modules
26
+ ```
27
+
28
+ > **Note:** `react-native-nitro-modules` is required as a peer dependency.
29
+
30
+ ## ⚙️ iOS Setup
31
+
32
+ On iOS we need to ask for permissions and configure services we want to scan.
33
+
34
+ ### Expo:
35
+
36
+ Add this to your `app.json`, `app.config.json` or `app.config.js`:
37
+
38
+ ```ts
39
+ {
40
+ ios: {
41
+ infoPlist: {
42
+ NSLocalNetworkUsageDescription:
43
+ 'This app needs local network access to discover devices',
44
+ NSBonjourServices: ['_bonjour._tcp', '_lnp._tcp.'],
45
+ },
46
+ },
47
+ }
48
+ // Add service types you want to scan to NSBonjourServices, first two service types are needed for permissions
49
+ ```
50
+
51
+ Run prebuild command:
52
+
53
+ ```sh
54
+ npx expo prebuild
55
+ ```
56
+
57
+ ### React Native:
58
+
59
+ Add this to your `Info.plist`:
60
+
61
+ ```xml
62
+ <key>NSLocalNetworkUsageDescription</key>
63
+ <string>This app needs local network access to discover devices</string>
64
+ <key>NSBonjourServices</key>
65
+ <array>
66
+ <!-- Needed for permissions -->
67
+ <string>_bonjour._tcp</string>
68
+ <string>_lnp._tcp</string>
69
+ <!-- Add other service types you need here -->
70
+ </array>
71
+ ```
72
+
73
+ ## 🚀 Quick Start
74
+
75
+ ```tsx
76
+ import {
77
+ Scanner,
78
+ useIsScanning,
79
+ type ScanResult,
80
+ } from '@dawidzawada/bonjour-zeroconf';
81
+ import { useEffect, useState } from 'react';
82
+
83
+ function App() {
84
+ const [devices, setDevices] = useState<ScanResult[]>([]);
9
85
 
10
- > `react-native-nitro-modules` is required as this library relies on [Nitro Modules](https://nitro.margelo.com/).
86
+ const handleScan = async () => {
87
+ const granted = await requestLocalNetworkPermission();
88
+ if (granted) {
89
+ Scanner.scan('_bonjour._tcp', 'local');
90
+ }
91
+ };
92
+
93
+ const handleStop = async () => {
94
+ Scanner.stop();
95
+ };
96
+
97
+ const handleCheck = async () => {
98
+ Alert.alert(`Is scanning? ${Scanner.isScanning}`);
99
+ };
100
+
101
+ useEffect(() => {
102
+ // Listen for discovered devices
103
+ const { remove } = Scanner.listenForScanResults((scan) => {
104
+ setResults(scan);
105
+ });
106
+
107
+ return () => {
108
+ remove();
109
+ };
110
+ }, []);
111
+
112
+ return (
113
+ <View>
114
+ <Button title={'Scan'} onPress={handleScan} />
115
+ <Button title={'Stop'} onPress={handleStop} />
116
+ {devices.map((device) => (
117
+ <Text key={device.name}>
118
+ {device.name} - {device.ipv4}:{device.port}
119
+ </Text>
120
+ ))}
121
+ </View>
122
+ );
123
+ }
11
124
  ```
12
125
 
13
- ## Usage
126
+ ---
127
+
128
+ ## 📖 API Reference
129
+
130
+ ### **Scanner**
131
+
132
+ #### `scan(type: string, domain: string, options?: ScanOptions)`
14
133
 
15
- ```js
16
- import { Scanner } from '@dawidzawada/bonjour-zeroconf';
134
+ Start scanning for services.
17
135
 
18
- // ...
136
+ ```ts
137
+ Scanner.scan('_http._tcp', 'local');
138
+ ```
19
139
 
20
- Scanner.scan('_bonjour._tcp', 'local');
140
+ ```ts
141
+ Scanner.scan('_printer._tcp', 'local', {
142
+ addressResolveTimeout: 10000, // ms
143
+ });
144
+ ```
145
+
146
+ **Common service types:**
147
+
148
+ - `_http._tcp` – HTTP servers
149
+ - `_ssh._tcp` – SSH servers
150
+ - `_airplay._tcp` – AirPlay devices
151
+ - `_printer._tcp` – Network printers
152
+
153
+ #### `scanFor(time: number, type: string, domain: string, options?: ScanOptions)`
154
+
155
+ Scan for services for a specified duration and return results as a Promise.
156
+
157
+ ```ts
158
+ const devices = await Scanner.scanFor(15, '_http._tcp', 'local');
159
+ console.log('Found devices:', devices);
160
+ ```
161
+
162
+ ```ts
163
+ const devices = await Scanner.scanFor(25, '_printer._tcp', 'local', {
164
+ addressResolveTimeout: 10000, // ms
165
+ });
166
+ ```
167
+
168
+ **Parameters:**
169
+
170
+ - `time` – Duration in seconds to scan before stopping
171
+ - `type` – Service type to discover
172
+ - `domain` – Domain to scan (typically `'local'`)
173
+ - `options` – Optional scan configuration
174
+
175
+ **Returns:** `Promise<ScanResult[]>` – Array of discovered services
176
+
177
+ #### `stop()`
178
+
179
+ Stop scanning and clear cached results.
180
+
181
+ ```ts
182
+ Scanner.stop();
183
+ ```
184
+
185
+ #### `listenForScanResults(callback)`
186
+
187
+ Listen for discovered services.
188
+
189
+ ```ts
190
+ const listener = Scanner.listenForScanResults((results: ScanResult[]) => {
191
+ console.log('Found devices:', results);
192
+ });
193
+
194
+ // Clean up listener
195
+ listener.remove();
196
+ ```
197
+
198
+ #### `listenForScanState(callback)`
199
+
200
+ Listen for scanning state changes.
201
+
202
+ ```ts
203
+ const listener = Scanner.listenForScanState((isScanning: boolean) => {
204
+ console.log('Scanning:', isScanning);
205
+ });
206
+
207
+ // Clean up listener
208
+ listener.remove();
209
+ ```
210
+
211
+ #### `listenForScanFail(callback)`
212
+
213
+ Listen for scan failures.
214
+
215
+ ```ts
216
+ const listener = Scanner.listenForScanFail((error: BonjourFail) => {
217
+ console.log('Scan failed:', error);
218
+ });
219
+
220
+ // Clean up listener
221
+ listener.remove();
222
+ ```
223
+
224
+ ---
225
+
226
+ ### **Hooks**
227
+
228
+ #### `useIsScanning()`
229
+
230
+ React hook that returns the current scanning state.
231
+
232
+ ```tsx
233
+ const isScanning = useIsScanning();
234
+ ```
235
+
236
+ #### `useLocalNetworkPermission()` (iOS only)
237
+
238
+ React hook for managing local network permission.
239
+
240
+ ```tsx
241
+ const { status, request } = useLocalNetworkPermission();
242
+ ```
243
+
244
+ ---
245
+
246
+ ### **Functions**
247
+
248
+ #### `requestLocalNetworkPermission()`
249
+
250
+ Displays prompt to request local network permission, always returns `true` on Android.
251
+
252
+ ```tsx
253
+ const granted = await requestLocalNetworkPermission();
254
+ ```
255
+
256
+ ---
257
+
258
+ ### **Types**
259
+
260
+ ```ts
261
+ interface ScanResult {
262
+ name: string;
263
+ ipv4?: string;
264
+ ipv6?: string;
265
+ hostname?: string;
266
+ port?: number;
267
+ }
268
+
269
+ interface ScanOptions {
270
+ addressResolveTimeout?: number; // milliseconds, default: 10000
271
+ }
272
+
273
+ enum BonjourFail {
274
+ DISCOVERY_FAILED = 'DISCOVERY_FAILED',
275
+ RESOLVE_FAILED = 'RESOLVE_FAILED',
276
+ }
21
277
  ```
22
278
 
23
279
  ## Contributing
@@ -26,10 +282,18 @@ Scanner.scan('_bonjour._tcp', 'local');
26
282
  - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
27
283
  - [Code of conduct](CODE_OF_CONDUCT.md)
28
284
 
285
+ ## Credits
286
+
287
+ - [Nitro Modules](https://nitro.margelo.com/) - High-performance native module framework
288
+ - [mrousavy](https://github.com/mrousavy) - Creator of Nitro Modules
289
+ - [react-native-builder-bob](https://github.com/callstack/react-native-builder-bob) - Library template
290
+
291
+ Solution for handling permissions is based on [react-native-local-network-permission](https://github.com/neurio/react-native-local-network-permission)
292
+
29
293
  ## License
30
294
 
31
295
  MIT
32
296
 
33
297
  ---
34
298
 
35
- Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
299
+ **Made with ❤️ for the React Native community**
@@ -11,6 +11,8 @@ import kotlinx.coroutines.withTimeoutOrNull
11
11
  import java.util.concurrent.Executors
12
12
 
13
13
  suspend fun BonjourZeroconf.resolveService(service: NsdServiceInfo, serviceKey: String, timeout: Long) {
14
+ if (!_isScanning) return
15
+
14
16
  if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
15
17
  // New API (Android 14+)
16
18
  resolveServiceNew(service, serviceKey, timeout)
@@ -52,6 +54,12 @@ suspend fun BonjourZeroconf.resolveServiceNew(service: NsdServiceInfo, serviceKe
52
54
  override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
53
55
  Log.d(TAG, "Service updated: ${serviceInfo.serviceName}")
54
56
  unregisterCallback()
57
+
58
+ if (!_isScanning) {
59
+ continuation.resume(null) {}
60
+ return
61
+ }
62
+
55
63
  continuation.resume(serviceInfo) {}
56
64
  }
57
65
 
@@ -114,6 +122,12 @@ suspend fun BonjourZeroconf.resolveServiceLegacy(service: NsdServiceInfo, servic
114
122
 
115
123
  override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
116
124
  Log.d(TAG, "Service resolved: ${serviceInfo.serviceName}")
125
+
126
+ if (!_isScanning) {
127
+ continuation.resume(null) {}
128
+ return
129
+ }
130
+
117
131
  continuation.resume(serviceInfo) {}
118
132
  }
119
133
  }
@@ -8,9 +8,11 @@ import com.facebook.proguard.annotations.DoNotStrip
8
8
  import java.util.UUID
9
9
  import java.util.concurrent.ConcurrentHashMap
10
10
  import com.margelo.nitro.NitroModules
11
+ import com.margelo.nitro.core.Promise
11
12
  import kotlinx.coroutines.CoroutineScope
12
13
  import kotlinx.coroutines.Dispatchers
13
14
  import kotlinx.coroutines.SupervisorJob
15
+ import kotlinx.coroutines.cancelChildren
14
16
  import kotlinx.coroutines.launch
15
17
  import kotlinx.coroutines.sync.Mutex
16
18
 
@@ -33,6 +35,7 @@ class BonjourZeroconf : HybridBonjourZeroconfSpec() {
33
35
  internal var currentDiscoveryListener: NsdManager.DiscoveryListener? = null
34
36
  internal val serviceCache = ConcurrentHashMap<String, ScanResult>()
35
37
  internal val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
38
+ internal val resolveScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
36
39
  internal val legacyResolveMutex = Mutex()
37
40
 
38
41
  override val isScanning: Boolean
@@ -68,7 +71,28 @@ class BonjourZeroconf : HybridBonjourZeroconfSpec() {
68
71
  }
69
72
  }
70
73
 
74
+ override fun scanFor(
75
+ time: Double,
76
+ type: String,
77
+ domain: String,
78
+ options: ScanOptions?
79
+ ): Promise<Array<ScanResult>> {
80
+ return Promise.async {
81
+ scan(type, domain, options)
82
+
83
+ kotlinx.coroutines.delay((time * 1000).toLong())
84
+
85
+ val results = serviceCache.values.toTypedArray()
86
+
87
+ stop()
88
+
89
+ results
90
+ }
91
+ }
92
+
71
93
  override fun stop() {
94
+ resolveScope.coroutineContext.cancelChildren()
95
+
72
96
  currentDiscoveryListener?.let { listener ->
73
97
  try {
74
98
  nsdManager?.stopServiceDiscovery(listener)
@@ -142,7 +166,7 @@ class BonjourZeroconf : HybridBonjourZeroconfSpec() {
142
166
  return
143
167
  }
144
168
 
145
- scope.launch {
169
+ resolveScope.launch {
146
170
  try {
147
171
  resolveService(service, serviceKey, resolveTimeout)
148
172
  } catch (e: Exception) {
@@ -6,15 +6,18 @@
6
6
  //
7
7
 
8
8
  enum AddressResolverError: Error {
9
- case timeout
10
- case extractionFailed
11
-
12
- var localizedDescription: String {
13
- switch self {
14
- case .timeout:
15
- return "Connection timed out"
16
- case .extractionFailed:
17
- return "Failed to extract IP and port information"
18
- }
9
+ case timeout
10
+ case extractionFailed
11
+ case cancelled
12
+
13
+ var localizedDescription: String {
14
+ switch self {
15
+ case .timeout:
16
+ return "Connection timed out"
17
+ case .extractionFailed:
18
+ return "Failed to extract IP and port information"
19
+ case .cancelled:
20
+ return "Cancelled resolve process"
19
21
  }
22
+ }
20
23
  }
@@ -9,25 +9,41 @@ import Network
9
9
  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
+ let taskId = UUID()
13
+
12
14
  do {
13
15
  return try await withCheckedThrowingContinuation { continuation in
14
16
  let connection = NWConnection(to: result.endpoint, using: .tcp)
15
17
 
16
- final class ResumeBox {
17
- var hasResumed = false
18
+ resolveLock.lock()
19
+ guard _isScanning else {
20
+ resolveLock.unlock()
21
+ continuation.resume(throwing: AddressResolverError.cancelled)
22
+ return
18
23
  }
24
+ activeConnections[taskId] = connection
25
+ resolveLock.unlock()
26
+
27
+ final class ResumeBox { var hasResumed = false }
19
28
  let box = ResumeBox()
20
29
 
21
- let timeoutTask = DispatchWorkItem {
30
+ let timeoutTask = DispatchWorkItem { [weak self] in
31
+ guard let self = self else { return }
22
32
  guard !box.hasResumed else { return }
23
33
  box.hasResumed = true
24
34
  Loggy.log(.debug, message: "Timeout resolving \(name)")
25
35
  continuation.resume(throwing: AddressResolverError.timeout)
26
- connection.cancel()
36
+ self.cleanupResolve(connection: connection, taskId: taskId)
27
37
  }
38
+
39
+ resolveLock.lock()
40
+ activeTimeouts[taskId] = timeoutTask
41
+ resolveLock.unlock()
42
+
28
43
  networkQueue.asyncAfter(deadline: .now() + timeout, execute: timeoutTask)
29
44
 
30
45
  connection.stateUpdateHandler = { [weak self] state in
46
+ guard let self = self else { return }
31
47
  switch state {
32
48
  case .ready:
33
49
  timeoutTask.cancel()
@@ -35,12 +51,12 @@ extension BonjourZeroconf {
35
51
  box.hasResumed = true
36
52
 
37
53
  if let remoteEndpoint = connection.currentPath?.remoteEndpoint,
38
- let scanResult = self?.extractIPAndPort(from: remoteEndpoint, serviceName: name) {
54
+ let scanResult = self.extractIPAndPort(from: remoteEndpoint, serviceName: name) {
39
55
  continuation.resume(returning: scanResult)
40
56
  } else {
41
57
  continuation.resume(throwing: AddressResolverError.extractionFailed)
42
58
  }
43
- connection.cancel()
59
+ self.cleanupResolve(connection: connection, taskId: taskId)
44
60
 
45
61
  case .failed(let error):
46
62
  timeoutTask.cancel()
@@ -48,7 +64,7 @@ extension BonjourZeroconf {
48
64
  box.hasResumed = true
49
65
  Loggy.log(.error, message: "Failed to resolve service \(name): \(error.localizedDescription)")
50
66
  continuation.resume(throwing: error)
51
- connection.cancel()
67
+ self.cleanupResolve(connection: connection, taskId: taskId)
52
68
 
53
69
  case .waiting(let error):
54
70
  Loggy.log(.debug, message: "Connection waiting for \(name): \(error.localizedDescription)")
@@ -61,25 +77,46 @@ extension BonjourZeroconf {
61
77
  }
62
78
  }
63
79
 
64
- connection.start(queue: networkQueue)
80
+ connection.start(queue: networkQueue)
65
81
  }
66
82
  } catch let error as AddressResolverError {
67
- switch error {
68
- case .timeout:
69
- notifyScanFailListeners(with: BonjourFail.resolveFailed)
70
- break;
71
- case .extractionFailed:
72
- notifyScanFailListeners(with: BonjourFail.extractionFailed)
73
- break;
74
- }
75
- return nil
76
- } catch {
77
- return nil
78
- }
83
+ switch error {
84
+ case .timeout:
85
+ notifyScanFailListeners(with: BonjourFail.resolveFailed)
86
+ case .extractionFailed:
87
+ notifyScanFailListeners(with: BonjourFail.extractionFailed)
88
+ case .cancelled:
89
+ Loggy.log(.debug, message: "Scanning stopped, cancelling address resolution")
90
+ break
91
+ }
92
+ return nil
93
+ } catch {
94
+ return nil
95
+ }
96
+ }
97
+
98
+ /// Cancels ongoing address resolve process
99
+ internal func cancelAddressResolving() {
100
+ Loggy.log(.debug, message: "Cancelling Address Resolving")
101
+ resolveLock.lock()
102
+ activeTimeouts.values.forEach { $0.cancel() }
103
+ activeTimeouts.removeAll()
104
+ activeConnections.values.forEach { $0.cancel() }
105
+ activeConnections.removeAll()
106
+ resolveLock.unlock()
107
+ }
108
+
109
+ /// Cleans up after single resolve process
110
+ private func cleanupResolve(connection: NWConnection, taskId: UUID) {
111
+ connection.cancel()
112
+ resolveLock.lock()
113
+ activeConnections.removeValue(forKey: taskId)
114
+ activeTimeouts.removeValue(forKey: taskId)
115
+ resolveLock.unlock()
79
116
  }
80
117
 
81
118
  /// Extract IP address and port from an endpoint
82
- internal func extractIPAndPort(from endpoint: NWEndpoint, serviceName: String) -> ScanResult? {
119
+ private func extractIPAndPort(from endpoint: NWEndpoint, serviceName: String) -> ScanResult? {
83
120
  switch endpoint {
84
121
  case .hostPort(let host, let port):
85
122
  var ipv4: String?
@@ -12,6 +12,11 @@ class BonjourZeroconf: HybridBonjourZeroconfSpec {
12
12
  internal var scanStateListeners: [UUID: (Bool) -> Void] = [:]
13
13
  internal var scanFailListeners: [UUID: (BonjourFail) -> Void] = [:]
14
14
  internal let networkQueue = DispatchQueue(label: "com.bonjour-zeroconf.network", qos: .userInitiated)
15
+ internal let timeoutScanQueue = DispatchQueue(label: "com.bonjour-zeroconf.scan", qos: .userInitiated)
16
+
17
+ internal var activeConnections: [UUID: NWConnection] = [:]
18
+ internal var activeTimeouts: [UUID: DispatchWorkItem] = [:]
19
+ internal let resolveLock = NSLock()
15
20
 
16
21
  var isScanning: Bool {
17
22
  return _isScanning
@@ -72,6 +77,20 @@ class BonjourZeroconf: HybridBonjourZeroconfSpec {
72
77
  browser.start(queue: networkQueue)
73
78
  }
74
79
 
80
+ func scanFor(time: Double, type: String, domain: String, options: ScanOptions?) throws -> Promise<[ScanResult]> {
81
+ return Promise.async {
82
+ self.scan(type: type, domain: domain, options: options)
83
+
84
+ try await Task.sleep(nanoseconds: UInt64(time * 1_000_000_000))
85
+
86
+ let results = await self.serviceCache.getAll()
87
+
88
+ self.stop()
89
+
90
+ return results
91
+ }
92
+ }
93
+
75
94
  func listenForScanResults(onResult: @escaping ([ScanResult]) -> Void) -> BonjourListener {
76
95
  let listenerId = UUID()
77
96
  self.scanResultsListeners[listenerId] = onResult
@@ -111,6 +130,8 @@ class BonjourZeroconf: HybridBonjourZeroconfSpec {
111
130
  if let browser = self._browser {
112
131
  browser.cancel()
113
132
  }
133
+
134
+ cancelAddressResolving()
114
135
 
115
136
  self._isScanning = false
116
137
  Task {
@@ -49,7 +49,6 @@ public class LocalNetworkAuthorization: NSObject {
49
49
  }
50
50
 
51
51
  private func reset() {
52
- print("resetting")
53
52
  self.browser?.cancel()
54
53
  self.browser = nil
55
54
  self.netService?.stop()
@@ -11,6 +11,7 @@ export interface BonjourZeroconf extends HybridObject<{
11
11
  }> {
12
12
  readonly isScanning: boolean;
13
13
  scan(type: string, domain: string, options?: ScanOptions): void;
14
+ scanFor(time: number, type: string, domain: string, options?: ScanOptions): Promise<ScanResult[]>;
14
15
  stop(): void;
15
16
  listenForScanResults(onResult: (results: ScanResult[]) => void): BonjourListener;
16
17
  listenForScanState(onChange: (isScanning: boolean) => void): BonjourListener;
@@ -1 +1 @@
1
- {"version":3,"file":"BonjourZeroconf.nitro.d.ts","sourceRoot":"","sources":["../../../../src/specs/BonjourZeroconf.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,MAAM,WAAW,WAAW;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,eACf,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IACzD,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAChE,IAAI,IAAI,IAAI,CAAC;IACb,oBAAoB,CAClB,QAAQ,EAAE,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,IAAI,GACxC,eAAe,CAAC;IACnB,kBAAkB,CAAC,QAAQ,EAAE,CAAC,UAAU,EAAE,OAAO,KAAK,IAAI,GAAG,eAAe,CAAC;IAC7E,iBAAiB,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,GAAG,eAAe,CAAC;CACzE"}
1
+ {"version":3,"file":"BonjourZeroconf.nitro.d.ts","sourceRoot":"","sources":["../../../../src/specs/BonjourZeroconf.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,MAAM,WAAW,WAAW;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,eACf,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IACzD,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAE7B,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAChE,OAAO,CACL,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IACzB,IAAI,IAAI,IAAI,CAAC;IACb,oBAAoB,CAClB,QAAQ,EAAE,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,IAAI,GACxC,eAAe,CAAC;IACnB,kBAAkB,CAAC,QAAQ,EAAE,CAAC,UAAU,EAAE,OAAO,KAAK,IAAI,GAAG,eAAe,CAAC;IAC7E,iBAAiB,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,GAAG,eAAe,CAAC;CACzE"}
@@ -2,7 +2,7 @@
2
2
  /// JBonjourFail.hpp
3
3
  /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
4
4
  /// https://github.com/mrousavy/nitro
5
- /// Copyright © 2025 Marc Rousavy @ Margelo
5
+ /// Copyright © 2026 Marc Rousavy @ Margelo
6
6
  ///
7
7
 
8
8
  #pragma once
@@ -2,7 +2,7 @@
2
2
  /// JBonjourListener.hpp
3
3
  /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
4
4
  /// https://github.com/mrousavy/nitro
5
- /// Copyright © 2025 Marc Rousavy @ Margelo
5
+ /// Copyright © 2026 Marc Rousavy @ Margelo
6
6
  ///
7
7
 
8
8
  #pragma once
@@ -2,7 +2,7 @@
2
2
  /// JFunc_void.hpp
3
3
  /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
4
4
  /// https://github.com/mrousavy/nitro
5
- /// Copyright © 2025 Marc Rousavy @ Margelo
5
+ /// Copyright © 2026 Marc Rousavy @ Margelo
6
6
  ///
7
7
 
8
8
  #pragma once
@@ -2,7 +2,7 @@
2
2
  /// JFunc_void_BonjourFail.hpp
3
3
  /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
4
4
  /// https://github.com/mrousavy/nitro
5
- /// Copyright © 2025 Marc Rousavy @ Margelo
5
+ /// Copyright © 2026 Marc Rousavy @ Margelo
6
6
  ///
7
7
 
8
8
  #pragma once
@@ -2,7 +2,7 @@
2
2
  /// JFunc_void_bool.hpp
3
3
  /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
4
4
  /// https://github.com/mrousavy/nitro
5
- /// Copyright © 2025 Marc Rousavy @ Margelo
5
+ /// Copyright © 2026 Marc Rousavy @ Margelo
6
6
  ///
7
7
 
8
8
  #pragma once
@@ -2,7 +2,7 @@
2
2
  /// JFunc_void_std__vector_ScanResult_.hpp
3
3
  /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
4
4
  /// https://github.com/mrousavy/nitro
5
- /// Copyright © 2025 Marc Rousavy @ Margelo
5
+ /// Copyright © 2026 Marc Rousavy @ Margelo
6
6
  ///
7
7
 
8
8
  #pragma once