@devioarts/capacitor-mdns 0.0.3 → 0.1.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 (36) hide show
  1. package/README.md +54 -67
  2. package/android/src/main/AndroidManifest.xml +2 -0
  3. package/android/src/main/java/com/devioarts/capacitor/mdns/mDNS.kt +134 -37
  4. package/android/src/main/java/com/devioarts/capacitor/mdns/mDNSPlugin.kt +18 -5
  5. package/dist/docs.json +65 -3
  6. package/dist/esm/definitions.d.ts +27 -1
  7. package/dist/esm/definitions.js.map +1 -1
  8. package/dist/esm/index.js +1 -0
  9. package/dist/esm/index.js.map +1 -1
  10. package/dist/esm/web.d.ts +3 -1
  11. package/dist/esm/web.js +12 -3
  12. package/dist/esm/web.js.map +1 -1
  13. package/dist/plugin.cjs.js +13 -3
  14. package/dist/plugin.cjs.js.map +1 -1
  15. package/dist/plugin.js +13 -3
  16. package/dist/plugin.js.map +1 -1
  17. package/docs/electron.md +65 -0
  18. package/docs/platform-setup.md +52 -0
  19. package/docs/playground.md +19 -0
  20. package/docs/runtime-behavior.md +14 -0
  21. package/electron/dist/plugin-settings.js +15 -0
  22. package/electron/dist/plugin-settings.js.map +1 -0
  23. package/electron/dist/plugin.cjs.js +344 -0
  24. package/electron/dist/plugin.cjs.js.map +1 -0
  25. package/electron/global.d.ts +2 -0
  26. package/electron/mdns-bridge.cjs +8 -2
  27. package/electron/mdns.ts +151 -63
  28. package/electron/rollup.config.mjs +22 -0
  29. package/electron/src/index.ts +1 -0
  30. package/electron/src/plugin-settings.ts +12 -0
  31. package/electron/tsconfig.json +21 -0
  32. package/electron/types-public/index.d.ts +25 -0
  33. package/ios/Sources/mDNSPlugin/mDNS.swift +95 -28
  34. package/ios/Sources/mDNSPlugin/mDNSPlugin.swift +12 -3
  35. package/ios/Tests/mDNSPluginTests/mDNSPluginTests.swift +3 -9
  36. package/package.json +46 -18
package/README.md CHANGED
@@ -1,9 +1,11 @@
1
1
  # @devioarts/capacitor-mdns
2
2
 
3
- mDNS plugin for Capacitor that supports Bonjour/mDNS advertisements and discovery.
3
+ mDNS plugin for Capacitor that supports Bonjour/mDNS advertisements and discovery on iOS,
4
+ Android, Electron, and a safe web fallback.
4
5
 
5
- #### Supported platforms: ✓ iOS ✓ Android ✓ Electron
6
- #### Demo (sources): [application](https://github.com/devioarts/capacitor-examples/tree/main/capacitor-mdns) or directly [file](https://github.com/devioarts/capacitor-examples/blob/main/capacitor-mdns/src/Playground.tsx)
6
+ #### Supported platforms: ✓ iOS ✓ Android ✓ Electron ✓ Web fallback
7
+
8
+ The in-repository [`playground`](./playground) is the example app used during development.
7
9
 
8
10
  ## Install
9
11
 
@@ -12,82 +14,37 @@ npm install @devioarts/capacitor-mdns
12
14
  npx cap sync
13
15
  ```
14
16
 
15
- ## Android
16
- #### /android/app/src/main/AndroidManifest.xml
17
- ```xml
18
- <uses-permission android:name="android.permission.INTERNET" />
19
- ```
17
+ ## Usage
20
18
 
21
- ## iOS
22
- #### /ios/App/App/Info.plist
23
- ```xml
24
- <key>NSLocalNetworkUsageDescription</key>
25
- <string>It is needed for the correct functioning of the application</string>
26
- <key>NSAppTransportSecurity</key>
27
- <dict>
28
- <key>NSAllowsLocalNetworking</key>
29
- <true/>
30
- </dict>
31
- <key>NSBonjourServices</key>
32
- <array>
33
- <string>_http._tcp</string>
34
- </array>
35
- ```
19
+ ```ts
20
+ import { mDNS } from '@devioarts/capacitor-mdns';
36
21
 
