react-native-zeroconf 0.16.1 → 0.17.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 (44) hide show
  1. package/NOTICE +0 -3
  2. package/README.md +1 -1
  3. package/android/build.gradle +0 -3
  4. package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroConfImplFactory.java +1 -1
  5. package/android/src/main/java/com/balthazargronon/RCTZeroconf/Zeroconf.java +12 -2
  6. package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroconfModule.java +129 -4
  7. package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroconfOptions.java +72 -0
  8. package/android/src/main/java/com/balthazargronon/RCTZeroconf/dnssd/DnssdImpl.java +633 -0
  9. package/android/src/main/java/com/balthazargronon/RCTZeroconf/nsd/NsdServiceImpl.java +331 -34
  10. package/android/src/main/java/com/balthazargronon/RCTZeroconf/nsd/ServiceTypesQuery.java +296 -0
  11. package/android/src/main/java/com/github/druk/dnssd/DNSSDEmbedded.java +1 -1
  12. package/android/src/main/jni/mdnsresponder/mDNSShared/dnssd_clientshim.c +59 -22
  13. package/dist/index.js +234 -48
  14. package/index.d.ts +101 -6
  15. package/ios/RNZeroconf/RNZeroconf.m +285 -22
  16. package/package.json +28 -4
  17. package/src/index.js +214 -44
  18. package/windows/ExperimentalFeatures.props +33 -0
  19. package/windows/NuGet.config +11 -0
  20. package/windows/RNZeroconf/RNZeroconf.cpp +227 -0
  21. package/windows/RNZeroconf/RNZeroconf.def +3 -0
  22. package/windows/RNZeroconf/RNZeroconf.h +70 -0
  23. package/windows/RNZeroconf/RNZeroconf.rc +0 -0
  24. package/windows/RNZeroconf/RNZeroconf.vcxproj +150 -0
  25. package/windows/RNZeroconf/RNZeroconf.vcxproj.filters +50 -0
  26. package/windows/RNZeroconf/ReactPackageProvider.cpp +20 -0
  27. package/windows/RNZeroconf/ReactPackageProvider.h +24 -0
  28. package/windows/RNZeroconf/ReactPackageProvider.idl +9 -0
  29. package/windows/RNZeroconf/ZeroconfCore.cpp +1042 -0
  30. package/windows/RNZeroconf/ZeroconfCore.h +165 -0
  31. package/windows/RNZeroconf/pch.cpp +1 -0
  32. package/windows/RNZeroconf/pch.h +30 -0
  33. package/windows/RNZeroconf/resource.h +5 -0
  34. package/windows/RNZeroconf/targetver.h +8 -0
  35. package/windows/RNZeroconf.sln +43 -0
  36. package/android/src/main/java/com/balthazargronon/RCTZeroconf/rx2dnssd/DnssdImpl.java +0 -305
  37. package/android/src/main/java/com/github/druk/rx2dnssd/BonjourService.java +0 -400
  38. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2BrowseListener.java +0 -55
  39. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2Dnssd.java +0 -127
  40. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2DnssdCommon.java +0 -280
  41. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2DnssdEmbedded.java +0 -31
  42. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2QueryListener.java +0 -70
  43. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2RegisterListener.java +0 -47
  44. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2ResolveListener.java +0 -51
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = exports.ImplType = void 0;
7
+ exports.useServiceTypes = useServiceTypes;
7
8
  exports.useZeroconf = useZeroconf;
8
9
  var _react = require("react");
9
10
  var _reactNative = require("react-native");
@@ -42,6 +43,12 @@ const withDefaults = (defaults, options) => {
42
43
  };
43
44
  const isOptionsObject = value => value !== null && typeof value === 'object';
44
45
 
46
+ // Options passed to native code, without the undefined ones
47
+ const nativeOptions = options => Object.fromEntries(Object.entries(options).filter(([, value]) => value !== undefined));
48
+
49
+ // Android 17 (API 37) runtime permission, enforced for apps targeting API 37 or declaring it
50
+ const ACCESS_LOCAL_NETWORK = 'android.permission.ACCESS_LOCAL_NETWORK';
51
+
45
52
  // Each instance runs its own scan, identified by this id in native events
