react-native-zeroconf 0.17.2 → 1.0.0-rc.1

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 (117) hide show
  1. package/NOTICE +2 -6
  2. package/README.md +1 -1
  3. package/android/CMakeLists.txt +64 -0
  4. package/android/build.gradle +1 -13
  5. package/android/generated/java/com/balthazargronon/RCTZeroconf/NativeZeroconfSpec.java +96 -0
  6. package/android/generated/jni/CMakeLists.txt +28 -0
  7. package/android/generated/jni/RNZeroconfSpec-generated.cpp +77 -0
  8. package/android/generated/jni/RNZeroconfSpec.h +31 -0
  9. package/android/generated/jni/react/renderer/components/RNZeroconfSpec/RNZeroconfSpecJSI.h +798 -0
  10. package/android/src/main/java/com/balthazargronon/RCTZeroconf/{ZeroconfModule.java → NsdHost.java} +97 -140
  11. package/android/src/main/java/com/balthazargronon/RCTZeroconf/NsdPayload.java +87 -0
  12. package/android/src/main/java/com/balthazargronon/RCTZeroconf/NsdPromise.java +40 -0
  13. package/android/src/main/java/com/balthazargronon/RCTZeroconf/Zeroconf.java +4 -6
  14. package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroconfNativeSupport.java +60 -0
  15. package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroconfOptions.java +15 -26
  16. package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroconfReactPackage.java +5 -15
  17. package/android/src/main/java/com/balthazargronon/RCTZeroconf/nsd/NsdServiceImpl.java +90 -96
  18. package/android/src/main/jni/mdnsresponder/mDNSPosix/PosixDaemon.c +19 -1
  19. package/android/src/main/jni/mdnsresponder/mDNSPosix/mDNSPosix.c +104 -1
  20. package/android/src/main/jni/mdnsresponder/mDNSPosix/mDNSPosix.h +7 -0
  21. package/android/src/main/jni/mdnsresponder/mDNSShared/dnssd_clientshim.c +34 -9
  22. package/cpp/Backend.cpp +16 -0
  23. package/cpp/Backend.h +116 -0
  24. package/cpp/ZeroconfModule.cpp +280 -0
  25. package/cpp/ZeroconfModule.h +144 -0
  26. package/cpp/android/NsdBackend.cpp +462 -0
  27. package/cpp/android/NsdBackend.h +61 -0
  28. package/cpp/android/ZeroconfAndroidModule.cpp +49 -0
  29. package/cpp/android/ZeroconfAndroidModule.h +15 -0
  30. package/cpp/apple/DispatchExecutor.cpp +66 -0
  31. package/cpp/apple/DispatchExecutor.h +37 -0
  32. package/cpp/apple/LocalNetworkAccess.h +19 -0
  33. package/cpp/apple/LocalNetworkAccess.mm +87 -0
  34. package/cpp/dnssd/DnssdBackend.cpp +976 -0
  35. package/cpp/dnssd/DnssdBackend.h +114 -0
  36. package/cpp/dnssd/Executor.h +28 -0
  37. package/cpp/embedded/EmbeddedExecutor.cpp +124 -0
  38. package/cpp/embedded/EmbeddedExecutor.h +57 -0
  39. package/ios/ZeroconfOnLoad.mm +34 -0
  40. package/ios/generated/ReactCodegen/RNZeroconfSpec/RNZeroconfSpec-generated.mm +140 -0
  41. package/ios/generated/ReactCodegen/RNZeroconfSpec/RNZeroconfSpec.h +189 -0
  42. package/ios/generated/ReactCodegen/RNZeroconfSpecJSI.h +798 -0
  43. package/lib/module/NativeZeroconf.js +15 -0
  44. package/lib/module/NativeZeroconf.js.map +1 -0
  45. package/lib/module/bridge.js +124 -0
  46. package/lib/module/bridge.js.map +1 -0
  47. package/{dist → lib/module}/index.js +149 -149
  48. package/lib/module/index.js.map +1 -0
  49. package/lib/module/package.json +1 -0
  50. package/lib/module/types.js +24 -0
  51. package/lib/module/types.js.map +1 -0
  52. package/lib/typescript/package.json +1 -0
  53. package/lib/typescript/src/NativeZeroconf.d.ts +75 -0
  54. package/lib/typescript/src/NativeZeroconf.d.ts.map +1 -0
  55. package/lib/typescript/src/bridge.d.ts +22 -0
  56. package/lib/typescript/src/bridge.d.ts.map +1 -0
  57. package/lib/typescript/src/index.d.ts +111 -0
  58. package/lib/typescript/src/index.d.ts.map +1 -0
  59. package/lib/typescript/src/types.d.ts +175 -0
  60. package/lib/typescript/src/types.d.ts.map +1 -0
  61. package/package.json +65 -29
  62. package/react-native-zeroconf.podspec +2 -1
  63. package/react-native.config.js +17 -0
  64. package/src/NativeZeroconf.ts +120 -0
  65. package/src/bridge.ts +153 -0
  66. package/src/index.ts +607 -0
  67. package/{index.d.ts → src/types.ts} +5 -99
  68. package/windows/RNZeroconf/RNZeroconf.vcxproj +17 -2
  69. package/windows/RNZeroconf/RNZeroconf.vcxproj.filters +0 -6
  70. package/windows/RNZeroconf/ReactPackageProvider.cpp +23 -20
  71. package/windows/RNZeroconf/WindowsBackend.cpp +156 -0
  72. package/windows/RNZeroconf/WindowsBackend.h +13 -0
  73. package/windows/RNZeroconf/ZeroconfCore.cpp +2 -2
  74. package/windows/RNZeroconf/ZeroconfCore.h +2 -2
  75. package/windows/RNZeroconf/ZeroconfWindowsModule.cpp +22 -0
  76. package/windows/RNZeroconf/ZeroconfWindowsModule.h +14 -0
  77. package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroConfImplFactory.java +0 -56
  78. package/android/src/main/java/com/balthazargronon/RCTZeroconf/dnssd/DnssdImpl.java +0 -654
  79. package/android/src/main/java/com/github/druk/dnssd/BaseListener.java +0 -33
  80. package/android/src/main/java/com/github/druk/dnssd/BrowseListener.java +0 -67
  81. package/android/src/main/java/com/github/druk/dnssd/DNSRecord.java +0 -47
  82. package/android/src/main/java/com/github/druk/dnssd/DNSSD.java +0 -650
  83. package/android/src/main/java/com/github/druk/dnssd/DNSSDEmbedded.java +0 -135
  84. package/android/src/main/java/com/github/druk/dnssd/DNSSDException.java +0 -66
  85. package/android/src/main/java/com/github/druk/dnssd/DNSSDRecordRegistrar.java +0 -55
  86. package/android/src/main/java/com/github/druk/dnssd/DNSSDRegistration.java +0 -54
  87. package/android/src/main/java/com/github/druk/dnssd/DNSSDService.java +0 -35
  88. package/android/src/main/java/com/github/druk/dnssd/DomainListener.java +0 -57
  89. package/android/src/main/java/com/github/druk/dnssd/InternalBrowseListener.java +0 -76
  90. package/android/src/main/java/com/github/druk/dnssd/InternalDNSSD.java +0 -870
  91. package/android/src/main/java/com/github/druk/dnssd/InternalDNSSDRecordRegistrar.java +0 -30
  92. package/android/src/main/java/com/github/druk/dnssd/InternalDNSSDRegistration.java +0 -34
  93. package/android/src/main/java/com/github/druk/dnssd/InternalDNSSDService.java +0 -29
  94. package/android/src/main/java/com/github/druk/dnssd/InternalDomainListener.java +0 -62
  95. package/android/src/main/java/com/github/druk/dnssd/InternalQueryListener.java +0 -61
  96. package/android/src/main/java/com/github/druk/dnssd/InternalRegisterListener.java +0 -51
  97. package/android/src/main/java/com/github/druk/dnssd/InternalResolveListener.java +0 -58
  98. package/android/src/main/java/com/github/druk/dnssd/NSClass.java +0 -16
  99. package/android/src/main/java/com/github/druk/dnssd/NSType.java +0 -58
  100. package/android/src/main/java/com/github/druk/dnssd/QueryListener.java +0 -54
  101. package/android/src/main/java/com/github/druk/dnssd/RegisterListener.java +0 -46
  102. package/android/src/main/java/com/github/druk/dnssd/RegisterRecordListener.java +0 -35
  103. package/android/src/main/java/com/github/druk/dnssd/ResolveListener.java +0 -55
  104. package/android/src/main/java/com/github/druk/dnssd/TXTRecord.java +0 -293
  105. package/android/src/main/jni/Android.mk +0 -73
  106. package/android/src/main/jni/Application.mk +0 -3
  107. package/android/src/main/jni/DNSSD.java.h +0 -290
  108. package/android/src/main/jni/JNISupport.c +0 -1140
  109. package/ios/RNZeroconf/RNNetServiceSerializer.h +0 -31
  110. package/ios/RNZeroconf/RNNetServiceSerializer.m +0 -84
  111. package/ios/RNZeroconf/RNZeroconf.h +0 -17
  112. package/ios/RNZeroconf/RNZeroconf.m +0 -1025
  113. package/ios/RNZeroconf.xcodeproj/project.pbxproj +0 -261
  114. package/ios/RNZeroconf.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  115. package/src/index.js +0 -563
  116. package/windows/RNZeroconf/RNZeroconf.cpp +0 -228
  117. package/windows/RNZeroconf/RNZeroconf.h +0 -70
