@devioarts/capacitor-mdns 0.0.2 → 0.0.3

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/Package.swift CHANGED
@@ -10,7 +10,7 @@ let package = Package(
10
10
  targets: ["mDNSPlugin"])
11
11
  ],
12
12
  dependencies: [
13
- .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "7.0.0")
13
+ .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0")
14
14
  ],
15
15
  targets: [
16
16
  .target(
@@ -11,7 +11,7 @@ buildscript {
11
11
  mavenCentral()
12
12
  }
13
13
  dependencies {
14
- classpath 'com.android.tools.build:gradle:8.7.2'
14
+ classpath 'com.android.tools.build:gradle:8.13.2'
15
15
  classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.24"
16
16
  }
17
17
  }
@@ -20,23 +20,23 @@ apply plugin: 'com.android.library'
20
20
  apply plugin: 'org.jetbrains.kotlin.android'
21
21
 
22
22
  android {
23
- namespace "com.devioarts.capacitor.mdns"
24
- compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35
23
+ namespace = "com.devioarts.capacitor.mdns"
24
+ compileSdk = project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 36
25
25
  defaultConfig {
26
- minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
27
- targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35
28
- versionCode 1
29
- versionName "1.0"
30
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
26
+ minSdk = project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
27
+ targetSdk = project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 36
28
+ versionCode = 1
29
+ versionName = "1.0"
30
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
31
31
  }
32
32
  buildTypes {
33
33
  release {
34
- minifyEnabled false
34
+ minifyEnabled = false
35
35
  proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
36
36
  }
37
37
  }
38
- lintOptions {
39
- abortOnError false
38
+ lint {
39
+ abortOnError = false
40
40
  }
41
41
  compileOptions {
42
42
  sourceCompatibility JavaVersion.VERSION_21
@@ -4,9 +4,11 @@ package com.devioarts.capacitor.mdns
4
4
  import android.content.Context
5
5
  import android.net.nsd.NsdManager
6
6
  import android.net.nsd.NsdServiceInfo
7
+ import android.os.Build
7
8
  import android.os.Handler
8
9
  import android.os.Looper
9
10
  import kotlinx.coroutines.*
11
+ import java.util.concurrent.Executor
10
12
  import java.util.concurrent.atomic.AtomicReference
11
13
 
12
14
  /**
@@ -33,7 +35,7 @@ class mDNS(
33
35
  data class MdnsService(
34
36
  val name: String,
35
37
  val type: String,
36
- val host: String?, // numeric address (v4/v6) or null if resolve fails
38
+ val hosts: List<String>, // numeric addresses (v4/v6)
37
39
  val port: Int
38
40
  )
39
41
 
@@ -50,6 +52,7 @@ class mDNS(
50
52
 
51
53
  /** Main looper utilities for non-suspending entry points. */
52
54
  private val mainHandler = Handler(Looper.getMainLooper())
55
+ private val mainExecutor = Executor { command -> mainHandler.post(command) }
53
56
  private inline fun runOnMain(crossinline block: () -> Unit) {
54
57
  if (Looper.myLooper() === Looper.getMainLooper()) block() else mainHandler.post { block() }
55
58
  }
@@ -130,6 +133,7 @@ class mDNS(
130
133
  // Prepare state
131
134
  discListenerRef.getAndSet(null)?.let { safeStopDiscovery(it) }
132
135
  val found = mutableListOf<MdnsService>()
136
+ val serviceInfoCallbacks = mutableListOf<NsdManager.ServiceInfoCallback>()
133
137
  val result = CompletableDeferred<List<MdnsService>>()
134
138
 
135
139
  // Normalization for "(n)" suffix appended by the OS.
@@ -139,12 +143,70 @@ class mDNS(
139
143
  val t = normalize(targetName ?: return true) // if no target, accept all
140
144
  return (c == t) || c.startsWith(t)
141
145
  }
146
+ fun upsert(item: MdnsService) {
147
+ val index = found.indexOfFirst { it.name == item.name && it.type == item.type && it.port == item.port }
148
+ if (index >= 0) found[index] = item else found.add(item)
149
+ }
150
+ fun unregisterServiceInfoCallbacks() {
151
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return
152
+ serviceInfoCallbacks.toList().forEach { safeUnregisterServiceInfoCallback(it) }
153
+ serviceInfoCallbacks.clear()
154
+ }
155
+ fun completeWithCurrentResults(discoveryListener: NsdManager.DiscoveryListener) {
156
+ unregisterServiceInfoCallbacks()
157
+ safeStopDiscovery(discoveryListener)
158
+ if (!result.isCompleted) result.complete(found.toList())
159
+ }
160
+
161
+ @Suppress("DEPRECATION")
162
+ fun resolveLegacy(si: NsdServiceInfo, discoveryListener: NsdManager.DiscoveryListener) {
163
+ nsd.resolveService(si, object : NsdManager.ResolveListener {
164
+ override fun onResolveFailed(s: NsdServiceInfo, errorCode: Int) { /* ignore */ }
165
+
166
+ override fun onServiceResolved(s: NsdServiceInfo) {
167
+ upsert(toMdnsService(s))
168
+
169
+ // Early-exit for exact/prefix target match.
170
+ if (targetName != null && matchesTarget(s.serviceName)) {
171
+ completeWithCurrentResults(discoveryListener)
172
+ }
173
+ }
174
+ })
175
+ }
176
+
177
+ fun registerServiceInfoCallback(si: NsdServiceInfo, discoveryListener: NsdManager.DiscoveryListener) {
178
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
179
+ resolveLegacy(si, discoveryListener)
180
+ return
181
+ }
182
+
183
+ val callback = object : NsdManager.ServiceInfoCallback {
184
+ override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) { /* ignore */ }
185
+ override fun onServiceInfoCallbackUnregistered() { /* no-op */ }
186
+ override fun onServiceLost() { /* no-op */ }
187
+
188
+ override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
189
+ upsert(toMdnsService(serviceInfo))
190
+
191
+ // Early-exit for exact/prefix target match.
192
+ if (targetName != null && matchesTarget(serviceInfo.serviceName)) {
193
+ completeWithCurrentResults(discoveryListener)
194
+ }
195
+ }
196
+ }
197
+
198
+ serviceInfoCallbacks.add(callback)
199
+ try {
200
+ nsd.registerServiceInfoCallback(si, mainExecutor, callback)
201
+ } catch (_: Throwable) {
202
+ serviceInfoCallbacks.remove(callback)
203
+ }
204
+ }
142
205
 
143
206
  val listener = object : NsdManager.DiscoveryListener {
144
207
  override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
145
208
  // Fail fast: stop discovery and complete with whatever we have (likely empty).
146
- safeStopDiscovery(this)
147
- if (!result.isCompleted) result.complete(found.toList())
209
+ completeWithCurrentResults(this)
148
210
  }
149
211
  override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) { /* no-op */ }
150
212
  override fun onDiscoveryStarted(serviceType: String) { /* no-op */ }
@@ -154,25 +216,7 @@ class mDNS(
154
216
  if (targetName != null && !matchesTarget(si.serviceName)) return
155
217
 
156
218
  val discoveryListener = this
157
- nsd.resolveService(si, object : NsdManager.ResolveListener {
158
- override fun onResolveFailed(s: NsdServiceInfo, errorCode: Int) { /* ignore */ }
159
-
160
- override fun onServiceResolved(s: NsdServiceInfo) {
161
- val item = MdnsService(
162
- name = s.serviceName,
163
- type = s.serviceType,
164
- host = s.host?.hostAddress,
165
- port = s.port
166
- )
167
- found.add(item)
168
-
169
- // Early-exit for exact/prefix target match.
170
- if (targetName != null && matchesTarget(s.serviceName)) {
171
- safeStopDiscovery(discoveryListener)
172
- if (!result.isCompleted) result.complete(found.toList())
173
- }
174
- }
175
- })
219
+ registerServiceInfoCallback(si, discoveryListener)
176
220
  }
177
221
 
178
222
  override fun onServiceLost(serviceInfo: NsdServiceInfo) { /* no-op */ }
@@ -184,8 +228,7 @@ class mDNS(
184
228
 
185
229
  val timeoutJob = scope.launch {
186
230
  delay(timeoutMs.toLong())
187
- safeStopDiscovery(listener)
188
- if (!result.isCompleted) result.complete(found.toList())
231
+ completeWithCurrentResults(listener)
189
232
  }
190
233
 
191
234
  try {
@@ -193,6 +236,7 @@ class mDNS(
193
236
  } finally {
194
237
  // Cleanup in all cases (early-exit or timeout)
195
238
  timeoutJob.cancel()
239
+ unregisterServiceInfoCallbacks()
196
240
  discListenerRef.getAndSet(null)?.let { safeStopDiscovery(it) }
197
241
  }
198
242
  }
@@ -218,4 +262,29 @@ class mDNS(
218
262
  private fun safeStopDiscovery(l: NsdManager.DiscoveryListener) {
219
263
  try { nsd.stopServiceDiscovery(l) } catch (_: Throwable) {}
220
264
  }
265
+
266
+ /** Best-effort stop service info updates; NSD may throw if not registered. */
267
+ private fun safeUnregisterServiceInfoCallback(l: NsdManager.ServiceInfoCallback) {
268
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
269
+ try { nsd.unregisterServiceInfoCallback(l) } catch (_: Throwable) {}
270
+ }
271
+ }
272
+
273
+ private fun toMdnsService(s: NsdServiceInfo): MdnsService =
274
+ MdnsService(
275
+ name = s.serviceName,
276
+ type = s.serviceType,
277
+ hosts = hostAddresses(s),
278
+ port = s.port
279
+ )
280
+
281
+ private fun hostAddresses(s: NsdServiceInfo): List<String> =
282
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
283
+ s.hostAddresses.mapNotNull { it.hostAddress }
284
+ } else {
285
+ legacyHostAddress(s)?.let { listOf(it) } ?: emptyList()
286
+ }
287
+
288
+ @Suppress("DEPRECATION")
289
+ private fun legacyHostAddress(s: NsdServiceInfo): String? = s.host?.hostAddress
221
290
  }
