react-native-zeroconf 0.17.0 → 0.17.2

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.
package/README.md CHANGED
@@ -1,5 +1,13 @@
1
1
  <p align="center">
2
- <img src=".github/banner.png" alt="react-native-zeroconf: Bonjour / mDNS discovery and publishing for React Native, on iOS, Android, macOS, tvOS and Expo" width="100%">
2
+ <a href="https://zeroconf.balthazar.dev"><img src=".github/banner.png" alt="react-native-zeroconf: Bonjour / mDNS discovery and publishing for React Native, on iOS, Android, Windows, macOS, tvOS and Expo" width="100%"></a>
3
+ </p>
4
+
5
+ <p align="center">
6
+ <a href="https://www.npmjs.com/package/react-native-zeroconf"><img src="https://img.shields.io/npm/v/react-native-zeroconf?style=flat" alt="npm version"></a>
7
+ <a href="https://www.npmjs.com/package/react-native-zeroconf"><img src="https://img.shields.io/npm/dm/react-native-zeroconf?style=flat" alt="npm downloads"></a>
8
+ <a href="https://github.com/balthazar/react-native-zeroconf/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/balthazar/react-native-zeroconf/ci.yml?branch=master&label=tests&style=flat" alt="tests"></a>
9
+ <a href="https://github.com/balthazar/react-native-zeroconf/actions/workflows/windows.yml"><img src="https://img.shields.io/github/actions/workflow/status/balthazar/react-native-zeroconf/windows.yml?branch=master&label=windows&style=flat" alt="windows"></a>
10
+ <a href="LICENSE"><img src="https://img.shields.io/npm/l/react-native-zeroconf?style=flat" alt="license"></a>
3
11
  </p>
4
12
 
5
13
  Find the services other devices advertise on the local network (printers, speakers, cameras, your own servers) and advertise your own, on iOS, Android, macOS, tvOS and Windows.
@@ -13,7 +21,7 @@ yarn add react-native-zeroconf
13
21
  cd ios && pod install