package/src/index.ts ADDED
@@ -0,0 +1,607 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react'
2
+ import { Platform, PermissionsAndroid } from 'react-native'
3
+ import { EventEmitter } from 'events'
4
+
5
+ import { getBridge, type Bridge, type BridgePayload, type LocalNetworkAccess } from './bridge'
6
+ import {
7
+ ImplType,
8
+ type LocalNetworkAccessOptions,
9
+ type PublishedService,
10
+ type PublishOptions,
11
+ type ResolveOptions,
12
+ type ScanOptions,
13
+ type Service,
14
+ type ServiceType,
15
+ type ServiceTypesScanOptions,
16
+ type TxtRecord,
17
+ type UpdateOptions,
18
+ type UseServiceTypesOptions,
19
+ type UseServiceTypesResult,
20
+ type UseZeroconfOptions,
21
+ type UseZeroconfResult,
22
+ type ZeroconfError,
23
+ type ZeroconfEvents,
24
+ } from './types'
25
+
26
+ export * from './types'
27
+
28
+ // Payloads of the native events, scan events carry the id of the scan they belong to
29
+ type NativePayload = BridgePayload
30
+
31
+ interface NativeErrorPayload extends NativePayload {
32
+ message: string
33
+ code: number | string
34
+ domain: ZeroconfError['domain']
35
+ serviceName?: string
36
+ }
37
+
38
+ const SCAN_DEFAULTS = {
39
+ type: 'http',
40
+ protocol: 'tcp',
41
+ domain: 'local.',
42
+ implType: ImplType.NSD as ImplType,
43
+ resolveTimeout: 5,
44
+ }
45
+
46
+ const PUBLISH_DEFAULTS = {
47
+ domain: 'local.',
48
+ txt: {} as TxtRecord,
49
+ implType: ImplType.NSD as ImplType,
50
+ }
51
+
52
+ /**
53
+ * Merge options over defaults, ignoring undefined values
54
+ */
55
+ const withDefaults = <D extends object, O extends object>(defaults: D, options: O): D & O => {
56
+ const merged: Record<string, unknown> = { ...(defaults as Record<string, unknown>) }
57
+ Object.entries(options).forEach(([key, value]) => {
58
+ if (value !== undefined) {
59
+ merged[key] = value
60
+ }
61
+ })
62
+ return merged as D & O
63
+ }
64
+
65
+ // The positional forms of scan() and publishService() were removed in 1.0: a clear error for JavaScript
66
+ // callers, TypeScript already rejects them
67
+ const removedPositionalForm = (options: unknown, method: string, replacement: string) => {
68
+ if (typeof options === 'string') {
69
+ throw new TypeError(
70
+ `react-native-zeroconf: ${method}() takes an options object since 1.0, use ${replacement}`,
71
+ )
72
+ }
73
+ }
74
+
75
+ // Options passed to native code, without the undefined ones
76
+ const nativeOptions = (options: Record<string, unknown>) =>
77
+ Object.fromEntries(Object.entries(options).filter(([, value]) => value !== undefined))
78
+
79
+ // Android 17 (API 37) runtime permission, enforced for apps targeting API 37 or declaring it
80
+ const ACCESS_LOCAL_NETWORK = 'android.permission.ACCESS_LOCAL_NETWORK'
81
+
82
+ // Each instance runs its own scan, identified by this id in native events
83
+ let instanceCount = 0
84
+
85
+ /**
86
+ * Error from a native { message, code, domain, serviceName } payload
87
+ */
88
+ const toError = (payload: unknown): ZeroconfError | Error => {
89
+ if (!payload || typeof payload !== 'object') {
90
+ return new Error(String(payload))
91
+ }
92
+ const { message, code, domain, serviceName } = payload as NativeErrorPayload
93
+ const error = new Error(message) as ZeroconfError
94
+ error.code = code
95
+ error.domain = domain
96
+ if (serviceName) {
97
+ error.serviceName = serviceName
98
+ }
99
+ return error
100
+ }
101
+
102
+ /**
103
+ * Promise rejections carry the same payload as error events in userInfo
104
+ */
105
+ const toRejection = (error: unknown) => {
106
+ const userInfo = (error as { userInfo?: { domain?: unknown } } | null)?.userInfo
107
+ return userInfo && userInfo.domain ? toError(userInfo) : error
108
+ }
109
+
110
+ /**
111
+ * Normalizes rejections, and marks the promise handled so callers that don't await it
112
+ * don't get unhandled rejection warnings (errors are also emitted as error events)
113
+ */
114
+ const asPromise = <T>(nativePromise: T | Promise<T>): Promise<T> => {
115
+ const promise = Promise.resolve(nativePromise).catch(error => {
116
+ throw toRejection(error)
117
+ })
118
+ promise.catch(() => {})
119
+ return promise
120
+ }
121
+
122
+ const isIPv6 = (address: string) => address.includes(':')
123
+
124
+ /**
125
+ * Sort addresses IPv4 first and expose them by family
126
+ */
127
+ const withAddressFamilies = (service: Omit<Service, 'ipv4' | 'ipv6'>): Service => {
128
+ const addresses = Array.isArray(service.addresses) ? service.addresses : []
129
+ const ipv4 = addresses.filter(address => !isIPv6(address))
130
+ const ipv6 = addresses.filter(isIPv6)
131
+ return { ...service, addresses: [...ipv4, ...ipv6], ipv4, ipv6 }
132
+ }
133
+
134
+ // Browsing this type lists the service types on the network instead of services
135
+ const SERVICE_TYPES = { type: 'services._dns-sd', protocol: 'udp' }
136
+
137
+ // "_http._tcp" -> { type: 'http', protocol: 'tcp' }
138
+ const parseServiceType = (name: string): ServiceType | null => {
139
+ const match = /^_([^.]+)\._(tcp|udp)$/.exec(name)
140
+ return match ? { type: match[1]!, protocol: match[2] as ServiceType['protocol'] } : null
141
+ }
142
+
143
+ /**
144
+ * TXT records as ordered [key, value] pairs, from an object or an array of pairs
145
+ */
146
+ const toTxtPairs = (txt: TxtRecord | undefined): Array<[string, string]> => {
147
+ const entries = Array.isArray(txt) ? txt : Object.entries(txt || {})
148
+ return entries.map(([key, value]) => [String(key), String(value)])
149
+ }
150
+
151
+ let bridge: Bridge | null = null
152
+
153
+ const nativeModule = (): Bridge => {
154
+ bridge = bridge || getBridge()
155
+ if (!bridge) {
156
+ throw new Error(
157
+ '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.',
158
+ )
159
+ }
160
+ return bridge
161
+ }
162
+
163
+ // Typed events on top of the `events` EventEmitter
164
+ type Listener<E extends keyof ZeroconfEvents> = ZeroconfEvents[E]
165
+
166
+ // Typed events, merged into the class below
167
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
168
+ interface Zeroconf {
169
+ on<E extends keyof ZeroconfEvents>(event: E, listener: Listener<E>): this
170
+ once<E extends keyof ZeroconfEvents>(event: E, listener: Listener<E>): this
171
+ addListener<E extends keyof ZeroconfEvents>(event: E, listener: Listener<E>): this
172
+ removeListener<E extends keyof ZeroconfEvents>(event: E, listener: Listener<E>): this
173
+ off<E extends keyof ZeroconfEvents>(event: E, listener: Listener<E>): this
174
+ removeAllListeners(event?: keyof ZeroconfEvents): this
175
+ listenerCount(event: keyof ZeroconfEvents): number
176
+ }
177
+
178
+ /** Extends the `events` package EventEmitter */
179
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
180
+ class Zeroconf extends EventEmitter {
181
+ private _services: Record<string, Service> = {}
182
+ private _serviceTypes: Record<string, ServiceType> = {}
183
+ private _scanningTypes = false
184
+ private _publishedServices: Record<string, PublishedService> = {}
185
+ private _publishedImplTypes: Record<string, ImplType> = {}
186
+ private _scanImplType: ImplType | null = null
187
+ private _scanId = `zeroconf-${++instanceCount}`
188
+ private _hasScanned = false
189
+ private _dListeners: Record<string, { remove(): void }> = {}
190
+
191
+ constructor() {
192
+ super()
193
+ nativeModule()
194
+ this.addDeviceListeners()
195
+ }
196
+
197
+ /**
198
+ * Add the native event listeners (called automatically in the constructor)
199
+ */
200
+ addDeviceListeners(): void {
201
+ if (Object.keys(this._dListeners).length) {
202
+ this.emit('error', new Error('RNZeroconf listeners already in place.'))
203
+ return
204
+ }
205
+
206
+ const native = nativeModule()
207
+ this._dListeners.start = native.listen('start', payload => {
208
+ if (this._isOwnEvent(payload)) {
209
+ this.emit('start')
210
+ }
211
+ })
212
+
213
+ this._dListeners.stop = native.listen('stop', payload => {
214
+ if (this._isOwnEvent(payload)) {
215
+ this.emit('stop')
216
+ }
217
+ })
218
+
219
+ this._dListeners.error = native.listen('error', err => {
220
+ if (this._isOwnEvent(err) && this.listenerCount('error') > 0) {
221
+ this.emit('error', toError(err))
222
+ }
223
+ })
224
+
225
+ this._dListeners.found = native.listen('found', (service: NativePayload) => {
226
+ if (!service || !service.name || !this._isOwnEvent(service)) {
227
+ return
228
+ }
229
+ const { name } = service
230
+
231
+ if (this._scanningTypes) {
232
+ const serviceType = parseServiceType(name)
233
+ if (serviceType && !this._serviceTypes[name]) {
234
+ this._serviceTypes[name] = serviceType
235
+ this.emit('typeFound', serviceType)
236
+ this.emit('update')
237
+ }
238
+ return
239
+ }
240
+
241
+ // Found services only have a name until they are resolved
242
+ this._services[name] = { name } as Service
243
+ this.emit('found', name)
244
+ this.emit('update')
245
+ })
246
+
247
+ this._dListeners.remove = native.listen('remove', (service: NativePayload) => {
248
+ if (!service || !service.name || !this._isOwnEvent(service)) {
249
+ return
250
+ }
251
+ const { name } = service
252
+
253
+ if (this._scanningTypes) {
254
+ const serviceType = this._serviceTypes[name]
255
+ if (serviceType) {
256
+ delete this._serviceTypes[name]
257
+ this.emit('typeRemove', serviceType)
258
+ this.emit('update')
259
+ }
260
+ return
261
+ }
262
+
263
+ delete this._services[name]
264
+
265
+ this.emit('remove', name)
266
+ this.emit('update')
267
+ })
268
+
269
+ this._dListeners.resolved = native.listen('resolved', payload => {
270
+ const data = payload as NativePayload & Omit<Service, 'ipv4' | 'ipv6'>
271
+ if (!data || !data.name || !this._isOwnEvent(data)) {
272
+ return
273
+ }
274
+
275
+ const resolved = { ...data }
276
+ delete resolved.scanId
277
+ const service = withAddressFamilies(resolved)
278
+ this._services[service.name] = service
279
+ this.emit('resolved', service)
280
+ this.emit('update')
281
+ })
282
+
283
+ this._dListeners.published = native.listen('published', payload => {
284
+ const service = payload as unknown as PublishedService
285
+ if (!service || !service.name) {
286
+ return
287
+ }
288
+
289
+ this._publishedServices[service.name] = service
290
+ this.emit('published', service)
291
+ })
292
+
293
+ this._dListeners.unpublished = native.listen('unpublished', payload => {
294
+ const service = payload as unknown as PublishedService
295
+ if (!service || !service.name) {
296
+ return
297
+ }
298
+
299
+ delete this._publishedServices[service.name]
300
+ this.emit('unpublished', service)
301
+ })
302
+ }
303
+
304
+ /**
305
+ * Add a listener and get a function that removes it, handy in useEffect cleanups
306
+ */
307
+ subscribe<E extends keyof ZeroconfEvents>(event: E, listener: Listener<E>): () => void {
308
+ this.on(event, listener)
309
+ return () => this.removeListener(event, listener)
310
+ }
311
+
312
+ /**
313
+ * Scan events belong to the instance that started the scan. Instances that never scanned still receive
314
+ * every scan's events, as before multiple scans, and events without a scan id (publishing) go to everyone.
315
+ */
316
+ private _isOwnEvent(payload: unknown): boolean {
317
+ if (!payload || typeof payload !== 'object' || !this._hasScanned) {
318
+ return true
319
+ }
320
+ const { scanId } = payload as NativePayload
321
+ return scanId == null || scanId === this._scanId
322
+ }
323
+
324
+ /**
325
+ * Remove the native event listeners, call it when you are done with the instance
326
+ */
327
+ removeDeviceListeners(): void {
328
+ Object.values(this._dListeners).forEach(listener => listener.remove())
329
+ this._dListeners = {}
330
+ }
331
+
332
+ /**
333
+ * All the services found so far, keyed by name
334
+ */
335
+ getServices(): Record<string, Service> {
336
+ return this._services
337
+ }
338
+
339
+ /**
340
+ * Service types found by `scanServiceTypes`
341
+ */
342
+ getServiceTypes(): ServiceType[] {
343
+ return Object.values(this._serviceTypes)
344
+ }
345
+
346
+ /**
347
+ * Scan for the service types advertised on the network (_http._tcp, _ipp._tcp...), emits typeFound and typeRemove.
348
+ * On iOS, only types declared in NSBonjourServices can then be scanned.
349
+ * 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.
350
+ */
351
+ scanServiceTypes({ domain, implType, networkInterface }: ServiceTypesScanOptions = {}): void {
352
+ this._startScan({ ...SERVICE_TYPES, domain, implType, networkInterface }, true)
353
+ }
354
+
355
+ /**
356
+ * Scan for services, defaults to `_http._tcp.` on the `local.` domain
357
+ */
358
+ scan(options: ScanOptions = {}): void {
359
+ removedPositionalForm(options, 'scan', 'scan({ type, protocol, domain, implType })')
360
+ this._startScan(options, false)
361
+ }
362
+
363
+ private _startScan(options: ScanOptions, scanningTypes: boolean): void {
364
+ const { type, protocol, domain, implType, resolveTimeout, subtype, networkInterface } =
365
+ withDefaults(SCAN_DEFAULTS, options)
366
+ const native = nativeModule()
367
+
368
+ this._services = {}
369
+ this._serviceTypes = {}
370
+ this._scanningTypes = scanningTypes
371
+ this._hasScanned = true
372
+ this.emit('update')
373
+ if (Platform.OS === 'android') {
374
+ if (this._scanImplType && implType !== this._scanImplType) {
375
+ // This instance's scan moves to the other implementation
376
+ native.stop(this._scanId, this._scanImplType)
377
+ }
378
+ this._scanImplType = implType
379
+ }
380
+ native.scan(
381
+ this._scanId,
382
+ type,
383
+ protocol,
384
+ domain,
385
+ implType,
386
+ nativeOptions({ resolveTimeout, subtype, networkInterface }),
387
+ )
388
+ }
389
+
390
+ /**
391
+ * Stop the current scan
392
+ * @param implType Android only, defaults to the implementation used by the last scan
393
+ */
394
+ stop(implType: ImplType = this._scanImplType || ImplType.NSD): void {
395
+ nativeModule().stop(this._scanId, implType)
396
+ }
397
+
398
+ /**
399
+ * Checks the Local Network permission, resolves 'granted', 'denied' or 'unknown'.
400
+ * iOS: uses a service type from NSBonjourServices (the first one by default) and can show the permission prompt.
401
+ * Android: apps targeting Android 17 (API 37) need ACCESS_LOCAL_NETWORK on Android 17 devices, it is requested
402
+ * when missing unless request is false. 'granted' when there is nothing to grant.
403
+ */
404
+ async checkLocalNetworkAccess({
405
+ type,
406
+ protocol = 'tcp',
407
+ timeout = 5,
408
+ request = true,
409
+ }: LocalNetworkAccessOptions = {}): Promise<LocalNetworkAccess> {
410
+ const native = nativeModule()
411
+ if (Platform.OS === 'android') {
412
+ const status = await asPromise(native.checkLocalNetworkAccess(null, timeout))
413
+ if (status !== 'denied' || !request) {
414
+ return status
415
+ }
416
+ const result = await PermissionsAndroid.request(
417
+ ACCESS_LOCAL_NETWORK as Parameters<typeof PermissionsAndroid.request>[0],
418
+ )
419
+ return result === PermissionsAndroid.RESULTS.GRANTED ? 'granted' : 'denied'
420
+ }
421
+ const serviceType = type ? `_${type}._${protocol}` : null
422
+ return asPromise(native.checkLocalNetworkAccess(serviceType, timeout))
423
+ }
424
+
425
+ /**
426
+ * Publish a service. Resolves with the published service once it is advertised, its name can differ
427
+ * from the requested one when that name is already taken. Rejects with a `ZeroconfError`.
428
+ */
429
+ publishService(options: PublishOptions): Promise<PublishedService> {
430
+ removedPositionalForm(
431
+ options,
432
+ 'publishService',
433
+ 'publishService({ type, protocol, domain, name, port, txt, implType })',
434
+ )
435
+ const { type, protocol, domain, name, port, txt, implType, subtypes, networkInterface } =
436
+ withDefaults(PUBLISH_DEFAULTS, options)
437
+ const native = nativeModule()
438
+ const txtRecord = toTxtPairs(txt)
439
+ const options_ = nativeOptions({ subtypes, networkInterface })
440
+ if (Platform.OS === 'android') {
441
+ this._publishedImplTypes[name] = implType
442
+ }
443
+ return asPromise(
444
+ native.registerService(type, protocol, domain, name, port, txtRecord, implType, options_),
445
+ )
446
+ }
447
+
448
+ /**
449
+ * Replace the TXT record of a published service, resolves with the updated service.
450
+ * Android `NSD` has no update: the service is published again under the same name.
451
+ */
452
+ updateService(
453
+ name: string,
454
+ { txt = {}, implType = this._publishedImplTypes[name] || ImplType.NSD }: UpdateOptions = {},
455
+ ): Promise<PublishedService> {
456
+ const txtRecord = toTxtPairs(txt)
457
+ return asPromise(nativeModule().updateService(name, txtRecord, implType))
458
+ }
459
+
460
+ /**
461
+ * Resolve one service by name without scanning, for a device found before.
462
+ * Resolves with the service, rejects with code 'TIMEOUT' when it doesn't answer in time.
463
+ */
464
+ resolveService(options: ResolveOptions): Promise<Service> {
465
+ const { name, type, protocol, domain, implType, timeout, networkInterface } = withDefaults(
466
+ { ...SCAN_DEFAULTS, timeout: 5 },
467
+ options,
468
+ )
469
+ const native = nativeModule()
470
+ const nativeOptions_ = nativeOptions({ timeout, networkInterface })
471
+ const promise = native.resolveService(name, type, protocol, domain, implType, nativeOptions_)
472
+ return asPromise(
473
+ asPromise(promise).then(service => (service && withAddressFamilies(service)) as Service),
474
+ )
475
+ }
476
+
477
+ /**
478
+ * Unpublish a service. Resolves once it is no longer advertised, rejects with a `ZeroconfError`
479
+ * (e.g. code `'NOT_PUBLISHED'`).
480
+ * @param implType Android only, defaults to the implementation the service was published with
481
+ */
482
+ unpublishService(
483
+ name: string,
484
+ implType: ImplType = this._publishedImplTypes[name] || ImplType.NSD,
485
+ ): Promise<PublishedService | null> {
486
+ if (Platform.OS === 'android') {
487
+ delete this._publishedImplTypes[name]
488
+ }
489
+ return asPromise(nativeModule().unregisterService(name, implType))
490
+ }
491
+ }
492
+
493
+ export default Zeroconf
494
+
495
+ interface ScanState<T> {
496
+ items: T[]
497
+ isScanning: boolean
498
+ error: ZeroconfError | null
499
+ stop(): void
500
+ restart(): void
501
+ }
502
+
503
+ // Runs a scan while mounted and enabled, and again when deps change or on restart
504
+ function useScan<T>(
505
+ enabled: boolean,
506
+ startScan: (zeroconf: Zeroconf) => void,
507
+ read: (zeroconf: Zeroconf) => T[],
508
+ deps: unknown[],
509
+ ): ScanState<T> {
510
+ const zeroconfRef = useRef<Zeroconf | null>(null)
511
+ const [items, setItems] = useState<T[]>([])
512
+ const [isScanning, setIsScanning] = useState(false)
513
+ const [error, setError] = useState<ZeroconfError | null>(null)
514
+ const [scanCount, setScanCount] = useState(0)
515
+ const readRef = useRef(read)
516
+ readRef.current = read
517
+
518
+ useEffect(() => {
519
+ const zeroconf = new Zeroconf()
520
+ zeroconfRef.current = zeroconf
521
+ const unsubscribes = [
522
+ zeroconf.subscribe('start', () => setIsScanning(true)),
523
+ zeroconf.subscribe('stop', () => setIsScanning(false)),
524
+ zeroconf.subscribe('update', () => setItems(readRef.current(zeroconf))),
525
+ zeroconf.subscribe('error', setError),
526
+ ]
527
+ return () => {
528
+ unsubscribes.forEach(unsubscribe => unsubscribe())
529
+ zeroconf.stop()
530
+ zeroconf.removeDeviceListeners()
531
+ zeroconfRef.current = null
532
+ }
533
+ }, [])
534
+
535
+ useEffect(() => {
536
+ const zeroconf = zeroconfRef.current
537
+ if (!zeroconf || !enabled) {
538
+ return undefined
539
+ }
540
+ setItems([])
541
+ setError(null)
542
+ startScan(zeroconf)
543
+ return () => zeroconf.stop()
544
+ // The options are the dependencies, not the callbacks built from them
545
+ }, [enabled, scanCount, ...deps])
546
+
547
+ const stop = useCallback(() => {
548
+ if (zeroconfRef.current) {
549
+ zeroconfRef.current.stop()
550
+ }
551
+ }, [])
552
+ const restart = useCallback(() => setScanCount(count => count + 1), [])
553
+
554
+ return { items, isScanning, error, stop, restart }
555
+ }
556
+
557
+ /**
558
+ * Scans while mounted and returns the resolved services. Scans again when the options change,
559
+ * stops and cleans up on unmount. Each hook runs its own scan, several can run at once.
560
+ *
561
+ * const { services, isScanning, error, stop, restart } = useZeroconf({ type: 'http' })
562
+ */
563
+ export function useZeroconf(options: UseZeroconfOptions = {}): UseZeroconfResult {
564
+ const {
565
+ type,
566
+ protocol,
567
+ domain,
568
+ implType,
569
+ resolveTimeout,
570
+ subtype,
571
+ networkInterface,
572
+ enabled = true,
573
+ } = options
574
+ const { items, ...scan } = useScan(
575
+ enabled,
576
+ zeroconf =>
577
+ zeroconf.scan({
578
+ type,
579
+ protocol,
580
+ domain,
581
+ implType,
582
+ resolveTimeout,
583
+ subtype,
584
+ networkInterface,
585
+ }),
586
+ // Found services only have a name until they are resolved
587
+ zeroconf => Object.values(zeroconf.getServices()).filter(service => service.addresses),
588
+ [type, protocol, domain, implType, resolveTimeout, subtype, networkInterface],
589
+ )
590
+ return { services: items, ...scan }
591
+ }
592
+
593
+ /**
594
+ * Lists the service types advertised on the network while mounted.
595
+ *
596
+ * const { serviceTypes, isScanning, error, stop, restart } = useServiceTypes()
597
+ */
598
+ export function useServiceTypes(options: UseServiceTypesOptions = {}): UseServiceTypesResult {
599
+ const { domain, implType, networkInterface, enabled = true } = options
600
+ const { items, ...scan } = useScan(
601
+ enabled,
602
+ zeroconf => zeroconf.scanServiceTypes({ domain, implType, networkInterface }),
603
+ zeroconf => zeroconf.getServiceTypes(),
604
+ [domain, implType, networkInterface],
605
+ )
606
+ return { serviceTypes: items, ...scan }
607
+ }