46
53
  let instanceCount = 0;
47
54
 
@@ -97,6 +104,20 @@ const withAddressFamilies = service => {
97
104
  /**
98
105
  * TXT records as ordered [key, value] pairs, from an object or an array of pairs
99
106
  */
107
+ // Browsing this type lists the service types on the network instead of services
108
+ const SERVICE_TYPES = {
109
+ type: 'services._dns-sd',
110
+ protocol: 'udp'
111
+ };
112
+
113
+ // "_http._tcp" -> { type: 'http', protocol: 'tcp' }
114
+ const parseServiceType = name => {
115
+ const match = /^_([^.]+)\._(tcp|udp)$/.exec(name);
116
+ return match ? {
117
+ type: match[1],
118
+ protocol: match[2]
119
+ } : null;
120
+ };
100
121
  const toTxtPairs = txt => {
101
122
  const entries = Array.isArray(txt) ? txt : Object.entries(txt || {});
102
123
  return entries.map(([key, value]) => [String(key), String(value)]);
@@ -108,6 +129,8 @@ class Zeroconf extends _events.EventEmitter {
108
129
  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.');
109
130
  }
110
131
  this._services = {};
132
+ this._serviceTypes = {};
133
+ this._scanningTypes = false;
111
134
  this._publishedServices = {};
112
135
  this._publishedImplTypes = {};
113
136
  this._scanImplType = null;
@@ -146,6 +169,15 @@ class Zeroconf extends _events.EventEmitter {
146
169
  const {
147
170
  name
148
171
  } = service;
172
+ if (this._scanningTypes) {
173
+ const serviceType = parseServiceType(name);
174
+ if (serviceType && !this._serviceTypes[name]) {
175
+ this._serviceTypes[name] = serviceType;
176
+ this.emit('typeFound', serviceType);
177
+ this.emit('update');
178
+ }
179
+ return;
180
+ }
149
181
  this._services[name] = {
150
182
  name
151
183
  };
@@ -159,6 +191,15 @@ class Zeroconf extends _events.EventEmitter {
159
191
  const {
160
192
  name
161
193
  } = service;
194
+ if (this._scanningTypes) {
195
+ const serviceType = this._serviceTypes[name];
196
+ if (serviceType) {
197
+ delete this._serviceTypes[name];
198
+ this.emit('typeRemove', serviceType);
199
+ this.emit('update');
200
+ }
201
+ return;
202
+ }
162
203
  delete this._services[name];
163
204
  this.emit('remove', name);
164
205
  this.emit('update');
@@ -226,6 +267,33 @@ class Zeroconf extends _events.EventEmitter {
226
267
  return this._services;
227
268
  }
228
269
 
270
+ /**
271
+ * Get the service types found by scanServiceTypes, as [{ type, protocol }]
272
+ */
273
+ getServiceTypes() {
274
+ return Object.values(this._serviceTypes);
275
+ }
276
+
277
+ /**
278
+ * Scan for the service types advertised on the network (_http._tcp, _ipp._tcp...), emits typeFound and typeRemove.
279
+ * On iOS, only types declared in NSBonjourServices can then be scanned.
280
+ * On iOS it requires the multicast entitlement. On Android with NSD and on Windows, services other apps publish on the same device are left out.
281
+ *
282
+ * scanServiceTypes({ domain, implType })
283
+ */
284
+ scanServiceTypes({
285
+ domain,
286
+ implType,
287
+ networkInterface
288
+ } = {}) {
289
+ this._startScan({
290
+ ...SERVICE_TYPES,
291
+ domain,
292
+ implType,
293
+ networkInterface
294
+ }, true);
295
+ }
296
+
229
297
  /**
230
298
  * Scan for Zeroconf services, defaults to _http._tcp. on the local. domain
231
299
  *
@@ -233,19 +301,26 @@ class Zeroconf extends _events.EventEmitter {
233
301
  * scan(type, protocol, domain, implType) is deprecated
234
302
  */
235
303
  scan(options, protocolArg, domainArg, implTypeArg) {
304
+ this._startScan(isOptionsObject(options) ? options : {
305
+ type: options,
306
+ protocol: protocolArg,
307
+ domain: domainArg,
308
+ implType: implTypeArg
309
+ }, false);
310
+ }
311
+ _startScan(options, scanningTypes) {
236
312
  const {
237
313
  type,
238
314
  protocol,
239
315
  domain,
240
316
  implType,
241
- resolveTimeout
242
- } = withDefaults(SCAN_DEFAULTS, isOptionsObject(options) ? options : {
243
- type: options,
244
- protocol: protocolArg,
245
- domain: domainArg,
246
- implType: implTypeArg
247
- });
317
+ resolveTimeout,
318
+ subtype,
319
+ networkInterface
320
+ } = withDefaults(SCAN_DEFAULTS, options);
248
321
  this._services = {};
322
+ this._serviceTypes = {};
323
+ this._scanningTypes = scanningTypes;
249
324
  this._hasScanned = true;
250
325
  this.emit('update');
251
326
  if (_reactNative.Platform.OS === 'android') {
@@ -254,9 +329,16 @@ class Zeroconf extends _events.EventEmitter {
254
329
  RNZeroconf.stop(this._scanId, this._scanImplType);
255
330
  }
256
331
  this._scanImplType = implType;
257
- RNZeroconf.scan(this._scanId, type, protocol, domain, implType);
332
+ RNZeroconf.scan(this._scanId, type, protocol, domain, implType, nativeOptions({
333
+ subtype,
334
+ networkInterface
335
+ }));
258
336
  } else {
259
- RNZeroconf.scan(this._scanId, type, protocol, domain, resolveTimeout);
337
+ RNZeroconf.scan(this._scanId, type, protocol, domain, nativeOptions({
338
+ resolveTimeout,
339
+ subtype,
340
+ networkInterface
341
+ }));
260
342
  }
261
343
  }
262
344
 
@@ -273,17 +355,27 @@ class Zeroconf extends _events.EventEmitter {
273
355
  }
274
356
 
275
357
  /**
276
- * Checks the iOS Local Network permission, resolves 'granted', 'denied' or 'unknown'.
277
- * Uses a service type from NSBonjourServices (the first one by default) and can show the permission prompt.
278
- * Android has no equivalent permission to check and resolves 'unknown'.
358
+ * Checks the Local Network permission, resolves 'granted', 'denied' or 'unknown'.
359
+ * iOS: uses a service type from NSBonjourServices (the first one by default) and can show the permission prompt.
360
+ * Android: apps targeting Android 17 (API 37) need ACCESS_LOCAL_NETWORK on Android 17 devices, it is requested
361
+ * when missing unless request is false. 'granted' when there is nothing to grant.
279
362
  */
280
- checkLocalNetworkAccess({
363
+ async checkLocalNetworkAccess({
281
364
  type,
282
365
  protocol = 'tcp',
283
- timeout = 5
366
+ timeout = 5,
367
+ request = true
284
368
  } = {}) {
285
369
  if (_reactNative.Platform.OS === 'android') {
286
- return Promise.resolve('unknown');
370
+ if (!RNZeroconf.checkLocalNetworkAccess) {
371
+ return 'unknown';
372
+ }
373
+ const status = await asPromise(RNZeroconf.checkLocalNetworkAccess());
374
+ if (status !== 'denied' || !request) {
375
+ return status;
376
+ }
377
+ const result = await _reactNative.PermissionsAndroid.request(ACCESS_LOCAL_NETWORK);
378
+ return result === _reactNative.PermissionsAndroid.RESULTS.GRANTED ? 'granted' : 'denied';
287
379
  }
288
380
  const serviceType = type ? `_${type}._${protocol}` : null;
289
381
  return asPromise(RNZeroconf.checkLocalNetworkAccess(serviceType, timeout));
@@ -304,7 +396,9 @@ class Zeroconf extends _events.EventEmitter {
304
396
  name,
305
397
  port,
306
398
  txt,
307
- implType
399
+ implType,
400
+ subtypes,
401
+ networkInterface
308
402
  } = withDefaults(PUBLISH_DEFAULTS, isOptionsObject(options) ? options : {
309
403
  type: options,
310
404
  protocol: protocolArg,
@@ -315,11 +409,59 @@ class Zeroconf extends _events.EventEmitter {
315
409
  implType: implTypeArg
316
410
  });
317
411
  const txtRecord = toTxtPairs(txt);
412
+ const native = nativeOptions({
413
+ subtypes,
414
+ networkInterface
415
+ });
318
416
  if (_reactNative.Platform.OS === 'android') {
319
417
  this._publishedImplTypes[name] = implType;
320
- return asPromise(RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord, implType));
418
+ return asPromise(RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord, implType, native));
419
+ }
420
+ return asPromise(RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord, native));
421
+ }
422
+
423
+ /**
424
+ * Replace the TXT record of a published service, resolves with the updated service.
425
+ * Android NSD has no update, the service is published again under the same name.
426
+ *
427
+ * updateService(name, { txt })
428
+ */
429
+ updateService(name, {
430
+ txt = {},
431
+ implType = this._publishedImplTypes[name] || ImplType.NSD
432
+ } = {}) {
433
+ const txtRecord = toTxtPairs(txt);
434
+ if (_reactNative.Platform.OS === 'android') {
435
+ return asPromise(RNZeroconf.updateService(name, txtRecord, implType));
321
436
  }
322
- return asPromise(RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord));
437
+ return asPromise(RNZeroconf.updateService(name, txtRecord));
438
+ }
439
+
440
+ /**
441
+ * Resolve one service by name without scanning, for a device found before.
442
+ * Resolves with the service, rejects with code 'TIMEOUT' when it doesn't answer in time.
443
+ *
444
+ * resolveService({ name, type, protocol, domain, implType, timeout, networkInterface })
445
+ */
446
+ resolveService(options) {
447
+ const {
448
+ name,
449
+ type,
450
+ protocol,
451
+ domain,
452
+ implType,
453
+ timeout,
454
+ networkInterface
455
+ } = withDefaults({
456
+ ...SCAN_DEFAULTS,
457
+ timeout: 5
458
+ }, options);
459
+ const native = nativeOptions({
460
+ timeout,
461
+ networkInterface
462
+ });
463
+ const promise = _reactNative.Platform.OS === 'android' ? RNZeroconf.resolveService(name, type, protocol, domain, implType, native) : RNZeroconf.resolveService(name, type, protocol, domain, native);
464
+ return asPromise(asPromise(promise).then(service => service && withAddressFamilies(service)));
323
465
  }
324
466
 
325
467
  /**
@@ -335,34 +477,20 @@ class Zeroconf extends _events.EventEmitter {
335
477
  }
336
478
  }
337
479
 
338
- /**
339
- * Scans while mounted and returns the resolved services.
340
- * Scans again when the options change, stops and cleans up on unmount.
341
- * Each hook runs its own scan, several can run at once.
342
- *
343
- * const { services, isScanning, error, stop, restart } = useZeroconf({ type: 'http' })
344
- */
480
+ // Runs a scan while mounted and enabled, and again when deps change or on restart
345
481
  exports.default = Zeroconf;
346
- function useZeroconf(options = {}) {
347
- const {
348
- type,
349
- protocol,
350
- domain,
351
- implType,
352
- resolveTimeout,
353
- enabled = true
354
- } = options;
482
+ function useScan(enabled, startScan, read, deps) {
355
483
  const zeroconfRef = (0, _react.useRef)(null);
356
- const [services, setServices] = (0, _react.useState)([]);
484
+ const [items, setItems] = (0, _react.useState)([]);
357
485
  const [isScanning, setIsScanning] = (0, _react.useState)(false);
358
486
  const [error, setError] = (0, _react.useState)(null);
359
487
  const [scanCount, setScanCount] = (0, _react.useState)(0);
488
+ const readRef = (0, _react.useRef)(read);
489
+ readRef.current = read;
360
490
  (0, _react.useEffect)(() => {
361
491
  const zeroconf = new Zeroconf();
362
492
  zeroconfRef.current = zeroconf;
363
- // Found services only have a name until they are resolved
364
- const updateServices = () => setServices(Object.values(zeroconf.getServices()).filter(service => service.addresses));
365
- const unsubscribes = [zeroconf.subscribe('start', () => setIsScanning(true)), zeroconf.subscribe('stop', () => setIsScanning(false)), zeroconf.subscribe('update', updateServices), zeroconf.subscribe('error', setError)];
493
+ const unsubscribes = [zeroconf.subscribe('start', () => setIsScanning(true)), zeroconf.subscribe('stop', () => setIsScanning(false)), zeroconf.subscribe('update', () => setItems(readRef.current(zeroconf))), zeroconf.subscribe('error', setError)];
366
494
  return () => {
367
495
  unsubscribes.forEach(unsubscribe => unsubscribe());
368
496
  zeroconf.stop();
@@ -375,17 +503,11 @@ function useZeroconf(options = {}) {
375
503
  if (!zeroconf || !enabled) {
376
504
  return undefined;
377
505
  }
378
- setServices([]);
506
+ setItems([]);
379
507
  setError(null);
380
- zeroconf.scan({
381
- type,
382
- protocol,
383
- domain,
384
- implType,
385
- resolveTimeout
386
- });
508
+ startScan(zeroconf);
387
509
  return () => zeroconf.stop();
388
- }, [enabled, type, protocol, domain, implType, resolveTimeout, scanCount]);
510
+ }, [enabled, scanCount, ...deps]);
389
511
  const stop = (0, _react.useCallback)(() => {
390
512
  if (zeroconfRef.current) {
391
513
  zeroconfRef.current.stop();
@@ -393,10 +515,74 @@ function useZeroconf(options = {}) {
393
515
  }, []);
394
516
  const restart = (0, _react.useCallback)(() => setScanCount(count => count + 1), []);
395
517
  return {
396
- services,
518
+ items,
397
519
  isScanning,
398
520
  error,
399
521
  stop,
400
522
  restart
401
523
  };
402
524
  }
525
+
526
+ /**
527
+ * Scans while mounted and returns the resolved services.
528
+ * Scans again when the options change, stops and cleans up on unmount.
529
+ * Each hook runs its own scan, several can run at once.
530
+ *
531
+ * const { services, isScanning, error, stop, restart } = useZeroconf({ type: 'http' })
532
+ */
533
+ function useZeroconf(options = {}) {
534
+ const {
535
+ type,
536
+ protocol,
537
+ domain,
538
+ implType,
539
+ resolveTimeout,
540
+ subtype,
541
+ networkInterface,
542
+ enabled = true
543
+ } = options;
544
+ const {
545
+ items,
546
+ ...scan
547
+ } = useScan(enabled, zeroconf => zeroconf.scan({
548
+ type,
549
+ protocol,
550
+ domain,
551
+ implType,
552
+ resolveTimeout,
553
+ subtype,
554
+ networkInterface
555
+ }),
556
+ // Found services only have a name until they are resolved
557
+ zeroconf => Object.values(zeroconf.getServices()).filter(service => service.addresses), [type, protocol, domain, implType, resolveTimeout, subtype, networkInterface]);
558
+ return {
559
+ services: items,
560
+ ...scan
561
+ };
562
+ }
563
+
564
+ /**
565
+ * Lists the service types advertised on the network while mounted.
566
+ *
567
+ * const { serviceTypes, isScanning, error, stop, restart } = useServiceTypes()
568
+ */
569
+ function useServiceTypes(options = {}) {
570
+ const {
571
+ domain,
572
+ implType,
573
+ networkInterface,
574
+ enabled = true
575
+ } = options;
576
+ const {
577
+ items,
578
+ ...scan
579
+ } = useScan(enabled, zeroconf => zeroconf.scanServiceTypes({
580
+ domain,
581
+ implType,
582
+ networkInterface
583
+ }), zeroconf => zeroconf.getServiceTypes(), [domain, implType, networkInterface]);
584
+ return {
585
+ serviceTypes: items,
586
+ ...scan
587
+ };
588
+ }
package/index.d.ts CHANGED
@@ -38,6 +38,13 @@ export interface ScanOptions {
38
38
  implType?: ImplType
39
39
  /** iOS only, seconds to try resolving a service before retrying once and giving up. Defaults to `5` */
40
40
  resolveTimeout?: number
41
+ /** Only find services registered with this subtype, e.g. `'printer'` */
42
+ subtype?: string
43
+ /**
44
+ * Network interface to scan on, e.g. `'en0'` on iOS or `'wlan0'` on Android. All of them by default.
45
+ * Android `NSD` needs Android 13 or later for it
46
+ */
47
+ networkInterface?: string
41
48
  }
42
49
 
43
50
  export interface PublishOptions {
@@ -53,6 +60,34 @@ export interface PublishOptions {
53
60
  txt?: TxtRecord
54
61
  /** Android only, defaults to `NSD` */
55
62
  implType?: ImplType
63
+ /** Subtypes to register the service with, e.g. `['printer']` */
64
+ subtypes?: string[]
65
+ /** Network interface to publish on, all of them by default. Android `NSD` needs Android 13 or later for it */
66
+ networkInterface?: string
67
+ }
68
+
69
+ export interface UpdateOptions {
70
+ /** The new TXT record, replaces the current one */
71
+ txt?: TxtRecord
72
+ /** Android only, defaults to the implementation the service was published with */
73
+ implType?: ImplType
74
+ }
75
+
76
+ export interface ResolveOptions {
77
+ /** Name of the service */
78
+ name: string
79
+ /** Service type without underscore, e.g. `'http'`. Defaults to `'http'` */
80
+ type?: string
81
+ /** Defaults to `'tcp'` */
82
+ protocol?: string
83
+ /** Defaults to `'local.'` */
84
+ domain?: string
85
+ /** Android only, defaults to `NSD` */
86
+ implType?: ImplType
87
+ /** Seconds before rejecting with code `'TIMEOUT'`, defaults to `5` */
88
+ timeout?: number
89
+ /** Network interface to resolve on, all of them by default */
90
+ networkInterface?: string
56
91
  }
57
92
 
58
93
  /**
@@ -61,11 +96,12 @@ export interface PublishOptions {
61
96
  * - `DNSSD`: DNSServiceErrorType, on iOS and from Android's embedded mDNSResponder,
62
97
  * e.g. `-65555` type missing from NSBonjourServices, `-65570` Local Network access denied
63
98
  * - `NsdManager`: Android NsdManager failure code
64
- * - `RNZeroconf`: the library's own errors, e.g. `'EXCEPTION'`
99
+ * - `Windows`: Win32 / DNS_STATUS code from the Windows DNS-SD functions
100
+ * - `RNZeroconf`: the library's own errors, e.g. `'EXCEPTION'`, `'TIMEOUT'`, `'UNKNOWN_INTERFACE'`
65
101
  */
66
102
  export interface ZeroconfError extends Error {
67
103
  code: number | string
68
- domain: 'DNSSD' | 'NsdManager' | 'RNZeroconf'
104
+ domain: 'DNSSD' | 'NsdManager' | 'RNZeroconf' | 'Windows'
69
105
  /** The service the error is about, when there is one */
70
106
  serviceName?: string
71
107
  }
@@ -75,8 +111,10 @@ export interface LocalNetworkAccessOptions {
75
111
  type?: string
76
112
  /** Defaults to `'tcp'` */
77
113
  protocol?: string
78
- /** Seconds to wait for an answer, defaults to `5` */
114
+ /** iOS: seconds to wait for an answer, defaults to `5` */
79
115
  timeout?: number
116
+ /** Android: request `ACCESS_LOCAL_NETWORK` when it is missing, defaults to `true` */
117
+ request?: boolean
80
118
  }
81
119
 
82
120
  /** Published services don't carry the address family fields */
@@ -91,13 +129,32 @@ export interface ZeroconfEvents {
91
129
  resolved: (service: Service) => void
92
130
  /** Service removed from the network */
93
131
  remove: (name: string) => void
94
- /** Services list changed */
132
+ /** Services or service types list changed */
95
133
  update: () => void
134
+ /** Service type found by `scanServiceTypes` */
135
+ typeFound: (serviceType: ServiceType) => void
136
+ /** Service type no longer advertised, during `scanServiceTypes` */
137
+ typeRemove: (serviceType: ServiceType) => void
96
138
  error: (error: ZeroconfError) => void
97
139
  published: (service: PublishedService) => void
98
140
  unpublished: (service: PublishedService) => void
99
141
  }
100
142
 
143
+ /** A service type advertised on the network, e.g. `{ type: 'http', protocol: 'tcp' }` for `_http._tcp` */
144
+ export interface ServiceType {
145
+ type: string
146
+ protocol: 'tcp' | 'udp'
147
+ }
148
+
149
+ export interface ServiceTypesScanOptions {
150
+ /** Defaults to `local.` */
151
+ domain?: string
152
+ /** Android only, defaults to `ImplType.NSD`. With `NSD`, services other apps publish on the same phone are left out (this app's own are included), `DNSSD` includes them */
153
+ implType?: ImplType
154
+ /** Network interface to list the types on, all of them by default */
155
+ networkInterface?: string
156
+ }
157
+
101
158
  /** Extends the `events` package EventEmitter */
102
159
  export default class Zeroconf {
103
160
  constructor()
@@ -121,14 +178,24 @@ export default class Zeroconf {
121
178
  /** All the services found so far, keyed by name */
122
179
  getServices(): Record<string, Service>
123
180
 
181
+ /** Service types found by `scanServiceTypes` */
182
+ getServiceTypes(): ServiceType[]
183
+
184
+ /**
185
+ * Scan for the service types advertised on the network, emits `typeFound` and `typeRemove`.
186
+ * On iOS it requires the `com.apple.developer.networking.multicast` entitlement.
187
+ */
188
+ scanServiceTypes(options?: ServiceTypesScanOptions): void
189
+
124
190
  /** Scan for services, defaults to `_http._tcp.` on the `local.` domain */
125
191
  scan(options?: ScanOptions): void
126
192
  /** @deprecated Use `scan({ type, protocol, domain, implType })` */
127
193
  scan(type?: string, protocol?: string, domain?: string, implType?: ImplType): void
128
194
 
129
195
  /**
130
- * Checks the iOS Local Network permission. Can show the permission prompt when it hasn't been answered yet.
131
- * Resolves `'unknown'` on Android, which has no equivalent permission to check.
196
+ * Checks the Local Network permission. iOS can show the permission prompt when it hasn't been answered yet.
197
+ * Android 17 (API 37): apps targeting API 37 need `ACCESS_LOCAL_NETWORK`, requested when missing unless
198
+ * `request` is `false`. Resolves `'granted'` when there is nothing to grant.
132
199
  */
133
200
  checkLocalNetworkAccess(
134
201
  options?: LocalNetworkAccessOptions,
@@ -162,6 +229,15 @@ export default class Zeroconf {
162
229
  * @param implType Android only, defaults to the implementation the service was published with
163
230
  */
164
231
  unpublishService(name: string, implType?: ImplType): Promise<PublishedService | null>
232
+
233
+ /**
234
+ * Replace the TXT record of a published service, resolves with the updated service.
235
+ * Android `NSD` has no update: the service is published again under the same name.
236
+ */
237
+ updateService(name: string, options: UpdateOptions): Promise<PublishedService>
238
+
239
+ /** Resolve one service by name without scanning. Rejects with code `'TIMEOUT'` when it doesn't answer in time */
240
+ resolveService(options: ResolveOptions): Promise<Service>
165
241
  }
166
242
 
167
243
  export interface UseZeroconfOptions extends ScanOptions {
@@ -185,3 +261,22 @@ export interface UseZeroconfResult {
185
261
  * stops and cleans up on unmount. Each hook runs its own scan, several can run at once.
186
262
  */
187
263
  export function useZeroconf(options?: UseZeroconfOptions): UseZeroconfResult
264
+
265
+ export interface UseServiceTypesOptions extends ServiceTypesScanOptions {
266
+ /** Scan only while true, defaults to `true` */
267
+ enabled?: boolean
268
+ }
269
+
270
+ export interface UseServiceTypesResult {
271
+ /** Service types advertised on the network, updated as they appear and disappear */
272
+ serviceTypes: ServiceType[]
273
+ isScanning: boolean
274
+ /** Last error, reset when a new scan starts */
275
+ error: ZeroconfError | null
276
+ stop(): void
277
+ /** Clears the service types and scans again */
278
+ restart(): void
279
+ }
280
+
281
+ /** Lists the service types advertised on the network while mounted */
282
+ export function useServiceTypes(options?: UseServiceTypesOptions): UseServiceTypesResult