react-native-zeroconf 0.17.3 → 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.
- package/NOTICE +2 -6
- package/README.md +1 -1
- package/android/CMakeLists.txt +64 -0
- package/android/build.gradle +1 -17
- package/android/generated/java/com/balthazargronon/RCTZeroconf/NativeZeroconfSpec.java +96 -0
- package/android/generated/jni/CMakeLists.txt +28 -0
- package/android/generated/jni/RNZeroconfSpec-generated.cpp +77 -0
- package/android/generated/jni/RNZeroconfSpec.h +31 -0
- package/android/generated/jni/react/renderer/components/RNZeroconfSpec/RNZeroconfSpecJSI.h +798 -0
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/{ZeroconfModule.java → NsdHost.java} +97 -140
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/NsdPayload.java +87 -0
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/NsdPromise.java +40 -0
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/Zeroconf.java +4 -6
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroconfNativeSupport.java +60 -0
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroconfOptions.java +15 -26
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroconfReactPackage.java +5 -15
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/nsd/NsdServiceImpl.java +90 -96
- package/android/src/main/jni/mdnsresponder/mDNSShared/dnssd_clientshim.c +31 -6
- package/cpp/Backend.cpp +16 -0
- package/cpp/Backend.h +116 -0
- package/cpp/ZeroconfModule.cpp +280 -0
- package/cpp/ZeroconfModule.h +144 -0
- package/cpp/android/NsdBackend.cpp +462 -0
- package/cpp/android/NsdBackend.h +61 -0
- package/cpp/android/ZeroconfAndroidModule.cpp +49 -0
- package/cpp/android/ZeroconfAndroidModule.h +15 -0
- package/cpp/apple/DispatchExecutor.cpp +66 -0
- package/cpp/apple/DispatchExecutor.h +37 -0
- package/cpp/apple/LocalNetworkAccess.h +19 -0
- package/cpp/apple/LocalNetworkAccess.mm +87 -0
- package/cpp/dnssd/DnssdBackend.cpp +976 -0
- package/cpp/dnssd/DnssdBackend.h +114 -0
- package/cpp/dnssd/Executor.h +28 -0
- package/cpp/embedded/EmbeddedExecutor.cpp +124 -0
- package/cpp/embedded/EmbeddedExecutor.h +57 -0
- package/ios/ZeroconfOnLoad.mm +34 -0
- package/ios/generated/ReactCodegen/RNZeroconfSpec/RNZeroconfSpec-generated.mm +140 -0
- package/ios/generated/ReactCodegen/RNZeroconfSpec/RNZeroconfSpec.h +189 -0
- package/ios/generated/ReactCodegen/RNZeroconfSpecJSI.h +798 -0
- package/lib/module/NativeZeroconf.js +15 -0
- package/lib/module/NativeZeroconf.js.map +1 -0
- package/lib/module/bridge.js +124 -0
- package/lib/module/bridge.js.map +1 -0
- package/{dist → lib/module}/index.js +149 -149
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +24 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeZeroconf.d.ts +75 -0
- package/lib/typescript/src/NativeZeroconf.d.ts.map +1 -0
- package/lib/typescript/src/bridge.d.ts +22 -0
- package/lib/typescript/src/bridge.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +111 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +175 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +64 -29
- package/react-native-zeroconf.podspec +2 -1
- package/react-native.config.js +17 -0
- package/src/NativeZeroconf.ts +120 -0
- package/src/bridge.ts +153 -0
- package/src/index.ts +607 -0
- package/{index.d.ts → src/types.ts} +5 -99
- package/windows/RNZeroconf/RNZeroconf.vcxproj +17 -2
- package/windows/RNZeroconf/RNZeroconf.vcxproj.filters +0 -6
- package/windows/RNZeroconf/ReactPackageProvider.cpp +23 -20
- package/windows/RNZeroconf/WindowsBackend.cpp +156 -0
- package/windows/RNZeroconf/WindowsBackend.h +13 -0
- package/windows/RNZeroconf/ZeroconfCore.cpp +2 -2
- package/windows/RNZeroconf/ZeroconfCore.h +2 -2
- package/windows/RNZeroconf/ZeroconfWindowsModule.cpp +22 -0
- package/windows/RNZeroconf/ZeroconfWindowsModule.h +14 -0
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroConfImplFactory.java +0 -56
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/dnssd/DnssdImpl.java +0 -654
- package/android/src/main/java/com/github/druk/dnssd/BaseListener.java +0 -33
- package/android/src/main/java/com/github/druk/dnssd/BrowseListener.java +0 -67
- package/android/src/main/java/com/github/druk/dnssd/DNSRecord.java +0 -47
- package/android/src/main/java/com/github/druk/dnssd/DNSSD.java +0 -681
- package/android/src/main/java/com/github/druk/dnssd/DNSSDEmbedded.java +0 -198
- package/android/src/main/java/com/github/druk/dnssd/DNSSDException.java +0 -66
- package/android/src/main/java/com/github/druk/dnssd/DNSSDRecordRegistrar.java +0 -55
- package/android/src/main/java/com/github/druk/dnssd/DNSSDRegistration.java +0 -54
- package/android/src/main/java/com/github/druk/dnssd/DNSSDService.java +0 -35
- package/android/src/main/java/com/github/druk/dnssd/DomainListener.java +0 -57
- package/android/src/main/java/com/github/druk/dnssd/InternalBrowseListener.java +0 -76
- package/android/src/main/java/com/github/druk/dnssd/InternalDNSSD.java +0 -870
- package/android/src/main/java/com/github/druk/dnssd/InternalDNSSDRecordRegistrar.java +0 -30
- package/android/src/main/java/com/github/druk/dnssd/InternalDNSSDRegistration.java +0 -34
- package/android/src/main/java/com/github/druk/dnssd/InternalDNSSDService.java +0 -29
- package/android/src/main/java/com/github/druk/dnssd/InternalDomainListener.java +0 -62
- package/android/src/main/java/com/github/druk/dnssd/InternalQueryListener.java +0 -61
- package/android/src/main/java/com/github/druk/dnssd/InternalRegisterListener.java +0 -51
- package/android/src/main/java/com/github/druk/dnssd/InternalResolveListener.java +0 -58
- package/android/src/main/java/com/github/druk/dnssd/NSClass.java +0 -16
- package/android/src/main/java/com/github/druk/dnssd/NSType.java +0 -58
- package/android/src/main/java/com/github/druk/dnssd/QueryListener.java +0 -54
- package/android/src/main/java/com/github/druk/dnssd/RegisterListener.java +0 -46
- package/android/src/main/java/com/github/druk/dnssd/RegisterRecordListener.java +0 -35
- package/android/src/main/java/com/github/druk/dnssd/ResolveListener.java +0 -55
- package/android/src/main/java/com/github/druk/dnssd/TXTRecord.java +0 -293
- package/android/src/main/jni/Android.mk +0 -73
- package/android/src/main/jni/Application.mk +0 -3
- package/android/src/main/jni/DNSSD.java.h +0 -290
- package/android/src/main/jni/JNISupport.c +0 -1221
- package/ios/RNZeroconf/RNNetServiceSerializer.h +0 -31
- package/ios/RNZeroconf/RNNetServiceSerializer.m +0 -84
- package/ios/RNZeroconf/RNZeroconf.h +0 -17
- package/ios/RNZeroconf/RNZeroconf.m +0 -1025
- package/ios/RNZeroconf.xcodeproj/project.pbxproj +0 -261
- package/ios/RNZeroconf.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
- package/src/index.js +0 -563
- package/windows/RNZeroconf/RNZeroconf.cpp +0 -228
- 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
|
+
}
|