@@ -26,7 +26,7 @@ import org.json.JSONObject
26
26
  * - discover -> {
27
27
  * error: boolean, errorMessage: string|null,
28
28
  * servicesFound: number,
29
- * services: Array<{ name, type, domain: "local.", port, hosts?: string[] }>
29
+ * services: Array<{ name, type, domain: "local.", port, hosts: string[] }>
30
30
  * }
31
31
  */
32
32
  @CapacitorPlugin(name = "mDNS")
@@ -139,10 +139,8 @@ class mDNSPlugin : Plugin() {
139
139
  put("name", s.name)
140
140
  put("type", s.type) // Android returns full type with dot
141
141
  put("domain", "local.") // NSD is mDNS only
142
- // normalize hosts to [] or [addr]
143
142
  val hosts = JSONArray()
144
- val addr = s.host
145
- if (!addr.isNullOrEmpty()) hosts.put(addr)
143
+ s.hosts.forEach { hosts.put(it) }
146
144
  put("hosts", hosts)
147
145
  put("port", s.port)
148
146
  // TXT not available via NSD -> omitted on Android
@@ -3,7 +3,7 @@
3
3
  * Values are UTF-8 strings; binary payloads are not supported by this API.
4
4
  * @public
5
5
  */
6
- export declare type MdnsTxt = Record<string, string>;
6
+ export type MdnsTxt = Record<string, string>;
7
7
  /**
8
8
  * Normalized description of a discovered Bonjour/mDNS service.
9
9
  * Returned from {@link mDNSPlugin.discover}.
@@ -3,7 +3,6 @@
3
3
  // const { createMDNSAPI } = require('.../electron/mdns-bridge.cjs')
4
4
  // contextBridge.exposeInMainWorld('mDNS', createMDNSAPI({ ipcRenderer }))
5
5
  // contextBridge.exposeInMainWorld('mdns', createMDNSAPI({ ipcRenderer })) // alias
6
- // eslint-disable-next-line no-undef
7
6
  module.exports.createMDNSAPI = ({ ipcRenderer }) => {
8
7
  return {
9
8
  /**
@@ -26,4 +25,4 @@ module.exports.createMDNSAPI = ({ ipcRenderer }) => {
26
25
  */
27
26
  discover: (options) => ipcRenderer.invoke('mdns:discover', options),
28
27
  }