37
- ---
38
- ## ElectronJS
22
+ const runtime = await mDNS.getPluginPlatform();
39
23
 
40
- ```shell
41
- npm i bonjour-service@1.3.0
24
+ await mDNS.startBroadcast({ type: '_http._tcp.', name: 'MyService', port: 9100 });
25
+ const result = await mDNS.discover({ type: '_http._tcp.', timeout: 3000 });
26
+ await mDNS.stopBroadcast();
42
27
  ```
43
28
 
44
- > Implementation example was developed on [capacitor-electron](https://github.com/devioarts/capacitor-examples/tree/main/capacitor-electron)
45
- > base, if you run electron differently, you may need to adjust the code.
29
+ ## Platform Setup
46
30
 
47
- #### /electron/main.ts
31
+ Android requires mDNS/network permissions in `AndroidManifest.xml`; the plugin also declares them
32
+ so they are merged during sync. iOS requires local network keys in `Info.plist`.
33
+ Electron support is intended for [`devioarts/capacitor-electron`](https://github.com/devioarts/capacitor-electron)
34
+ and uses metadata exported from `@devioarts/capacitor-mdns/electron/settings`.
48
35
 
49
- ```typescript
50
- // ...
51
- import { mDNS } from '@devioarts/capacitor-mdns/electron/mdns'
52
- // ...
53
- const mdns = new mDNS();
54
- // ...
55
- app.whenReady().then(() => {
56
- //...
57
- mdns.init();
58
- //...
59
- });
60
- /* Or you can use app.on:ready (whenReady is recomended)
61
- app.on('ready', () => {
62
- // ...
63
- mdns.init();
64
- // ...
65
- });
66
- */
67
-
68
- app.on('before-quit', async () => {
69
- // ...
70
- mdns.destroy();
71
- // ...
72
- })
73
- //...
74
- ```
75
- ### electron/preload.cjs
76
- ```javascript
77
- //...
78
- // THIS IS IMPORTANT FOR PLUGIN!
79
- const {createMDNSAPI} = require("@devioarts/capacitor-mdns/electron/mdns-bridge.cjs");
80
- //...
81
- // THIS IS IMPORTANT FOR PLUGIN!
82
- contextBridge.exposeInMainWorld('mDNS', createMDNSAPI({ ipcRenderer }));
83
- contextBridge.exposeInMainWorld('mdns', createMDNSAPI({ ipcRenderer })) // alias
84
- ```
36
+ Detailed setup lives in:
37
+
38
+ - [iOS and Android setup](./docs/platform-setup.md)
39
+ - [Electron setup](./docs/electron.md)
40
+ - [Playground](./docs/playground.md)
41
+ - [Runtime behavior](./docs/runtime-behavior.md)
85
42
 
86
- ---
87
43
  ## API
88
44
 
89
45
  <docgen-index>
90
46
 
47
+ * [`getPluginPlatform()`](#getpluginplatform)
91
48
  * [`startBroadcast(...)`](#startbroadcast)
92
49
  * [`stopBroadcast()`](#stopbroadcast)
93
50
  * [`discover(...)`](#discover)
@@ -101,6 +58,19 @@ contextBridge.exposeInMainWorld('mdns', createMDNSAPI({ ipcRenderer })) // alias
101
58
 
102
59
  Public API surface of the Capacitor mDNS plugin.
103
60
 
61
+ ### getPluginPlatform()
62
+
63
+ ```typescript
64
+ getPluginPlatform() => Promise<MdnsPluginPlatformResult>
65
+ ```
66
+
67
+ Return the platform implementation currently serving plugin calls.
68
+
69
+ **Returns:** <code>Promise&lt;<a href="#mdnspluginplatformresult">MdnsPluginPlatformResult</a>&gt;</code>
70
+
71
+ --------------------
72
+
73
+
104
74
  ### startBroadcast(...)
105
75
 
106
76
  ```typescript
@@ -151,6 +121,16 @@ Discover services of a given type and optionally filter by instance name.
151
121
  ### Interfaces
152
122
 
153
123
 
124
+ #### MdnsPluginPlatformResult
125
+
126
+ Result of getPluginPlatform(). Useful for validating that calls are routed
127
+ through the expected platform bridge.
128
+
129
+ | Prop | Type | Description |
130
+ | -------------- | ----------------------------------------------------------------- | -------------------------------------------------- |
131
+ | **`platform`** | <code><a href="#mdnspluginplatform">MdnsPluginPlatform</a></code> | Platform implementation that produced this result. |
132
+
133
+
154
134
  #### MdnsBroadcastResult
155
135
 
156
136
  Result of startBroadcast(). Indicates whether advertising is active
@@ -231,6 +211,13 @@ Options for Bonjour/mDNS discovery.
231
211
  ### Type Aliases
232
212
 
233
213
 
214
+ #### MdnsPluginPlatform
215
+
216
+ Runtime implementation that handled a plugin call.
217
+
218
+ <code>'ios' | 'android' | 'electron' | 'web'</code>
219
+
220
+
234
221
  #### MdnsTxt
235
222
 
236
223
  Key–value map for TXT records of a Bonjour/mDNS service.
@@ -1,2 +1,4 @@
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.CHANGE_WIFI_MULTICAST_STATE" />
2
4
  </manifest>
@@ -8,6 +8,8 @@ import android.os.Build
8
8
  import android.os.Handler
9
9
  import android.os.Looper
10
10
  import kotlinx.coroutines.*
11
+ import kotlinx.coroutines.sync.Mutex
12
+ import kotlinx.coroutines.sync.withLock
11
13
  import java.util.concurrent.Executor
12
14
  import java.util.concurrent.atomic.AtomicReference
13
15
 
@@ -31,6 +33,10 @@ class mDNS(
31
33
  context: Context,
32
34
  private val externalScope: CoroutineScope? = null
33
35
  ) {
36
+ private companion object {
37
+ const val PUBLISH_TIMEOUT_MS = 5000L
38
+ }
39
+
34
40
  /** Normalized service representation returned to the bridge. */
35
41
  data class MdnsService(
36
42
  val name: String,
@@ -39,16 +45,26 @@ class mDNS(
39
45
  val port: Int
40
46
  )
41
47
 
48
+ private class RegistrationSession(
49
+ val listener: NsdManager.RegistrationListener,
50
+ val timeoutJob: Job,
51
+ val onSuccess: (String) -> Unit,
52
+ val onError: (Throwable) -> Unit
53
+ ) {
54
+ var completed = false
55
+ }
56
+
42
57
  /** NSD system service. */
43
58
  private val nsd: NsdManager =
44
59
  context.getSystemService(Context.NSD_SERVICE) as NsdManager
45
60
 
46
61
  /** Keep strong references to listeners so they outlive registration/discovery. */
47
- private val regListenerRef = AtomicReference<NsdManager.RegistrationListener?>(null)
62
+ private var activeRegistration: RegistrationSession? = null
48
63
  private val discListenerRef = AtomicReference<NsdManager.DiscoveryListener?>(null)
49
64
 
50
65
  /** Optional external scope (from plugin); otherwise create our own on Main. */
51
66
  private val scope = externalScope ?: CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
67
+ private val discoverMutex = Mutex()
52
68
 
53
69
  /** Main looper utilities for non-suspending entry points. */
54
70
  private val mainHandler = Handler(Looper.getMainLooper())
@@ -73,29 +89,58 @@ class mDNS(
73
89
  onSuccess: (String) -> Unit,
74
90
  onError: (Throwable) -> Unit
75
91
  ) {
76
- require(port > 0) { "Port must be > 0" }
92
+ require(port in 1..65535) { "Port must be between 1 and 65535" }
77
93
  val type = if (typeRaw.endsWith(".")) typeRaw else "$typeRaw."
78
94
 
79
95
  runOnMain {
80
- // Ensure a clean state before registering again.
81
- regListenerRef.getAndSet(null)?.let { safeUnregister(it) }
96
+ try {
97
+ // Restarting while publish is pending must complete the old JS promise.
98
+ stopActiveRegistration("Replaced by a new broadcast request")
82
99
 
83
- val info = NsdServiceInfo().apply {
84
- serviceType = type
85
- serviceName = name
86
- setPort(port)
87
- }
100
+ val info = NsdServiceInfo().apply {
101
+ serviceType = type
102
+ serviceName = name
103
+ setPort(port)
104
+ }
88
105
 
89
- val listener = object : NsdManager.RegistrationListener {
90
- override fun onServiceRegistered(nsi: NsdServiceInfo) = onSuccess(nsi.serviceName)
91
- override fun onRegistrationFailed(nsi: NsdServiceInfo, errorCode: Int) =
92
- onError(IllegalStateException("Registration failed: $errorCode"))
93
- override fun onServiceUnregistered(nsi: NsdServiceInfo) { /* no-op */ }
94
- override fun onUnregistrationFailed(nsi: NsdServiceInfo, errorCode: Int) { /* no-op */ }
95
- }
106
+ lateinit var session: RegistrationSession
107
+ val listener = object : NsdManager.RegistrationListener {
108
+ override fun onServiceRegistered(nsi: NsdServiceInfo) =
109
+ completeRegistrationSuccess(session, nsi.serviceName)
110
+
111
+ override fun onRegistrationFailed(nsi: NsdServiceInfo, errorCode: Int) =
112
+ completeRegistrationError(
113
+ session,
114
+ IllegalStateException("Registration failed: $errorCode"),
115
+ clearActive = true
116
+ )
117
+
118
+ override fun onServiceUnregistered(nsi: NsdServiceInfo) { /* no-op */ }
119
+ override fun onUnregistrationFailed(nsi: NsdServiceInfo, errorCode: Int) { /* no-op */ }
120
+ }
121
+
122
+ val timeoutJob = scope.launch {
123
+ delay(PUBLISH_TIMEOUT_MS)
124
+ runOnMain {
125
+ completeRegistrationError(
126
+ session,
127
+ IllegalStateException("Timed out waiting for service publish after ${PUBLISH_TIMEOUT_MS}ms"),
128
+ clearActive = true,
129
+ unregister = true
130
+ )
131
+ }
132
+ }
96
133
 
97
- regListenerRef.set(listener)
98
- nsd.registerService(info, NsdManager.PROTOCOL_DNS_SD, listener)
134
+ session = RegistrationSession(listener, timeoutJob, onSuccess, onError)
135
+ activeRegistration = session
136
+ try {
137
+ nsd.registerService(info, NsdManager.PROTOCOL_DNS_SD, listener)
138
+ } catch (t: Throwable) {
139
+ completeRegistrationError(session, t, clearActive = true)
140
+ }
141
+ } catch (t: Throwable) {
142
+ onError(t)
143
+ }
99
144
  }
100
145
  }
101
146
 
@@ -104,7 +149,7 @@ class mDNS(
104
149
  */
105
150
  fun stopBroadcast() {
106
151
  runOnMain {
107
- regListenerRef.getAndSet(null)?.let { safeUnregister(it) }
152
+ stopActiveRegistration("Publish stopped before completion")
108
153
  }
109
154
  }
110
155
 
@@ -127,7 +172,7 @@ class mDNS(
127
172
  typeRaw: String,
128
173
  targetName: String? = null,
129
174
  timeoutMs: Int = 3000
130
- ): List<MdnsService> = withContext(Dispatchers.Main.immediate) {
175
+ ): List<MdnsService> = discoverMutex.withLock { withContext(Dispatchers.Main.immediate) {
131
176
  val type = if (typeRaw.endsWith(".")) typeRaw else "$typeRaw."
132
177
 
133
178
  // Prepare state
@@ -155,23 +200,35 @@ class mDNS(
155
200
  fun completeWithCurrentResults(discoveryListener: NsdManager.DiscoveryListener) {
156
201
  unregisterServiceInfoCallbacks()
157
202
  safeStopDiscovery(discoveryListener)
203
+ discListenerRef.compareAndSet(discoveryListener, null)
158
204
  if (!result.isCompleted) result.complete(found.toList())
159
205
  }
206
+ fun completeWithError(discoveryListener: NsdManager.DiscoveryListener, error: Throwable) {
207
+ unregisterServiceInfoCallbacks()
208
+ safeStopDiscovery(discoveryListener)
209
+ discListenerRef.compareAndSet(discoveryListener, null)
210
+ if (!result.isCompleted) result.completeExceptionally(error)
211
+ }
160
212
 
161
213
  @Suppress("DEPRECATION")
162
214
  fun resolveLegacy(si: NsdServiceInfo, discoveryListener: NsdManager.DiscoveryListener) {
163
- nsd.resolveService(si, object : NsdManager.ResolveListener {
164
- override fun onResolveFailed(s: NsdServiceInfo, errorCode: Int) { /* ignore */ }
215
+ try {
216
+ nsd.resolveService(si, object : NsdManager.ResolveListener {
217
+ override fun onResolveFailed(s: NsdServiceInfo, errorCode: Int) { /* ignore */ }
165
218
 
166
- override fun onServiceResolved(s: NsdServiceInfo) {
167
- upsert(toMdnsService(s))
219
+ override fun onServiceResolved(s: NsdServiceInfo) {
220
+ if (result.isCompleted) return
221
+ upsert(toMdnsService(s))
168
222
 
169
- // Early-exit for exact/prefix target match.
170
- if (targetName != null && matchesTarget(s.serviceName)) {
171
- completeWithCurrentResults(discoveryListener)
223
+ // Early-exit for exact/prefix target match.
224
+ if (targetName != null && matchesTarget(s.serviceName)) {
225
+ completeWithCurrentResults(discoveryListener)
226
+ }
172
227
  }
173
- }
174
- })
228
+ })
229
+ } catch (_: Throwable) {
230
+ // Some Android releases throw when the NSD daemon is busy; ignore this candidate.
231
+ }
175
232
  }
176
233
 
177
234
  fun registerServiceInfoCallback(si: NsdServiceInfo, discoveryListener: NsdManager.DiscoveryListener) {
@@ -186,6 +243,7 @@ class mDNS(
186
243
  override fun onServiceLost() { /* no-op */ }
187
244
 
188
245
  override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
246
+ if (result.isCompleted) return
189
247
  upsert(toMdnsService(serviceInfo))
190
248
 
191
249
  // Early-exit for exact/prefix target match.
@@ -205,14 +263,14 @@ class mDNS(
205
263
 
206
264
  val listener = object : NsdManager.DiscoveryListener {
207
265
  override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
208
- // Fail fast: stop discovery and complete with whatever we have (likely empty).
209
- completeWithCurrentResults(this)
266
+ completeWithError(this, IllegalStateException("Discovery failed to start: $errorCode"))
210
267
  }
211
268
  override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) { /* no-op */ }
212
269
  override fun onDiscoveryStarted(serviceType: String) { /* no-op */ }
213
270
  override fun onDiscoveryStopped(serviceType: String) { /* no-op */ }
214
271
 
215
272
  override fun onServiceFound(si: NsdServiceInfo) {
273
+ if (result.isCompleted) return
216
274
  if (targetName != null && !matchesTarget(si.serviceName)) return
217
275
 
218
276
  val discoveryListener = this
@@ -224,10 +282,15 @@ class mDNS(
224
282
 
225
283
  // Start discovery and schedule timeout
226
284
  discListenerRef.set(listener)
227
- nsd.discoverServices(type, NsdManager.PROTOCOL_DNS_SD, listener)
285
+ try {
286
+ nsd.discoverServices(type, NsdManager.PROTOCOL_DNS_SD, listener)
287
+ } catch (t: Throwable) {
288
+ discListenerRef.compareAndSet(listener, null)
289
+ throw t
290
+ }
228
291
 
229
292
  val timeoutJob = scope.launch {
230
- delay(timeoutMs.toLong())
293
+ delay(timeoutMs.coerceAtLeast(0).toLong())
231
294
  completeWithCurrentResults(listener)
232
295
  }
233
296
 
@@ -237,17 +300,19 @@ class mDNS(
237
300
  // Cleanup in all cases (early-exit or timeout)
238
301
  timeoutJob.cancel()
239
302
  unregisterServiceInfoCallbacks()
240
- discListenerRef.getAndSet(null)?.let { safeStopDiscovery(it) }
303
+ if (discListenerRef.compareAndSet(listener, null)) safeStopDiscovery(listener)
241
304
  }
242
- }
305
+ } }
243
306
 
244
307
  /**
245
308
  * Close the manager and release any outstanding NSD listeners.
246
309
  * Safe to call multiple times; also invoked by the plugin's handleOnDestroy().
247
310
  */
248
311
  fun close() {
249
- stopBroadcast()
250
- discListenerRef.getAndSet(null)?.let { safeStopDiscovery(it) }
312
+ runOnMain {
313
+ stopActiveRegistration("mDNS manager closed before publish completed")
314
+ discListenerRef.getAndSet(null)?.let { safeStopDiscovery(it) }
315
+ }
251
316
  if (externalScope == null) scope.cancel()
252
317
  }
253
318
 
@@ -258,6 +323,38 @@ class mDNS(
258
323
  try { nsd.unregisterService(l) } catch (_: Throwable) {}
259
324
  }
260
325
 
326
+ private fun completeRegistrationSuccess(session: RegistrationSession, publishedName: String) {
327
+ if (session.completed) return
328
+ session.completed = true
329
+ session.timeoutJob.cancel()
330
+ session.onSuccess(publishedName)
331
+ }
332
+
333
+ private fun completeRegistrationError(
334
+ session: RegistrationSession,
335
+ error: Throwable,
336
+ clearActive: Boolean,
337
+ unregister: Boolean = false
338
+ ) {
339
+ if (session.completed) return
340
+ session.completed = true
341
+ session.timeoutJob.cancel()
342
+ if (unregister) safeUnregister(session.listener)
343
+ if (clearActive && activeRegistration === session) activeRegistration = null
344
+ session.onError(error)
345
+ }
346
+
347
+ private fun stopActiveRegistration(reason: String) {
348
+ val session = activeRegistration ?: return
349
+ activeRegistration = null
350
+ safeUnregister(session.listener)
351
+ if (!session.completed) {
352
+ completeRegistrationError(session, IllegalStateException(reason), clearActive = false)
353
+ } else {
354
+ session.timeoutJob.cancel()
355
+ }
356
+ }
357
+
261
358
  /** Best-effort stop discovery; NSD may throw if discovery is not active. */
262
359
  private fun safeStopDiscovery(l: NsdManager.DiscoveryListener) {
263
360
  try { nsd.stopServiceDiscovery(l) } catch (_: Throwable) {}
@@ -52,6 +52,14 @@ class mDNSPlugin : Plugin() {
52
52
 
53
53
  private fun jnull(msg: String?): Any = msg ?: JSONObject.NULL
54
54
 
55
+ private fun normalizeType(raw: String?): String {
56
+ val type = raw?.trim()?.takeIf { it.isNotEmpty() } ?: "_http._tcp."
57
+ return if (type.endsWith(".")) type else "$type."
58
+ }
59
+
60
+ private fun normalizeName(raw: String?): String =
61
+ raw?.trim()?.takeIf { it.isNotEmpty() } ?: (context?.packageName ?: "DevIOArtsMDNS")
62
+
55
63
  private fun jsResultBroadcast(publishing: Boolean, name: String, error: Boolean, msg: String?): JSObject =
56
64
  JSObject().put("publishing", publishing)
57
65
  .put("name", name)
@@ -72,6 +80,11 @@ class mDNSPlugin : Plugin() {
72
80
 
73
81
  // ----------------------- API -----------------------
74
82
 
83
+ @PluginMethod
84
+ fun getPluginPlatform(call: PluginCall) {
85
+ call.resolve(JSObject().put("platform", "android"))
86
+ }
87
+
75
88
  /**
76
89
  * Start advertising a Bonjour/mDNS service.
77
90
  * - type: default "_http._tcp."
@@ -80,11 +93,11 @@ class mDNSPlugin : Plugin() {
80
93
  */
81
94
  @PluginMethod
82
95
  fun startBroadcast(call: PluginCall) {
83
- val type = call.getString("type") ?: "_http._tcp."
84
- val name = call.getString("name") ?: (context?.packageName ?: "DevIOArtsMDNS")
96
+ val type = normalizeType(call.getString("type"))
97
+ val name = normalizeName(call.getString("name"))
85
98
  val port = call.getInt("port") ?: 0
86
99
 
87
- if (port <= 0) {
100
+ if (port !in 1..65535) {
88
101
  call.resolve(jsResultBroadcast(false, "", true, "Missing/invalid port"))
89
102
  return
90
103
  }
@@ -127,8 +140,8 @@ class mDNSPlugin : Plugin() {
127
140
  */
128
141
  @PluginMethod
129
142
  fun discover(call: PluginCall) {
130
- val type = call.getString("type") ?: "_http._tcp."
131
- val targetName = call.getString("name")
143
+ val type = normalizeType(call.getString("type"))
144
+ val targetName = call.getString("name")?.trim()?.takeIf { it.isNotEmpty() }
132
145
  val timeout = call.getInt("timeout") ?: 3000
133
146
 
134
147
  scope.launch {
package/dist/docs.json CHANGED
@@ -13,6 +13,23 @@
13
13
  }
14
14
  ],
15
15
  "methods": [
16
+ {
17
+ "name": "getPluginPlatform",
18
+ "signature": "() => Promise<MdnsPluginPlatformResult>",
19
+ "parameters": [],
20
+ "returns": "Promise<MdnsPluginPlatformResult>",
21
+ "tags": [
22
+ {
23
+ "name": "returns",
24
+ "text": "Promise resolving to `MdnsPluginPlatformResult`."
25
+ }
26
+ ],
27
+ "docs": "Return the platform implementation currently serving plugin calls.",
28
+ "complexTypes": [
29
+ "MdnsPluginPlatformResult"
30
+ ],
31
+ "slug": "getpluginplatform"
32
+ },
16
33
  {
17
34
  "name": "startBroadcast",
18
35
  "signature": "(options: MdnsBroadcastOptions) => Promise<MdnsBroadcastResult>",
@@ -53,7 +70,7 @@
53
70
  },
54
71
  {
55
72
  "name": "remarks",
56
- "text": "The `publishing` flag may be `false` after a successful stop."
73
+ "text": "The `publishing` flag may be `false` after a successful stop.\nStopping is idempotent and is safe to call even if publishing never fully started."
57
74
  }
58
75
  ],
59
76
  "docs": "Stop advertising the currently registered service (no-op if none).",
@@ -98,6 +115,28 @@
98
115
  "properties": []
99
116
  },
100
117
  "interfaces": [
118
+ {
119
+ "name": "MdnsPluginPlatformResult",
120
+ "slug": "mdnspluginplatformresult",
121
+ "docs": "Result of getPluginPlatform(). Useful for validating that calls are routed\nthrough the expected platform bridge.",
122
+ "tags": [
123
+ {
124
+ "name": "public"
125
+ }
126
+ ],
127
+ "methods": [],
128
+ "properties": [
129
+ {
130
+ "name": "platform",
131
+ "tags": [],
132
+ "docs": "Platform implementation that produced this result.",
133
+ "complexTypes": [
134
+ "MdnsPluginPlatform"
135
+ ],
136
+ "type": "MdnsPluginPlatform"
137
+ }
138
+ ]
139
+ },
101
140
  {
102
141
  "name": "MdnsBroadcastResult",
103
142
  "slug": "mdnsbroadcastresult",
@@ -145,7 +184,7 @@
145
184
  "docs": "Options for starting a Bonjour/mDNS advertisement.",
146
185
  "tags": [
147
186
  {
148
- "text": "- The `type` **must** end with a dot (`.`). If omitted, the implementation appends it.\n- Instance `name` should be short and human-readable; the OS may append `\" (n)\"` to ensure uniqueness.\n- Android NSD does not expose TXT records to other apps; advertising TXT is supported on iOS.",
187
+ "text": "- The `type` **must** end with a dot (`.`). If omitted, the implementation appends it.\n- Instance `name` should be short and human-readable; the OS may append `\" (n)\"` to ensure uniqueness.\n- Android NSD does not expose TXT records to other apps; advertising TXT is supported on iOS.\n- Native implementations guard publish with a timeout and return `error: true` instead of\n leaving the Promise pending when the OS does not deliver a publish callback.\n- Only one advertisement is active at a time. Starting a new one stops the previous one first.",
149
188
  "name": "remarks"
150
189
  },
151
190
  {
@@ -359,7 +398,7 @@
359
398
  "docs": "Options for Bonjour/mDNS discovery.",
360
399
  "tags": [
361
400
  {
362
- "text": "- The `id` filter matches the normalized name and also accepts prefix matches\n (to handle OS-added `\" (n)\"` suffixes).\n- When `useNW` is `true` on iOS, discovery uses `NWBrowser` for better P2P behavior.",
401
+ "text": "- The `name` filter matches the normalized name and also accepts prefix matches\n (to handle OS-added `\" (n)\"` suffixes).\n- When `useNW` is `true` on iOS, discovery uses `NWBrowser` for better P2P behavior.\n- Discovery is timeboxed. Concurrent discovery requests are serialized or safely replace\n the previous native session so old callbacks cannot complete a newer request.",
363
402
  "name": "remarks"
364
403
  },
365
404
  {
@@ -425,6 +464,29 @@
425
464
  ],
426
465
  "enums": [],
427
466
  "typeAliases": [
467
+ {
468
+ "name": "MdnsPluginPlatform",
469
+ "slug": "mdnspluginplatform",
470
+ "docs": "Runtime implementation that handled a plugin call.",
471
+ "types": [
472
+ {
473
+ "text": "'ios'",
474
+ "complexTypes": []
475
+ },
476
+ {
477
+ "text": "'android'",
478
+ "complexTypes": []
479
+ },
480
+ {
481
+ "text": "'electron'",
482
+ "complexTypes": []
483
+ },
484
+ {
485
+ "text": "'web'",
486
+ "complexTypes": []
487
+ }
488
+ ]
489
+ },
428
490
  {
429
491
  "name": "MdnsTxt",
430
492
  "slug": "mdnstxt",