14
22
  ```
15
23
 
16
- On iOS, declare the service types you use in `NSBonjourServices`, see [Permissions and Setup](https://zeroconf.balthazar.dev/Permissions-and-Setup). Expo works with development builds, see [Installation](https://zeroconf.balthazar.dev/Installation).
24
+ On iOS, declare the service types you use in `NSBonjourServices`, see [Permissions and Setup](https://zeroconf.balthazar.dev/Permissions-and-Setup). Expo works with development builds, see [Installation with Expo](https://zeroconf.balthazar.dev/Installation-Expo).
17
25
 
18
26
  ## Usage
19
27
 
@@ -21,6 +21,8 @@ public class ZeroconfOptions {
21
21
  @Nullable public String networkInterface;
22
22
  /** resolveService: seconds before giving up */
23
23
  public double timeoutSeconds = 5;
24
+ /** Scan: seconds to resolve each service, retried once before a TIMEOUT error */
25
+ public double resolveTimeoutSeconds = 5;
24
26
 
25
27
  public static ZeroconfOptions from(@Nullable ReadableMap map) {
26
28
  ZeroconfOptions options = new ZeroconfOptions();
@@ -42,6 +44,9 @@ public class ZeroconfOptions {
42
44
  if (map.hasKey("timeout") && map.getType("timeout") == ReadableType.Number && map.getDouble("timeout") > 0) {
43
45
  options.timeoutSeconds = map.getDouble("timeout");
44
46
  }
47
+ if (map.hasKey("resolveTimeout") && map.getType("resolveTimeout") == ReadableType.Number && map.getDouble("resolveTimeout") > 0) {
48
+ options.resolveTimeoutSeconds = map.getDouble("resolveTimeout");
49
+ }
45
50
  return options;
46
51
  }
47
52
 
@@ -66,6 +66,7 @@ public class DnssdImpl implements Zeroconf {
66
66
  final String scanId;
67
67
  final boolean typesOnly;
68
68
  int ifIndex = DNSSD.ALL_INTERFACES;
69
+ long resolveTimeoutMs = 5000;
69
70
  DNSSDService browse;
70
71
  // A service is reported once per network interface, count them so found/remove are emitted once
71
72
  final Map<String, Integer> foundInterfaces = new HashMap<>();
@@ -87,6 +88,9 @@ public class DnssdImpl implements Zeroconf {
87
88
  int port;
88
89
  Map<String, String> txt = new LinkedHashMap<>();
89
90
  final Set<String> addresses = new LinkedHashSet<>();
91
+ // Scans: emitted once an address is known, retried once after a timeout
92
+ boolean emitted;
93
+ boolean retried;
90
94
 
91
95
  Resolve(String name) {
92
96
  this.name = name;
@@ -144,6 +148,7 @@ public class DnssdImpl implements Zeroconf {
144
148
  return;
145
149
  }
146
150
  scan.ifIndex = ifIndex;
151
+ scan.resolveTimeoutMs = (long) (options.resolveTimeoutSeconds * 1000);
147
152
  // "_ipp._tcp,_printer" browses the _printer subtype
148
153
  String regType = String.format("_%s._%s", type, protocol);
149
154
  if (options.subtype != null) {
@@ -254,10 +259,25 @@ public class DnssdImpl implements Zeroconf {
254
259
 
255
260
  // Resolve: host, port and TXT record, then the host's IPv4 and IPv6 addresses
256
261
 
257
- private void startResolve(@Nullable final Scan scan, int ifIndex, final String serviceName, String regType, String domain, @Nullable Resolve existing) {
262
+ private void startResolve(@Nullable final Scan scan, final int ifIndex, final String serviceName, final String regType, final String domain, @Nullable Resolve existing) {
258
263
  final Resolve resolve = existing != null ? existing : new Resolve(serviceName);
259
264
  if (scan != null) {
260
265
  scan.resolves.put(serviceName, resolve);
266
+ // DNSSD gives up silently after its own timeout, report it before
267
+ mainHandler.postDelayed(() -> executor.execute(() -> {
268
+ if (!isCurrent(scan, resolve) || resolve.emitted) {
269
+ return;
270
+ }
271
+ resolve.stop();
272
+ // Slow devices can time out, retry once before reporting the error
273
+ if (!resolve.retried) {
274
+ resolve.retried = true;
275
+ startResolve(scan, ifIndex, serviceName, regType, domain, resolve);
276
+ return;
277
+ }
278
+ scan.resolves.remove(serviceName);
279
+ zeroconfModule.sendError(ZeroconfModule.ERROR_DOMAIN_LIBRARY, ZeroconfModule.ERROR_CODE_TIMEOUT, "Resolving service " + serviceName + " failed: timed out", serviceName, scan.scanId);
280
+ }), scan.resolveTimeoutMs);
261
281
  }
262
282
  try {
263
283
  resolve.operations.add(dnssd.resolve(0, ifIndex, serviceName, regType, domain, new ResolveListener() {
@@ -306,6 +326,7 @@ public class DnssdImpl implements Zeroconf {
306
326
  return;
307
327
  }
308
328
  // Emitted for each address, the first one is usable right away
329
+ resolve.emitted = true;
309
330
  sendScanEvent(ZeroconfModule.EVENT_RESOLVE, resolveToMap(resolve), scan.scanId);
310
331
  });
311
332
  }
@@ -63,6 +63,7 @@ public class NsdServiceImpl implements Zeroconf {
63
63
 
64
64
  private static class NsdScan {
65
65
  final String scanId;
66
+ double resolveTimeoutSeconds = 5;
66
67
  NsdManager.DiscoveryListener discoveryListener;
67
68
  // Android 14+: services are followed with registerServiceInfoCallback, null before
68
69
  @Nullable ServiceInfoCallbacks infoCallbacks;
@@ -83,11 +84,14 @@ public class NsdServiceImpl implements Zeroconf {
83
84
  final NsdServiceInfo serviceInfo;
84
85
  // resolveService(): settled instead of emitting events
85
86
  @Nullable final Promise promise;
87
+ // Seconds before the TIMEOUT error, counted from the start of the resolve
88
+ final double timeoutSeconds;
86
89
 
87
- PendingResolve(@Nullable String scanId, NsdServiceInfo serviceInfo, @Nullable Promise promise) {
90
+ PendingResolve(@Nullable String scanId, NsdServiceInfo serviceInfo, @Nullable Promise promise, double timeoutSeconds) {
88
91
  this.scanId = scanId;
89
92
  this.serviceInfo = serviceInfo;
90
93
  this.promise = promise;
94
+ this.timeoutSeconds = timeoutSeconds;
91
95
  }
92
96
  }
93
97
 
@@ -104,6 +108,7 @@ public class NsdServiceImpl implements Zeroconf {
104
108
  this.stop(scanId);
105
109
 
106
110
  final NsdScan scan = new NsdScan(scanId);
111
+ scan.resolveTimeoutSeconds = options.resolveTimeoutSeconds;
107
112
  final boolean typesOnly = ZeroconfModule.isServiceTypesScan(type, protocol);
108
113
 
109
114
  Network network = null;
@@ -154,7 +159,8 @@ public class NsdServiceImpl implements Zeroconf {
154
159
  }
155
160
 
156
161
  if (Build.VERSION.SDK_INT >= 34) {
157
- scan.infoCallbacks = new ServiceInfoCallbacks(nsdManager, mCallbackExecutor, new ServiceInfoCallbacks.Listener() {
162
+ long timeoutMs = (long) (options.resolveTimeoutSeconds * 1000);
163
+ scan.infoCallbacks = new ServiceInfoCallbacks(nsdManager, mCallbackExecutor, mHandler, timeoutMs, new ServiceInfoCallbacks.Listener() {
158
164
  @Override
159
165
  public void onServiceUpdated(NsdServiceInfo serviceInfo) {
160
166
  if (getHostAddresses(serviceInfo).isEmpty()) {
@@ -167,6 +173,11 @@ public class NsdServiceImpl implements Zeroconf {
167
173
  public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
168
174
  zeroconfModule.sendError(ZeroconfModule.ERROR_DOMAIN_NSD, errorCode, "Resolving service " + serviceInfo.getServiceName() + " failed: " + ZeroconfModule.describeNsdError(errorCode), serviceInfo.getServiceName(), scanId);
169
175
  }
176
+
177
+ @Override
178
+ public void onTimeout(NsdServiceInfo serviceInfo) {
179
+ sendResolveTimeout(serviceInfo.getServiceName(), scanId);
180
+ }
170
181
  });
171
182
  }
172
183
 
@@ -204,7 +215,7 @@ public class NsdServiceImpl implements Zeroconf {
204
215
  if (scan.infoCallbacks != null) {
205
216
  scan.infoCallbacks.register(serviceInfo);
206
217
  } else {
207
- enqueueResolve(scanId, serviceInfo);
218
+ enqueueResolve(scan, serviceInfo);
208
219
  }
209
220
  }
210
221
 
@@ -278,7 +289,8 @@ public class NsdServiceImpl implements Zeroconf {
278
289
  synchronized (mResolveQueue) {
279
290
  Iterator<PendingResolve> iterator = mResolveQueue.iterator();
280
291
  while (iterator.hasNext()) {
281
- if (iterator.next().scanId.equals(scanId)) {
292
+ // resolveService() entries have no scan id
293
+ if (scanId.equals(iterator.next().scanId)) {
282
294
  iterator.remove();
283
295
  }
284
296
  }
@@ -384,7 +396,7 @@ public class NsdServiceImpl implements Zeroconf {
384
396
  if (Build.VERSION.SDK_INT < 34) {
385
397
  // One resolve at a time before Android 14, shares the queue of the scans
386
398
  synchronized (mResolveQueue) {
387
- mResolveQueue.add(new PendingResolve(null, serviceInfo, promise));
399
+ mResolveQueue.add(new PendingResolve(null, serviceInfo, promise, options.timeoutSeconds));
388
400
  }
389
401
  resolveNext();
390
402
  return;
@@ -520,17 +532,38 @@ public class NsdServiceImpl implements Zeroconf {
520
532
  return mNsdManager;
521
533
  }
522
534
 
535
+ // Before Android 14 each NsdManager client resolves one service at a time and a resolve can't be cancelled:
536
+ // a service that never answers would block every later resolve. After a timeout the queue moves on to a
537
+ // client of its own, an NsdManager from a new Context (system services are cached per Context)
538
+ @Nullable private volatile NsdManager mResolveManager;
539
+
540
+ private NsdManager getResolveManager() {
541
+ return mResolveManager != null ? mResolveManager : getNsdManager();
542
+ }
543
+
544
+ private void replaceResolveManager() {
545
+ Context context = getReactApplicationContext();
546
+ Context fresh = context.createConfigurationContext(context.getResources().getConfiguration());
547
+ mResolveManager = (NsdManager) fresh.getSystemService(Context.NSD_SERVICE);
548
+ }
549
+
523
550
  private ReactApplicationContext getReactApplicationContext() {
524
551
  return reactApplicationContext;
525
552
  }
526
553
 
527
- private void enqueueResolve(String scanId, NsdServiceInfo serviceInfo) {
554
+ private void enqueueResolve(NsdScan scan, NsdServiceInfo serviceInfo) {
555
+ // A resolve can't be cancelled before Android 14, so there is no retry: waiting twice as long
556
+ // matches the total time of the other implementations, which retry once
528
557
  synchronized (mResolveQueue) {
529
- mResolveQueue.add(new PendingResolve(scanId, serviceInfo, null));
558
+ mResolveQueue.add(new PendingResolve(scan.scanId, serviceInfo, null, scan.resolveTimeoutSeconds * 2));
530
559
  }
531
560
  resolveNext();
532
561
  }
533
562
 
563
+ private void sendResolveTimeout(String serviceName, String scanId) {
564
+ zeroconfModule.sendError(ZeroconfModule.ERROR_DOMAIN_LIBRARY, ZeroconfModule.ERROR_CODE_TIMEOUT, "Resolving service " + serviceName + " failed: timed out", serviceName, scanId);
565
+ }
566
+
534
567
  private void resolveNext() {
535
568
  PendingResolve next;
536
569
  synchronized (mResolveQueue) {
@@ -540,8 +573,11 @@ public class NsdServiceImpl implements Zeroconf {
540
573
  next = mResolveQueue.poll();
541
574
  mIsResolving = true;
542
575
  }
576
+ final ZeroResolveListener listener = new ZeroResolveListener(next);
543
577
  try {
544
- getNsdManager().resolveService(next.serviceInfo, new ZeroResolveListener(next.scanId, next.promise));
578
+ getResolveManager().resolveService(next.serviceInfo, listener);
579
+ // The queue waits for the answer, or moves on to another client after the timeout
580
+ mHandler.postDelayed(listener::timedOut, (long) (next.timeoutSeconds * 1000));
545
581
  } catch (Throwable e) {
546
582
  Log.e(TAG, "resolveService failed", e);
547
583
  String serviceName = next.serviceInfo.getServiceName();
@@ -558,25 +594,58 @@ public class NsdServiceImpl implements Zeroconf {
558
594
  }
559
595
 
560
596
  private class ZeroResolveListener implements NsdManager.ResolveListener {
597
+ private final PendingResolve pending;
561
598
  @Nullable private final String scanId;
562
599
  @Nullable private final Promise promise;
600
+ // Settled: answered, failed or timed out. A late answer after the timeout is ignored
601
+ private boolean settled = false;
563
602
 
564
- ZeroResolveListener(@Nullable String scanId, @Nullable Promise promise) {
565
- this.scanId = scanId;
566
- this.promise = promise;
603
+ ZeroResolveListener(PendingResolve pending) {
604
+ this.pending = pending;
605
+ this.scanId = pending.scanId;
606
+ this.promise = pending.promise;
607
+ }
608
+
609
+ private synchronized boolean settle() {
610
+ if (settled) {
611
+ return false;
612
+ }
613
+ settled = true;
614
+ return true;
615
+ }
616
+
617
+ void timedOut() {
618
+ if (!settle()) {
619
+ return;
620
+ }
621
+ String serviceName = pending.serviceInfo.getServiceName();
622
+ if (promise != null) {
623
+ ZeroconfModule.reject(promise, ZeroconfModule.ERROR_DOMAIN_LIBRARY, ZeroconfModule.ERROR_CODE_TIMEOUT, "Resolving service " + serviceName + " failed: timed out", serviceName);
624
+ } else if (mScans.containsKey(scanId)) {
625
+ sendResolveTimeout(serviceName, scanId);
626
+ }
627
+ replaceResolveManager();
628
+ onResolveDone();
567
629
  }
568
630
 
569
631
  @Override
570
632
  public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
571
633
  if (errorCode == NsdManager.FAILURE_ALREADY_ACTIVE) {
572
634
  // Another resolve (possibly from another app or listener) is in flight: retry shortly
635
+ if (!settle()) {
636
+ return;
637
+ }
573
638
  synchronized (mResolveQueue) {
574
- mResolveQueue.addFirst(new PendingResolve(scanId, serviceInfo, promise));
639
+ mResolveQueue.addFirst(pending);
575
640
  mIsResolving = false;
576
641
  }
577
642
  mHandler.postDelayed(NsdServiceImpl.this::resolveNext, RESOLVE_RETRY_DELAY_MS);
578
643
  return;
579
644
  }
645
+ if (!settle()) {
646
+ // Timed out: the queue already moved on to another client
647
+ return;
648
+ }
580
649
  String message = "Resolving service " + serviceInfo.getServiceName() + " failed: " + ZeroconfModule.describeNsdError(errorCode);
581
650
  if (promise != null) {
582
651
  ZeroconfModule.reject(promise, ZeroconfModule.ERROR_DOMAIN_NSD, errorCode, message, serviceInfo.getServiceName());
@@ -588,6 +657,10 @@ public class NsdServiceImpl implements Zeroconf {
588
657
 
589
658
  @Override
590
659
  public void onServiceResolved(NsdServiceInfo serviceInfo) {
660
+ if (!settle()) {
661
+ // Timed out: the queue already moved on to another client
662
+ return;
663
+ }
591
664
  if (promise != null) {
592
665
  promise.resolve(serviceInfoToMap(serviceInfo));
593
666
  } else if (mScans.containsKey(scanId)) {
@@ -3,6 +3,7 @@ package com.balthazargronon.RCTZeroconf.nsd;
3
3
  import android.annotation.TargetApi;
4
4
  import android.net.nsd.NsdManager;
5
5
  import android.net.nsd.NsdServiceInfo;
6
+ import android.os.Handler;
6
7
  import android.util.Log;
7
8
 
8
9
  import java.util.ArrayList;
@@ -13,6 +14,7 @@ import java.util.concurrent.Executor;
13
14
  /**
14
15
  * Follows found services with NsdManager.registerServiceInfoCallback (Android 14+) instead of one-shot resolves:
15
16
  * services can be followed concurrently, updates (addresses, TXT records) are delivered as they change.
17
+ * A service without addresses after the resolve timeout is followed again once, then reported with onTimeout.
16
18
  * Only loaded on API 34+, so older devices never touch these APIs.
17
19
  */
18
20
  @TargetApi(34)
@@ -23,34 +25,55 @@ class ServiceInfoCallbacks {
23
25
  void onServiceUpdated(NsdServiceInfo serviceInfo);
24
26
 
25
27
  void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode);
28
+
29
+ void onTimeout(NsdServiceInfo serviceInfo);
30
+ }
31
+
32
+ private static class Followed {
33
+ NsdManager.ServiceInfoCallback callback;
34
+ Runnable timeout;
35
+ volatile boolean resolved;
26
36
  }
27
37
 
28
38
  private final NsdManager nsdManager;
29
39
  private final Executor executor;
40
+ private final Handler handler;
41
+ private final long timeoutMs;
30
42
  private final Listener listener;
31
- private final Map<String, NsdManager.ServiceInfoCallback> callbacks = new ConcurrentHashMap<>();
43
+ private final Map<String, Followed> followed = new ConcurrentHashMap<>();
32
44
 
33
- ServiceInfoCallbacks(NsdManager nsdManager, Executor executor, Listener listener) {
45
+ ServiceInfoCallbacks(NsdManager nsdManager, Executor executor, Handler handler, long timeoutMs, Listener listener) {
34
46
  this.nsdManager = nsdManager;
35
47
  this.executor = executor;
48
+ this.handler = handler;
49
+ this.timeoutMs = timeoutMs;
36
50
  this.listener = listener;
37
51
  }
38
52
 
39
53
  void register(final NsdServiceInfo serviceInfo) {
40
- final String name = serviceInfo.getServiceName();
41
- if (callbacks.containsKey(name)) {
54
+ if (followed.containsKey(serviceInfo.getServiceName())) {
42
55
  return;
43
56
  }
57
+ follow(serviceInfo, false);
58
+ }
44
59
 
45
- NsdManager.ServiceInfoCallback callback = new NsdManager.ServiceInfoCallback() {
60
+ private void follow(final NsdServiceInfo serviceInfo, final boolean retried) {
61
+ final String name = serviceInfo.getServiceName();
62
+ final Followed entry = new Followed();
63
+ entry.callback = new NsdManager.ServiceInfoCallback() {
46
64
  @Override
47
65
  public void onServiceInfoCallbackRegistrationFailed(int errorCode) {
48
- callbacks.remove(name);
49
- listener.onRegistrationFailed(serviceInfo, errorCode);
66
+ if (followed.remove(name, entry)) {
67
+ handler.removeCallbacks(entry.timeout);
68
+ listener.onRegistrationFailed(serviceInfo, errorCode);
69
+ }
50
70
  }
51
71
 
52
72
  @Override
53
73
  public void onServiceUpdated(NsdServiceInfo updated) {
74
+ if (!updated.getHostAddresses().isEmpty()) {
75
+ entry.resolved = true;
76
+ }
54
77
  listener.onServiceUpdated(updated);
55
78
  }
56
79
 
@@ -63,18 +86,32 @@ class ServiceInfoCallbacks {
63
86
  public void onServiceInfoCallbackUnregistered() {
64
87
  }
65
88
  };
66
- callbacks.put(name, callback);
89
+ entry.timeout = () -> {
90
+ if (followed.get(name) != entry || entry.resolved) {
91
+ return;
92
+ }
93
+ unregister(name);
94
+ // Slow devices can time out, follow again once before reporting the error
95
+ if (!retried) {
96
+ follow(serviceInfo, true);
97
+ } else {
98
+ listener.onTimeout(serviceInfo);
99
+ }
100
+ };
101
+ followed.put(name, entry);
67
102
  Log.d(TAG, "Following " + name + " with registerServiceInfoCallback");
68
- nsdManager.registerServiceInfoCallback(serviceInfo, executor, callback);
103
+ nsdManager.registerServiceInfoCallback(serviceInfo, executor, entry.callback);
104
+ handler.postDelayed(entry.timeout, timeoutMs);
69
105
  }
70
106
 
71
107
  void unregister(String name) {
72
- NsdManager.ServiceInfoCallback callback = callbacks.remove(name);
73
- if (callback == null) {
108
+ Followed entry = followed.remove(name);
109
+ if (entry == null) {
74
110
  return;
75
111
  }
112
+ handler.removeCallbacks(entry.timeout);
76
113
  try {
77
- nsdManager.unregisterServiceInfoCallback(callback);
114
+ nsdManager.unregisterServiceInfoCallback(entry.callback);
78
115
  } catch (IllegalArgumentException e) {
79
116
  // Registration failed or was already unregistered
80
117
  Log.w(TAG, "unregisterServiceInfoCallback failed", e);
@@ -82,7 +119,7 @@ class ServiceInfoCallbacks {
82
119
  }
83
120
 
84
121
  void unregisterAll() {
85
- for (String name : new ArrayList<>(callbacks.keySet())) {
122
+ for (String name : new ArrayList<>(followed.keySet())) {
86
123
  unregister(name);
87
124
  }
88
125
  }
package/dist/index.js CHANGED
@@ -330,6 +330,7 @@ class Zeroconf extends _events.EventEmitter {
330
330
  }
331
331
  this._scanImplType = implType;
332
332
  RNZeroconf.scan(this._scanId, type, protocol, domain, implType, nativeOptions({
333
+ resolveTimeout,
333
334
  subtype,
334
335
  networkInterface
335
336
  }));
package/index.d.ts CHANGED
@@ -36,7 +36,10 @@ export interface ScanOptions {
36
36
  domain?: string
37
37
  /** Android only, defaults to `NSD` */
38
38
  implType?: ImplType
39
- /** iOS only, seconds to try resolving a service before retrying once and giving up. Defaults to `5` */
39
+ /**
40
+ * Seconds to try resolving each service before retrying once, then emitting an `error` with code `'TIMEOUT'`.
41
+ * Defaults to `5`. Android `NSD` before Android 14 can't retry a resolve, the error comes after twice this time
42
+ */
40
43
  resolveTimeout?: number
41
44
  /** Only find services registered with this subtype, e.g. `'printer'` */
42
45
  subtype?: string
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-zeroconf",
3
- "version": "0.17.0",
3
+ "version": "0.17.2",
4
4
  "description": "A Zeroconf discovery utility for react-native",
5
5
  "main": "dist",
6
6
  "types": "index.d.ts",
package/src/index.js CHANGED
@@ -329,7 +329,7 @@ export default class Zeroconf extends EventEmitter {
329
329
  protocol,
330
330
  domain,
331
331
  implType,
332
- nativeOptions({ subtype, networkInterface }),
332
+ nativeOptions({ resolveTimeout, subtype, networkInterface }),
333
333
  )
334
334
  } else {
335
335
  RNZeroconf.scan(
@@ -140,7 +140,8 @@ void RNZeroconf::Emit(std::wstring_view eventName, React::JSValueObject body) no
140
140
  }
141
141
 
142
142
  void RNZeroconf::scan(std::string scanId, std::string type, std::string protocol, std::string domain, React::JSValue options) noexcept {
143
- m_zeroconf->Scan(scanId, Wide(type), Wide(protocol), Wide(domain), StringOption(options, "subtype"), StringOption(options, "networkInterface"));
143
+ m_zeroconf->Scan(scanId, Wide(type), Wide(protocol), Wide(domain), StringOption(options, "subtype"), StringOption(options, "networkInterface"),
144
+ NumberOption(options, "resolveTimeout"));
144
145
  }
145
146
 
146
147
  void RNZeroconf::stop(React::JSValue scanId) noexcept {
@@ -38,6 +38,8 @@ struct Resolve : Operation {
38
38
  DNS_SERVICE_RESOLVE_REQUEST request{};
39
39
  DNS_SERVICE_CANCEL cancel{};
40
40
  bool done = false;
41
+ // Scans: retried once after a timeout
42
+ bool retried = false;
41
43
  ServiceCallback promiseResolve;
42
44
  ErrorCallback promiseReject;
43
45
  };
@@ -50,6 +52,7 @@ struct Browse : Operation {
50
52
  std::wstring queryName;
51
53
  bool typesOnly = false;
52
54
  ULONG interfaceIndex = 0;
55
+ DWORD resolveTimeoutMs = 5000;
53
56
  DNS_SERVICE_BROWSE_REQUEST request{};
54
57
  DNS_SERVICE_CANCEL cancel{};
55
58
  // Lower-case PTR target -> name reported to JavaScript
@@ -362,7 +365,8 @@ void Zeroconf::Scan(
362
365
  const std::wstring &protocol,
363
366
  const std::wstring &domain,
364
367
  const std::wstring &subtype,
365
- const std::wstring &networkInterface) {
368
+ const std::wstring &networkInterface,
369
+ double resolveTimeoutSeconds) {
366
370
  After after;
367
371
  {
368
372
  std::lock_guard<std::recursive_mutex> lock(mutex_);
@@ -370,6 +374,7 @@ void Zeroconf::Scan(
370
374
 
371
375
  auto browse = std::make_shared<Browse>(this);
372
376
  browse->scanId = scanId;
377
+ browse->resolveTimeoutMs = static_cast<DWORD>((resolveTimeoutSeconds > 0 ? resolveTimeoutSeconds : 5) * 1000);
373
378
  browse->regType = L"_" + type + L"._" + protocol;
374
379
  browse->domain = DomainName(domain);
375
380
  browse->typesOnly = type == L"services._dns-sd" && protocol == L"udp";
@@ -490,7 +495,7 @@ void Zeroconf::OnBrowse(const std::shared_ptr<Browse> &browse, DWORD status, PDN
490
495
  resolve->queryName = WithoutTrailingDot(record->pName);
491
496
  resolve->interfaceIndex = browse->interfaceIndex;
492
497
  browse->resolves[instanceKey] = resolve;
493
- StartResolve(resolve, after);
498
+ StartScanResolve(browse, resolve, after);
494
499
  }
495
500
  continue;
496
501
  }
@@ -552,7 +557,7 @@ void Zeroconf::OnBrowse(const std::shared_ptr<Browse> &browse, DWORD status, PDN
552
557
  resolve->queryName = target;
553
558
  resolve->interfaceIndex = browse->interfaceIndex;
554
559
  browse->resolves[key] = resolve;
555
- StartResolve(resolve, after);
560
+ StartScanResolve(browse, resolve, after);
556
561
  }
557
562
  }
558
563
  }
@@ -590,6 +595,54 @@ void Zeroconf::StartResolve(const std::shared_ptr<Resolve> &resolve, After &afte
590
595
  });
591
596
  }
592
597
 
598
+ // Resolves of a scan time out after the scan's resolveTimeout, are retried once, then reported with a TIMEOUT error
599
+ void Zeroconf::StartScanResolve(const std::shared_ptr<Browse> &browse, const std::shared_ptr<Resolve> &resolve, After &after) {
600
+ StartResolve(resolve, after);
601
+ if (resolve->done) {
602
+ return;
603
+ }
604
+ std::weak_ptr<Resolve> weak = resolve;
605
+ auto alive = alive_;
606
+ DWORD timeout = browse->resolveTimeoutMs;
607
+ std::thread([this, weak, alive, timeout] {
608
+ Sleep(timeout);
609
+ if (!*alive) return;
610
+ auto expired = weak.lock();
611
+ if (!expired) return;
612
+ After timedOut;
613
+ {
614
+ std::lock_guard<std::recursive_mutex> lock(mutex_);
615
+ auto browse = expired->browse.lock();
616
+ auto current = browse ? browses_.find(browse->scanId) : browses_.end();
617
+ if (expired->done || current == browses_.end() || current->second != browse) return;
618
+ auto entry = browse->resolves.find(expired->key);
619
+ if (entry == browse->resolves.end() || entry->second != expired) return;
620
+ CancelResolve(expired, timedOut);
621
+ browse->resolves.erase(entry);
622
+ if (!expired->retried) {
623
+ // Slow devices can time out, retry once before reporting the error
624
+ auto retry = std::make_shared<Resolve>(this);
625
+ retry->browse = browse;
626
+ retry->scanId = expired->scanId;
627
+ retry->name = expired->name;
628
+ retry->key = expired->key;
629
+ retry->queryName = expired->queryName;
630
+ retry->interfaceIndex = expired->interfaceIndex;
631
+ retry->retried = true;
632
+ browse->resolves[retry->key] = retry;
633
+ StartScanResolve(browse, retry, timedOut);
634
+ } else {
635
+ Error error = LibraryError(L"TIMEOUT", L"Resolving service " + expired->name + L" failed: timed out", expired->name);
636
+ std::string scanId = expired->scanId;
637
+ timedOut.push_back([this, scanId, error] {
638
+ if (events_.error) events_.error(scanId, error);
639
+ });
640
+ }
641
+ }
642
+ Run(timedOut);
643
+ }).detach();
644
+ }
645
+
593
646
  void Zeroconf::CancelResolve(const std::shared_ptr<Resolve> &resolve, After &after) {
594
647
  if (resolve->done) {
595
648
  return;
@@ -89,7 +89,8 @@ class Zeroconf {
89
89
  const std::wstring &protocol,
90
90
  const std::wstring &domain,
91
91
  const std::wstring &subtype,
92
- const std::wstring &networkInterface);
92
+ const std::wstring &networkInterface,
93
+ double resolveTimeoutSeconds = 5);
93
94
  void Stop(const std::string &scanId);
94
95
  void StopAll();
95
96
 
@@ -128,6 +129,7 @@ class Zeroconf {
128
129
 
129
130
  void StopLocked(const std::string &scanId, After &after);
130
131
  void StartResolve(const std::shared_ptr<Resolve> &resolve, After &after);
132
+ void StartScanResolve(const std::shared_ptr<Browse> &browse, const std::shared_ptr<Resolve> &resolve, After &after);
131
133
  void CancelResolve(const std::shared_ptr<Resolve> &resolve, After &after);
132
134
  void PublishAs(
133
135
  const std::wstring &regType,