29
- }
28
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devioarts/capacitor-mdns",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "mDNS plugin for CapacitorJS",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
@@ -57,31 +57,28 @@
57
57
  "release": "npm publish --access public"
58
58
  },
59
59
  "devDependencies": {
60
- "@capacitor/android": "^7.0.0",
61
- "@capacitor/core": "^7.0.0",
62
- "@capacitor/docgen": "^0.3.0",
63
- "@capacitor/ios": "^7.0.0",
64
- "@ionic/eslint-config": "^0.4.0",
60
+ "@capacitor/android": "^8.4.0",
61
+ "@capacitor/core": "^8.4.0",
62
+ "@capacitor/docgen": "^0.3.1",
63
+ "@capacitor/ios": "^8.4.0",
65
64
  "@ionic/prettier-config": "^4.0.0",
66
65
  "@ionic/swiftlint-config": "^2.0.0",
67
- "eslint": "^8.57.0",
68
- "prettier": "^3.4.2",
69
- "prettier-plugin-java": "^2.6.6",
70
- "rimraf": "^6.0.1",
71
- "rollup": "^4.30.1",
66
+ "@typescript-eslint/eslint-plugin": "^8.61.0",
67
+ "@typescript-eslint/parser": "^8.61.0",
68
+ "bonjour-service": "^1.4.0",
69
+ "electron": "^42.4.0",
70
+ "eslint": "^10.4.1",
71
+ "prettier": "^3.8.4",
72
+ "prettier-plugin-java": "^2.9.7",
73
+ "rimraf": "^6.1.3",
74
+ "rollup": "^4.61.1",
72
75
  "swiftlint": "^2.0.0",
73
- "typescript": "~4.1.5",
74
- "electron": "^37.3.1",
75
- "bonjour-service": "^1.3.0"
76
+ "typescript": "^6.0.3"
76
77
  },
77
78
  "peerDependencies": {
78
- "@capacitor/core": ">=7.0.0"
79
+ "@capacitor/core": ">=8.0.0"
79
80
  },
80
81
  "prettier": "@ionic/prettier-config",
81
- "swiftlint": "@ionic/swiftlint-config",
82
- "eslintConfig": {
83
- "extends": "@ionic/eslint-config/recommended"
84
- },
85
82
  "capacitor": {
86
83
  "ios": {
87
84
  "src": "ios"