react-native-zeroconf 0.14.0 → 0.15.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.
package/README.md CHANGED
@@ -11,6 +11,7 @@ Discover and publish network services using Zeroconf protocols (Bonjour, Avahi,
11
11
  - **Cross-Platform**: Works on iOS and Android
12
12
  - **Dual Android Implementation**: Choose between NSD (Android native) or DNSSD (embedded mDNSResponder)
13
13
  - **Android 15+ Compatible**: Includes 16KB page size alignment (Google Play requirement starting November 1, 2025)
14
+ - **TypeScript**: Type definitions included
14
15
 
15
16
  ## Installation
16
17
 
@@ -34,7 +35,7 @@ For manual installation, see the [wiki](https://github.com/balthazar/react-nativ
34
35
 
35
36
  ### Android Permissions
36
37
 
37
- Add the following permissions to your `AndroidManifest.xml`:
38
+ The library declares the following permissions in its own manifest, so they are merged into your app automatically:
38
39
 
39
40
  ```xml
40
41
  <uses-permission android:name="android.permission.INTERNET" />
@@ -82,6 +83,23 @@ setTimeout(() => {
82
83
  }, 10000)
83
84
  ```
84
85
 
86
+ ## TypeScript
87
+
88
+ Type definitions ship with the package, so `@types/react-native-zeroconf` is no longer needed (uninstall it to avoid conflicting types). Event listeners are typed by event name:
89
+
90
+ ```typescript
91
+ import Zeroconf, { ImplType, Service } from 'react-native-zeroconf'
92
+
93
+ const zeroconf = new Zeroconf()
94
+
95
+ zeroconf.on('resolved', (service: Service) => {
96
+ console.log(service.name, service.addresses, service.txt)
97
+ })
98
+ zeroconf.on('remove', name => console.log(`${name} left`))
99
+
100
+ zeroconf.scan('http', 'tcp', 'local.', ImplType.DNSSD)
101
+ ```
102
+
85
103
  ## API Reference
86
104
 
87
105
  ### Constructor
@@ -118,12 +136,10 @@ Stop the current scan.
118
136
 
119
137
  | Parameter | Type | Default | Description |
120
138
  | ---------- | ------ | ------- | ---------------------------------------------- |
121
- | `implType` | string | `'NSD'` | **Android only**: Which implementation to stop |
139
+ | `implType` | string | last scan's | **Android only**: Which implementation to stop. Defaults to the one used by the last `scan()` |
122
140
 
123
141
  ```javascript
124
142
  zeroconf.stop()
125
- // or on Android with DNSSD:
126
- zeroconf.stop('DNSSD')
127
143
  ```
128
144
 
129
145
  #### `getServices()`
@@ -163,7 +179,7 @@ Remove a published service.
163
179
  | Parameter | Type | Default | Description |
164
180
  | ---------- | ------ | -------- | -------------------------------------- |
165
181
  | `name` | string | required | Name of the service to unpublish |
166
- | `implType` | string | `'NSD'` | **Android only**: Which implementation |
182
+ | `implType` | string | publish's | **Android only**: Which implementation. Defaults to the one the service was published with |
167
183
 
168
184
  ```javascript
169
185
  zeroconf.unpublishService('MyWebServer')
@@ -18,11 +18,6 @@ android {
18
18
  abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
19
19
  }
20
20
 
21
- externalNativeBuild {
22
- ndkBuild {
23
- arguments "NDK_APPLICATION_MK:=src/main/jni/Application.mk"
24
- }
25
- }
26
21
  }
27
22
 
28
23
  externalNativeBuild {
@@ -50,6 +45,4 @@ dependencies {
50
45
  // RxJava2 for Rx2Dnssd (bundled from Discord's RxDNSSD fork)
51
46
  api 'io.reactivex.rxjava2:rxjava:2.2.21'
52
47
  api 'io.reactivex.rxjava2:rxandroid:2.1.1'
53
- // https://mvnrepository.com/artifact/org.apache.commons/commons-lang3
54
- api group: 'org.apache.commons', name: 'commons-lang3', version: '3.11'
55
48
  }
@@ -1,2 +1,6 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.INTERNET" />
3
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
4
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
5
+ <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
2
6
  </manifest>
@@ -5,9 +5,9 @@ import com.balthazargronon.RCTZeroconf.rx2dnssd.DnssdImpl;
5
5
  import com.facebook.react.bridge.ReactApplicationContext;
6
6
  import com.facebook.react.bridge.ReactContext;
7
7
 
8
- import org.apache.commons.lang3.StringUtils;
9
-
8
+ import java.util.ArrayList;
10
9
  import java.util.HashMap;
10
+ import java.util.List;
11
11
  import java.util.Map;
12
12
 
13
13
  public class ZeroConfImplFactory {
@@ -25,7 +25,7 @@ public class ZeroConfImplFactory {
25
25
  }
26
26
 
27
27
  public Zeroconf getZeroconf(String implType) {
28
- if (StringUtils.isBlank(implType)) implType = ZeroConfImplFactory.NSD_IMPL;
28
+ if (implType == null || implType.trim().isEmpty()) implType = ZeroConfImplFactory.NSD_IMPL;
29
29
  return getOrCreateImpl(implType);
30
30
  }
31
31
 
@@ -46,4 +46,11 @@ public class ZeroConfImplFactory {
46
46
  return zeroconfMap.get(implType);
47
47
  }
48
48
 
49
+ /**
50
+ * Implementations that have already been created, without instantiating new ones
51
+ */
52
+ public List<Zeroconf> getCreatedImpls() {
53
+ return new ArrayList<>(zeroconfMap.values());
54
+ }
55
+
49
56
  }
@@ -104,8 +104,9 @@ public class ZeroconfModule extends ReactContextBaseJavaModule {
104
104
  public void onCatalystInstanceDestroy() {
105
105
  super.onCatalystInstanceDestroy();
106
106
  try {
107
- stop(ZeroConfImplFactory.NSD_IMPL);
108
- stop(ZeroConfImplFactory.DNSSD_IMPL);
107
+ for (Zeroconf impl : zeroConfFactory.getCreatedImpls()) {
108
+ impl.stop();
109
+ }
109
110
  } catch (Throwable e) {
110
111
  Log.e(getClass().getName(), e.getMessage(), e);
111
112
  sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_ERROR, "Exception During Catalyst Destroy: " + e.getMessage());
@@ -6,6 +6,9 @@ import android.net.nsd.NsdManager;
6
6
  import android.net.nsd.NsdServiceInfo;
7
7
  import android.net.wifi.WifiManager;
8
8
  import android.os.Build;
9
+ import android.os.Handler;
10
+ import android.os.Looper;
11
+ import android.util.Log;
9
12
 
10
13
  import com.balthazargronon.RCTZeroconf.Zeroconf;
11
14
  import com.balthazargronon.RCTZeroconf.ZeroconfModule;
@@ -18,13 +21,17 @@ import com.facebook.react.bridge.WritableMap;
18
21
  import com.facebook.react.bridge.WritableNativeArray;
19
22
  import com.facebook.react.bridge.WritableNativeMap;
20
23
 
21
- import java.io.UnsupportedEncodingException;
22
24
  import java.net.InetAddress;
23
- import java.util.HashMap;
25
+ import java.nio.charset.StandardCharsets;
26
+ import java.util.ArrayDeque;
24
27
  import java.util.Locale;
25
28
  import java.util.Map;
29
+ import java.util.concurrent.ConcurrentHashMap;
26
30
 
27
31
  public class NsdServiceImpl implements Zeroconf {
32
+ private static final String TAG = "NsdServiceImpl";
33
+ private static final long RESOLVE_RETRY_DELAY_MS = 100;
34
+
28
35
  private NsdManager mNsdManager;
29
36
  private NsdManager.DiscoveryListener mDiscoveryListener;
30
37
  private WifiManager.MulticastLock multicastLock;
@@ -32,10 +39,15 @@ public class NsdServiceImpl implements Zeroconf {
32
39
  private ZeroconfModule zeroconfModule;
33
40
  private ReactApplicationContext reactApplicationContext;
34
41
 
42
+ // NsdManager can only resolve one service at a time before API 34, so resolves are queued
43
+ private final ArrayDeque<NsdServiceInfo> mResolveQueue = new ArrayDeque<>();
44
+ private boolean mIsResolving = false;
45
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
46
+
35
47
  public NsdServiceImpl(ZeroconfModule zeroconfModule, ReactApplicationContext reactApplicationContext) {
36
48
  this.zeroconfModule = zeroconfModule;
37
49
  this.reactApplicationContext = reactApplicationContext;
38
- mPublishedServices = new HashMap<String, NsdManager.RegistrationListener>();
50
+ mPublishedServices = new ConcurrentHashMap<String, NsdManager.RegistrationListener>();
39
51
  }
40
52
 
41
53
  @Override
@@ -68,29 +80,29 @@ public class NsdServiceImpl implements Zeroconf {
68
80
 
69
81
  @Override
70
82
  public void onDiscoveryStarted(String serviceType) {
71
- System.out.println("On Discovery Started");
83
+ Log.d(TAG, "Discovery started");
72
84
  zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_START, null);
73
85
  }
74
86
 
75
87
  @Override
76
88
  public void onDiscoveryStopped(String serviceType) {
77
- System.out.println("On Discovery Stopped");
89
+ Log.d(TAG, "Discovery stopped");
78
90
  zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_STOP, null);
79
91
  }
80
92
 
81
93
  @Override
82
94
  public void onServiceFound(NsdServiceInfo serviceInfo) {
83
- System.out.println("On Service Found");
95
+ Log.d(TAG, "Service found");
84
96
  WritableMap service = new WritableNativeMap();
85
97
  service.putString(ZeroconfModule.KEY_SERVICE_NAME, serviceInfo.getServiceName());
86
98
 
87
99
  zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_FOUND, service);
88
- mNsdManager.resolveService(serviceInfo, new NsdServiceImpl.ZeroResolveListener());
100
+ enqueueResolve(serviceInfo);
89
101
  }
90
102
 
91
103
  @Override
92
104
  public void onServiceLost(NsdServiceInfo serviceInfo) {
93
- System.out.println("On Service Lost");
105
+ Log.d(TAG, "Service lost");
94
106
  WritableMap service = new WritableNativeMap();
95
107
  service.putString(ZeroconfModule.KEY_SERVICE_NAME, serviceInfo.getServiceName());
96
108
  zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_REMOVE, service);
@@ -103,8 +115,19 @@ public class NsdServiceImpl implements Zeroconf {
103
115
 
104
116
  @Override
105
117
  public void stop() {
106
- if (mDiscoveryListener != null) {
107
- mNsdManager.stopServiceDiscovery(mDiscoveryListener);
118
+ if (mDiscoveryListener != null && mNsdManager != null) {
119
+ try {
120
+ mNsdManager.stopServiceDiscovery(mDiscoveryListener);
121
+ } catch (IllegalArgumentException e) {
122
+ // Listener was never registered or already unregistered (e.g. discovery failed to start)
123
+ Log.w(TAG, "stopServiceDiscovery failed", e);
124
+ }
125
+ }
126
+
127
+ mHandler.removeCallbacksAndMessages(null);
128
+ synchronized (mResolveQueue) {
129
+ mResolveQueue.clear();
130
+ mIsResolving = false;
108
131
  }
109
132
 
110
133
  if (multicastLock != null) {
@@ -159,21 +182,60 @@ public class NsdServiceImpl implements Zeroconf {
159
182
  return reactApplicationContext;
160
183
  }
161
184
 
185
+ private void enqueueResolve(NsdServiceInfo serviceInfo) {
186
+ synchronized (mResolveQueue) {
187
+ mResolveQueue.add(serviceInfo);
188
+ }
189
+ resolveNext();
190
+ }
191
+
192
+ private void resolveNext() {
193
+ NsdServiceInfo next;
194
+ synchronized (mResolveQueue) {
195
+ if (mIsResolving || mResolveQueue.isEmpty()) {
196
+ return;
197
+ }
198
+ next = mResolveQueue.poll();
199
+ mIsResolving = true;
200
+ }
201
+ try {
202
+ getNsdManager().resolveService(next, new ZeroResolveListener());
203
+ } catch (Throwable e) {
204
+ Log.e(TAG, "resolveService failed", e);
205
+ zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_ERROR, "Resolving service failed: " + e.getMessage());
206
+ onResolveDone();
207
+ }
208
+ }
209
+
210
+ private void onResolveDone() {
211
+ synchronized (mResolveQueue) {
212
+ mIsResolving = false;
213
+ }
214
+ resolveNext();
215
+ }
216
+
162
217
  private class ZeroResolveListener implements NsdManager.ResolveListener {
163
218
  @Override
164
219
  public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
165
220
  if (errorCode == NsdManager.FAILURE_ALREADY_ACTIVE) {
166
- mNsdManager.resolveService(serviceInfo, this);
167
- } else {
168
- String error = "Resolving service failed with code: " + errorCode;
169
- zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_ERROR, error);
221
+ // Another resolve (possibly from another app or listener) is in flight: retry shortly
222
+ synchronized (mResolveQueue) {
223
+ mResolveQueue.addFirst(serviceInfo);
224
+ mIsResolving = false;
225
+ }
226
+ mHandler.postDelayed(NsdServiceImpl.this::resolveNext, RESOLVE_RETRY_DELAY_MS);
227
+ return;
170
228
  }
229
+ String error = "Resolving service failed with code: " + errorCode;
230
+ zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_ERROR, error);
231
+ onResolveDone();
171
232
  }
172
233
 
173
234
  @Override
174
235
  public void onServiceResolved(NsdServiceInfo serviceInfo) {
175
236
  WritableMap service = serviceInfoToMap(serviceInfo);
176
237
  zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_RESOLVE, service);
238
+ onResolveDone();
177
239
  }
178
240
  }
179
241
 
@@ -194,7 +256,8 @@ public class NsdServiceImpl implements Zeroconf {
194
256
 
195
257
  @Override
196
258
  public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
197
- // Registration failed! Put debugging code here to determine why.
259
+ String error = "Registering service " + serviceInfo.getServiceName() + " failed with code: " + errorCode;
260
+ zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_ERROR, error);
198
261
  }
199
262
 
200
263
  @Override
@@ -207,7 +270,8 @@ public class NsdServiceImpl implements Zeroconf {
207
270
 
208
271
  @Override
209
272
  public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
210
- // Unregistration failed. Put debugging code here to determine why.
273
+ String error = "Unregistering service " + serviceInfo.getServiceName() + " failed with code: " + errorCode;
274
+ zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_ERROR, error);
211
275
  }
212
276
  }
213
277
 
@@ -234,13 +298,10 @@ public class NsdServiceImpl implements Zeroconf {
234
298
 
235
299
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
236
300
  Map<String, byte[]> attributes = serviceInfo.getAttributes();
237
- for (String key : attributes.keySet()) {
238
- try {
301
+ if (attributes != null) {
302
+ for (String key : attributes.keySet()) {
239
303
  byte[] recordValue = attributes.get(key);
240
- txtRecords.putString(String.format(Locale.getDefault(), "%s", key), String.format(Locale.getDefault(), "%s", recordValue != null ? new String(recordValue, "UTF_8") : ""));
241
- } catch (UnsupportedEncodingException e) {
242
- String error = "Failed to encode txtRecord: " + e;
243
- zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_ERROR, error);
304
+ txtRecords.putString(String.format(Locale.getDefault(), "%s", key), recordValue != null ? new String(recordValue, StandardCharsets.UTF_8) : "");
244
305
  }
245
306
  }
246
307
  }
@@ -26,11 +26,14 @@ import java.util.List;
26
26
 
27
27
  import javax.annotation.Nullable;
28
28
 
29
+ import io.reactivex.Flowable;
29
30
  import io.reactivex.android.schedulers.AndroidSchedulers;
30
31
  import io.reactivex.disposables.Disposable;
31
32
  import io.reactivex.schedulers.Schedulers;
32
33
 
33
34
  public class DnssdImpl implements Zeroconf {
35
+ private static final String TAG = "DnssdImpl";
36
+
34
37
  private Rx2Dnssd rxDnssd;
35
38
 
36
39
  @Nullable
@@ -74,26 +77,49 @@ public class DnssdImpl implements Zeroconf {
74
77
  }
75
78
 
76
79
  String serviceType = getServiceType(type, protocol);
77
- Log.d("DnssdImpl", "Starting DNSSD scan for: " + serviceType);
80
+ Log.d(TAG, "Starting DNSSD scan for: " + serviceType);
78
81
 
79
82
  // Emit start event
80
83
  zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_START, null);
81
84
 
82
85
  browseDisposable = rxDnssd.browse(serviceType, "local.")
83
- .compose(rxDnssd.resolve())
84
- .compose(rxDnssd.queryRecords())
86
+ .doOnNext(bonjourService -> {
87
+ if (!bonjourService.isLost()) {
88
+ zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_FOUND, serviceNameToMap(bonjourService));
89
+ }
90
+ })
91
+ // Resolve each service independently so a single failure doesn't end the whole browse
92
+ .flatMap(bonjourService -> bonjourService.isLost()
93
+ ? Flowable.just(bonjourService)
94
+ : Flowable.just(bonjourService)
95
+ .compose(rxDnssd.resolve())
96
+ .compose(rxDnssd.queryRecords())
97
+ .onErrorResumeNext((Throwable throwable) -> {
98
+ Log.e(TAG, "Error resolving service: ", throwable);
99
+ zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_ERROR, "Resolving service failed: " + throwable.getMessage());
100
+ return Flowable.empty();
101
+ }))
85
102
  .subscribeOn(Schedulers.io())
86
103
  .observeOn(AndroidSchedulers.mainThread())
87
104
  .subscribe(bonjourService -> {
88
- WritableMap service = serviceInfoToMap(bonjourService);
89
- Log.d(getClass().getName(), service.toString());
90
- zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_RESOLVE, service);
105
+ if (bonjourService.isLost()) {
106
+ zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_REMOVE, serviceNameToMap(bonjourService));
107
+ return;
108
+ }
109
+ zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_RESOLVE, serviceInfoToMap(bonjourService));
91
110
  }, throwable -> {
92
- Log.e(getClass().getName(), "Error resolving service: ", throwable);
93
- zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_ERROR, throwable.getMessage());
111
+ Log.e(TAG, "Error browsing services: ", throwable);
112
+ zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_ERROR, "Browsing services failed: " + throwable.getMessage());
113
+ zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_STOP, null);
94
114
  });
95
115
  }
96
116
 
117
+ private WritableMap serviceNameToMap(BonjourService serviceInfo) {
118
+ WritableMap service = new WritableNativeMap();
119
+ service.putString(ZeroconfModule.KEY_SERVICE_NAME, serviceInfo.getServiceName());
120
+ return service;
121
+ }
122
+
97
123
  private String getServiceType(String type, String protocol) {
98
124
  return String.format("_%s._%s", type, protocol);
99
125
  }
@@ -102,10 +128,11 @@ public class DnssdImpl implements Zeroconf {
102
128
  WritableMap service = new WritableNativeMap();
103
129
  service.putString(ZeroconfModule.KEY_SERVICE_NAME, serviceInfo.getServiceName());
104
130
  final List<InetAddress> hostList = serviceInfo.getInetAddresses();
105
- final String fullServiceName;
106
- Log.d("TAG", serviceInfo.getServiceName());
107
- fullServiceName = serviceInfo.getServiceName();
108
- service.putString(ZeroconfModule.KEY_SERVICE_HOST, fullServiceName);
131
+ final String hostname = serviceInfo.getHostname();
132
+ final String fullServiceName = hostname != null
133
+ ? hostname + serviceInfo.getRegType()
134
+ : serviceInfo.getServiceName();
135
+ service.putString(ZeroconfModule.KEY_SERVICE_HOST, hostname != null ? hostname : serviceInfo.getServiceName());
109
136
 
110
137
  WritableArray addresses = new WritableNativeArray();
111
138
  for (InetAddress host : hostList) {
@@ -170,12 +197,14 @@ public class DnssdImpl implements Zeroconf {
170
197
  .subscribeOn(Schedulers.io())
171
198
  .observeOn(AndroidSchedulers.mainThread())
172
199
  .subscribe(bonjourService -> {
173
- Log.i("TAG", "Register successfully " + bonjourService.toString());
200
+ Log.i(TAG, "Registered service " + bonjourService.toString());
174
201
 
175
202
  mPublishedServices.put(bs.getServiceName(), bs);
176
203
  zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_PUBLISHED, serviceInfoToMap(bonjourService));
177
204
  }, throwable -> {
178
- Log.e("TAG", "error", throwable);
205
+ Log.e(TAG, "Error registering service: ", throwable);
206
+ zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_ERROR, "Registering service failed: " + throwable.getMessage());
207
+ mRegisteredDisposables.remove(name);
179
208
  });
180
209
 
181
210
  mRegisteredDisposables.put(name, registerDisposable);
package/dist/index.js CHANGED
@@ -14,8 +14,13 @@ const ImplType = exports.ImplType = {
14
14
  class Zeroconf extends _events.EventEmitter {
15
15
  constructor(props) {
16
16
  super(props);
17
+ if (!RNZeroconf) {
18
+ throw new Error('react-native-zeroconf: native module not found. Make sure the library is linked and the app rebuilt. Expo Go is not supported, use a development build instead.');
19
+ }
17
20
  this._services = {};
18
21
  this._publishedServices = {};
22
+ this._publishedImplTypes = {};
23
+ this._scanImplType = null;
19
24
  this._dListeners = {};
20
25
  this.addDeviceListeners();
21
26
  }
@@ -103,6 +108,11 @@ class Zeroconf extends _events.EventEmitter {
103
108
  this._services = {};
104
109
  this.emit('update');
105
110
  if (_reactNative.Platform.OS === 'android') {
111
+ if (this._scanImplType && implType !== this._scanImplType) {
112
+ // Only one scan runs at a time, stop the one running on the other implementation
113
+ RNZeroconf.stop(this._scanImplType);
114
+ }
115
+ this._scanImplType = implType;
106
116
  RNZeroconf.scan(type, protocol, domain, implType);
107
117
  } else {
108
118
  RNZeroconf.scan(type, protocol, domain);
@@ -110,9 +120,10 @@ class Zeroconf extends _events.EventEmitter {
110
120
  }
111
121
 
112
122
  /**
113
- * Stop current scan if any
123
+ * Stop current scan if any,
124
+ * Defaults to the implementation used by the last scan
114
125
  */
115
- stop(implType = ImplType.NSD) {
126
+ stop(implType = this._scanImplType || ImplType.NSD) {
116
127
  if (_reactNative.Platform.OS === 'android') {
117
128
  RNZeroconf.stop(implType);
118
129
  } else {
@@ -124,21 +135,22 @@ class Zeroconf extends _events.EventEmitter {
124
135
  * Publish a service
125
136
  */
126
137
  publishService(type, protocol, domain = 'local.', name, port, txt = {}, implType = ImplType.NSD) {
127
- if (Object.keys(txt).length !== 0) {
128
- Object.entries(txt).map(([key, value]) => txt[key] = value.toString());
129
- }
138
+ const txtRecord = Object.fromEntries(Object.entries(txt || {}).map(([key, value]) => [key, String(value)]));
130
139
  if (_reactNative.Platform.OS === 'android') {
131
- RNZeroconf.registerService(type, protocol, domain, name, port, txt, implType);
140
+ this._publishedImplTypes[name] = implType;
141
+ RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord, implType);
132
142
  } else {
133
- RNZeroconf.registerService(type, protocol, domain, name, port, txt);
143
+ RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord);
134
144
  }
135
145
  }
136
146
 
137
147
  /**
138
- * Unpublish a service
148
+ * Unpublish a service,
149
+ * Defaults to the implementation the service was published with
139
150
  */
140
- unpublishService(name, implType = ImplType.NSD) {
151
+ unpublishService(name, implType = this._publishedImplTypes[name] || ImplType.NSD) {
141
152
  if (_reactNative.Platform.OS === 'android') {
153
+ delete this._publishedImplTypes[name];
142
154
  RNZeroconf.unregisterService(name, implType);
143
155
  } else {
144
156
  RNZeroconf.unregisterService(name);
package/index.d.ts ADDED
@@ -0,0 +1,91 @@
1
+ export declare const ImplType: {
2
+ readonly NSD: 'NSD'
3
+ readonly DNSSD: 'DNSSD'
4
+ }
5
+
6
+ /** Android only: which discovery implementation to use */
7
+ export type ImplType = (typeof ImplType)[keyof typeof ImplType]
8
+
9
+ export interface Service {
10
+ /** Human-readable service name */
11
+ name: string
12
+ /** Full service name, e.g. `XeroxPrinter.local._http._tcp.` */
13
+ fullName: string
14
+ /** Hostname, e.g. `XeroxPrinter.local.` */
15
+ host: string
16
+ port: number
17
+ /** IPv4 and/or IPv6 addresses */
18
+ addresses: string[]
19
+ /** TXT record attributes */
20
+ txt: Record<string, string>
21
+ }
22
+
23
+ export interface ZeroconfEvents {
24
+ start: () => void
25
+ stop: () => void
26
+ /** Service found, before resolution */
27
+ found: (name: string) => void
28
+ /** Service fully resolved with network info */
29
+ resolved: (service: Service) => void
30
+ /** Service removed from the network */
31
+ remove: (name: string) => void
32
+ /** Services list changed */
33
+ update: () => void
34
+ error: (error: Error) => void
35
+ published: (service: Service) => void
36
+ unpublished: (service: Service) => void
37
+ }
38
+
39
+ /** Extends the `events` package EventEmitter */
40
+ export default class Zeroconf {
41
+ constructor()
42
+
43
+ on<E extends keyof ZeroconfEvents>(event: E, listener: ZeroconfEvents[E]): this
44
+ once<E extends keyof ZeroconfEvents>(event: E, listener: ZeroconfEvents[E]): this
45
+ addListener<E extends keyof ZeroconfEvents>(event: E, listener: ZeroconfEvents[E]): this
46
+ removeListener<E extends keyof ZeroconfEvents>(event: E, listener: ZeroconfEvents[E]): this
47
+ off<E extends keyof ZeroconfEvents>(event: E, listener: ZeroconfEvents[E]): this
48
+ removeAllListeners(event?: keyof ZeroconfEvents): this
49
+ listenerCount(event: keyof ZeroconfEvents): number
50
+
51
+ /** Add the native event listeners (called automatically in the constructor) */
52
+ addDeviceListeners(): void
53
+
54
+ /** Remove the native event listeners, call it when you are done with the instance */
55
+ removeDeviceListeners(): void
56
+
57
+ /** All the services found so far, keyed by name */
58
+ getServices(): Record<string, Service>
59
+
60
+ /**
61
+ * Scan for services, defaults to `_http._tcp.` on the `local.` domain
62
+ * @param implType Android only, defaults to `NSD`
63
+ */
64
+ scan(type?: string, protocol?: string, domain?: string, implType?: ImplType): void
65
+
66
+ /**
67
+ * Stop the current scan
68
+ * @param implType Android only, defaults to the implementation used by the last scan
69
+ */
70
+ stop(implType?: ImplType): void
71
+
72
+ /**
73
+ * Publish a service. Type and protocol are passed without underscores, e.g. `('http', 'tcp', ...)`
74
+ * @param implType Android only, defaults to `NSD`
75
+ */
76
+ publishService(
77
+ type: string,
78
+ protocol: string,
79
+ domain: string | undefined,
80
+ name: string,
81
+ port: number,
82
+ txt?: Record<string, string | number | boolean>,
83
+ implType?: ImplType,
84
+ ): void
85
+
86
+ /**
87
+ * Unpublish a service
88
+ * @param implType Android only, defaults to the implementation the service was published with
89
+ */
90
+ unpublishService(name: string, implType?: ImplType): void
91
+ }
@@ -35,9 +35,13 @@ const NSString *kRNServiceTxtRecords = @"txt";
35
35
  NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
36
36
  for (NSString *key in txtRecordDict) {
37
37
  @try{
38
- dict[key] = [[NSString alloc]
39
- initWithData:txtRecordDict[key]
40
- encoding:NSASCIIStringEncoding];
38
+ NSData *value = txtRecordDict[key];
39
+ NSString *string = [[NSString alloc] initWithData:value encoding:NSUTF8StringEncoding];
40
+ if (string == nil) {
41
+ // Not valid UTF-8, fall back to a lossless 8-bit decoding
42
+ string = [[NSString alloc] initWithData:value encoding:NSISOLatin1StringEncoding];
43
+ }
44
+ dict[key] = string;
41
45
  }
42
46
  @catch(NSException *exception){
43
47
  NSLog(@"%@", exception);
@@ -106,6 +106,14 @@ RCT_EXPORT_METHOD(unregisterService:(NSString *) serviceName)
106
106
  return;
107
107
  }
108
108
 
109
+ // Stop any pending resolve for the removed service
110
+ NSNetService *resolving = self.resolvingServices[service.name];
111
+ if (resolving) {
112
+ resolving.delegate = nil;
113
+ [resolving stop];
114
+ [self.resolvingServices removeObjectForKey:service.name];
115
+ }
116
+
109
117
  NSDictionary *serviceInfo = [RNNetServiceSerializer serializeServiceToDictionary:service resolved:NO];
110
118
  [self.bridge.eventDispatcher sendDeviceEventWithName:@"RNZeroconfRemove" body:serviceInfo];
111
119
  }
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "react-native-zeroconf",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "A Zeroconf discovery utility for react-native",
5
5
  "main": "dist",
6
+ "types": "index.d.ts",
6
7
  "scripts": {
7
8
  "lint": "eslint src/*.js",
8
9
  "format": "prettier --write src/*.js",
@@ -15,6 +16,7 @@
15
16
  "files": [
16
17
  "src",
17
18
  "dist",
19
+ "index.d.ts",
18
20
  "android",
19
21
  "ios",
20
22
  "react-native-zeroconf.podspec",
package/src/index.js CHANGED
@@ -12,8 +12,16 @@ export default class Zeroconf extends EventEmitter {
12
12
  constructor(props) {
13
13
  super(props)
14
14
 
15
+ if (!RNZeroconf) {
16
+ throw new Error(
17
+ 'react-native-zeroconf: native module not found. Make sure the library is linked and the app rebuilt. Expo Go is not supported, use a development build instead.',
18
+ )
19
+ }
20
+
15
21
  this._services = {}
16
22
  this._publishedServices = {}
23
+ this._publishedImplTypes = {}
24
+ this._scanImplType = null
17
25
  this._dListeners = {}
18
26
 
19
27
  this.addDeviceListeners()
@@ -122,6 +130,11 @@ export default class Zeroconf extends EventEmitter {
122
130
  this._services = {}
123
131
  this.emit('update')
124
132
  if (Platform.OS === 'android') {
133
+ if (this._scanImplType && implType !== this._scanImplType) {
134
+ // Only one scan runs at a time, stop the one running on the other implementation
135
+ RNZeroconf.stop(this._scanImplType)
136
+ }
137
+ this._scanImplType = implType
125
138
  RNZeroconf.scan(type, protocol, domain, implType)
126
139
  } else {
127
140
  RNZeroconf.scan(type, protocol, domain)
@@ -129,9 +142,10 @@ export default class Zeroconf extends EventEmitter {
129
142
  }
130
143
 
131
144
  /**
132
- * Stop current scan if any
145
+ * Stop current scan if any,
146
+ * Defaults to the implementation used by the last scan
133
147
  */
134
- stop(implType = ImplType.NSD) {
148
+ stop(implType = this._scanImplType || ImplType.NSD) {
135
149
  if (Platform.OS === 'android') {
136
150
  RNZeroconf.stop(implType)
137
151
  } else {
@@ -143,21 +157,24 @@ export default class Zeroconf extends EventEmitter {
143
157
  * Publish a service
144
158
  */
145
159
  publishService(type, protocol, domain = 'local.', name, port, txt = {}, implType = ImplType.NSD) {
146
- if (Object.keys(txt).length !== 0) {
147
- Object.entries(txt).map(([key, value]) => (txt[key] = value.toString()))
148
- }
160
+ const txtRecord = Object.fromEntries(
161
+ Object.entries(txt || {}).map(([key, value]) => [key, String(value)]),
162
+ )
149
163
  if (Platform.OS === 'android') {
150
- RNZeroconf.registerService(type, protocol, domain, name, port, txt, implType)
164
+ this._publishedImplTypes[name] = implType
165
+ RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord, implType)
151
166
  } else {
152
- RNZeroconf.registerService(type, protocol, domain, name, port, txt)
167
+ RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord)
153
168
  }
154
169
  }
155
170
 
156
171
  /**
157
- * Unpublish a service
172
+ * Unpublish a service,
173
+ * Defaults to the implementation the service was published with
158
174
  */
159
- unpublishService(name, implType = ImplType.NSD) {
175
+ unpublishService(name, implType = this._publishedImplTypes[name] || ImplType.NSD) {
160
176
  if (Platform.OS === 'android') {
177
+ delete this._publishedImplTypes[name]
161
178
  RNZeroconf.unregisterService(name, implType)
162
179
  } else {
163
180
  RNZeroconf.unregisterService(name)