@devioarts/capacitor-mdns 0.0.3 → 0.2.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 (46) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/DevioartsCapacitorMdns.podspec +3 -3
  3. package/Package.swift +1 -1
  4. package/README.md +272 -101
  5. package/android/build.gradle +3 -9
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/java/com/devioarts/capacitor/mdns/AndroidNsdBackend.kt +145 -0
  8. package/android/src/main/java/com/devioarts/capacitor/mdns/MdnsOptions.kt +32 -0
  9. package/android/src/main/java/com/devioarts/capacitor/mdns/MdnsValidation.kt +26 -0
  10. package/android/src/main/java/com/devioarts/capacitor/mdns/NsdBackend.kt +59 -0
  11. package/android/src/main/java/com/devioarts/capacitor/mdns/mDNS.kt +496 -175
  12. package/android/src/main/java/com/devioarts/capacitor/mdns/mDNSPlugin.kt +85 -49
  13. package/dist/docs.json +313 -15
  14. package/dist/esm/definitions.d.ts +89 -16
  15. package/dist/esm/definitions.js.map +1 -1
  16. package/dist/esm/index.js +22 -1
  17. package/dist/esm/index.js.map +1 -1
  18. package/dist/esm/web.d.ts +8 -1
  19. package/dist/esm/web.js +35 -3
  20. package/dist/esm/web.js.map +1 -1
  21. package/dist/plugin.cjs.js +57 -4
  22. package/dist/plugin.cjs.js.map +1 -1
  23. package/dist/plugin.js +57 -4
  24. package/dist/plugin.js.map +1 -1
  25. package/docs/electron.md +91 -0
  26. package/docs/platform-setup.md +58 -0
  27. package/docs/playground.md +50 -0
  28. package/docs/runtime-behavior.md +80 -0
  29. package/electron/dist/plugin-settings.js +22 -0
  30. package/electron/dist/plugin-settings.js.map +1 -0
  31. package/electron/dist/plugin.cjs.js +774 -0
  32. package/electron/dist/plugin.cjs.js.map +1 -0
  33. package/electron/global.d.ts +2 -0
  34. package/electron/mdns-bridge.cjs +33 -4
  35. package/electron/mdns.ts +628 -107
  36. package/electron/rollup.config.mjs +22 -0
  37. package/electron/src/index.ts +2 -0
  38. package/electron/src/plugin-settings.ts +19 -0
  39. package/electron/tsconfig.json +21 -0
  40. package/electron/types-public/index.d.ts +47 -0
  41. package/ios/Sources/mDNSPlugin/mDNS.swift +481 -124
  42. package/ios/Sources/mDNSPlugin/mDNSPlugin.swift +85 -23
  43. package/ios/Tests/mDNSPluginTests/BridgeTests.swift +46 -0
  44. package/ios/Tests/mDNSPluginTests/MDNSTests.swift +490 -0
  45. package/package.json +55 -20
  46. package/ios/Tests/mDNSPluginTests/mDNSPluginTests.swift +0 -15
@@ -1,5 +1,6 @@
1
1
  import Capacitor
2
2
  import Foundation
3
+ import CoreFoundation
3
4
 
4
5
  /**
5
6
  * Capacitor bridge for the iOS mDNS plugin.
@@ -20,44 +21,68 @@ public class mDNSPlugin: CAPPlugin, CAPBridgedPlugin {
20
21
  public let identifier = "mDNSPlugin"
21
22
  public let jsName = "mDNS"
22
23
  public let pluginMethods: [CAPPluginMethod] = [
24
+ CAPPluginMethod(name: "getPluginPlatform", returnType: CAPPluginReturnPromise),
23
25
  CAPPluginMethod(name: "startBroadcast", returnType: CAPPluginReturnPromise),
24
26
  CAPPluginMethod(name: "stopBroadcast", returnType: CAPPluginReturnPromise),
25
- CAPPluginMethod(name: "discover", returnType: CAPPluginReturnPromise)
27
+ CAPPluginMethod(name: "discover", returnType: CAPPluginReturnPromise),
28
+ CAPPluginMethod(name: "watchServices", returnType: CAPPluginReturnPromise),
29
+ CAPPluginMethod(name: "stopWatching", returnType: CAPPluginReturnPromise)
26
30
  ]
27
31
 
28
32
  private let mdns = MDNS()
29
33
 
34
+ override public func load() {
35
+ super.load()
36
+ mdns.watchDelegate = self
37
+ }
38
+
30
39
  // MARK: - Helpers
31
40
 
32
41
  private func jnull(_ v: String?) -> Any {
33
42
  return v ?? NSNull()
34
43
  }
35
44
 
36
- private func normalizeType(_ raw: String?) -> String {
37
- var t = raw ?? "_http._tcp."
38
- if t.last != "." { t += "." }
39
- return t
45
+ private func stringOption(_ call: CAPPluginCall, _ key: String) throws -> String? {
46
+ guard let raw = call.options[key], !(raw is NSNull) else { return nil }
47
+ guard let value = raw as? String else { throw inputError("Invalid \(key)") }
48
+ return value
49
+ }
50
+ private func inputError(_ message: String) -> NSError {
51
+ NSError(domain: "mDNS", code: -1, userInfo: [NSLocalizedDescriptionKey: message])
52
+ }
53
+ private func numberOption(_ call: CAPPluginCall, _ key: String, fallback: Double) throws -> Double {
54
+ guard let raw = call.options[key], !(raw is NSNull) else { return fallback }
55
+ guard let value = raw as? NSNumber, CFGetTypeID(value) != CFBooleanGetTypeID(), value.doubleValue.isFinite else { throw inputError("Invalid \(key)") }
56
+ return value.doubleValue
40
57
  }
41
-
42
58
  // MARK: - API
43
59
 
60
+ /// getPluginPlatform()
61
+ @objc public func getPluginPlatform(_ call: CAPPluginCall) {
62
+ call.resolve([
63
+ "platform": "ios"
64
+ ])
65
+ }
66
+
44
67
  /// startBroadcast({ type?, name?, port, txt? })
45
68
  @objc public func startBroadcast(_ call: CAPPluginCall) {
46
- let type = normalizeType(call.getString("type"))
47
- let name = (call.getString("name")?.trimmingCharacters(in: .whitespacesAndNewlines)).flatMap { $0.isEmpty ? nil : $0 } ?? "DevIOArtsMDNS"
48
- let port = call.getInt("port") ?? 0
49
- let txt = call.getObject("txt") as? [String: String]
50
-
51
- guard port > 0 else {
52
- call.resolve([
53
- "publishing": false,
54
- "name": "",
55
- "error": true,
56
- "errorMessage": "Missing/invalid port"
57
- ])
69
+ let type: String, name: String, port: Int, txt: [String: String]?
70
+ do {
71
+ type = try MDNS.validatedType(stringOption(call, "type") ?? "")
72
+ name = try MDNS.validatedName(stringOption(call, "name") ?? "")
73
+ let number = try numberOption(call, "port", fallback: 0)
74
+ guard (1...65535).contains(number), number.rounded(.towardZero) == number else { throw inputError("Missing/invalid port") }
75
+ port = Int(number)
76
+ let raw = call.options["txt"]
77
+ if raw == nil || raw is NSNull { txt = nil } else {
78
+ guard let values = raw as? [String: String] else { throw inputError("TXT must be an object of strings") }
79
+ txt = values
80
+ }
81
+ try MDNS.validateTXT(txt)
82
+ } catch {
83
+ call.resolve(["publishing": mdns.isPublishing, "name": "", "error": true, "errorMessage": error.localizedDescription])
58
84
  return
59
85
  }
60
-
61
86
  mdns.broadcast(type: type, name: name, port: port, txt: txt) { result in
62
87
  switch result {
63
88
  case .success(let finalName):
@@ -98,11 +123,17 @@ public class mDNSPlugin: CAPPlugin, CAPBridgedPlugin {
98
123
 
99
124
  /// discover({ type?, name?, timeout? })
100
125
  @objc public func discover(_ call: CAPPluginCall) {
101
- let type = normalizeType(call.getString("type"))
102
- let targetName = call.getString("name")
103
- let timeout = call.getInt("timeout") ?? 3000
126
+ let type: String, targetName: String?, timeout: Int
127
+ do {
128
+ type = try MDNS.validatedType(stringOption(call, "type") ?? "")
129
+ let name = try MDNS.validatedName(stringOption(call, "name") ?? "")
130
+ targetName = name.isEmpty ? nil : name
131
+ timeout = Int(min(2_147_483_647, max(0, try numberOption(call, "timeout", fallback: 3000))))
132
+ } catch {
133
+ call.resolve(["error": true, "errorMessage": error.localizedDescription, "servicesFound": 0, "services": []])
134
+ return
135
+ }
104
136
  let useNW = call.getBool("useNW") ?? true
105
-
106
137
  mdns.discover(type: type, name: targetName, timeoutMs: timeout, useNW: useNW) { result in
107
138
  switch result {
108
139
  case .success(let services):
@@ -122,4 +153,35 @@ public class mDNSPlugin: CAPPlugin, CAPBridgedPlugin {
122
153
  }
123
154
  }
124
155
  }
156
+
157
+ @objc public func watchServices(_ call: CAPPluginCall) {
158
+ guard let types = call.getArray("types", String.self), !types.isEmpty else {
159
+ call.resolve(["error": true, "errorMessage": "Expected a nonempty array of service types", "watchersStarted": 0])
160
+ return
161
+ }
162
+ mdns.watchServices(types: types) { result in
163
+ switch result {
164
+ case .success(let count): call.resolve(["error": false, "errorMessage": NSNull(), "watchersStarted": count])
165
+ case .failure(let error): call.resolve(["error": true, "errorMessage": error.localizedDescription, "watchersStarted": 0])
166
+ }
167
+ }
168
+ }
169
+
170
+ @objc public func stopWatching(_ call: CAPPluginCall) {
171
+ mdns.stopWatching {
172
+ call.resolve(["watching": false, "error": false, "errorMessage": NSNull()])
173
+ }
174
+ }
175
+ }
176
+
177
+ extension mDNSPlugin: MDNSWatchDelegate {
178
+ public func mdns(_ mdns: MDNS, didFindService service: [String: Any]) {
179
+ notifyListeners("serviceFound", data: ["service": service])
180
+ }
181
+ public func mdns(_ mdns: MDNS, didLoseService service: [String: Any]) {
182
+ notifyListeners("serviceLost", data: ["service": service])
183
+ }
184
+ public func mdns(_ mdns: MDNS, watchError type: String, message: String, watching: Bool) {
185
+ notifyListeners("watchError", data: ["type": type, "errorMessage": message, "watching": watching])
186
+ }
125
187
  }
@@ -0,0 +1,46 @@
1
+ import Capacitor
2
+ import XCTest
3
+ @testable import mDNSPlugin
4
+
5
+ final class BridgeTests: XCTestCase {
6
+ private func invoke(_ method: String, options: [String: Any], perform: (CAPPluginCall) -> Void) throws -> [String: Any] {
7
+ var result: [String: Any]?
8
+ let call = try XCTUnwrap(CAPPluginCall(
9
+ callbackId: "test", methodName: method, options: options,
10
+ success: { response, _ in result = response?.data },
11
+ error: { _ in XCTFail("Invalid input must resolve with error: true") }
12
+ ))
13
+ perform(call)
14
+ return try XCTUnwrap(result)
15
+ }
16
+
17
+ func testBroadcastRejectsInvalidRawBridgeValues() throws {
18
+ let plugin = mDNSPlugin()
19
+ let cases: [[String: Any]] = [
20
+ ["port": 80.5], ["port": true], ["port": "80"], ["port": Double.nan],
21
+ ["port": 0], ["port": 65536], ["port": 80, "txt": ["a": 1]],
22
+ ["port": 80, "txt": ["a": true]], ["port": 80, "type": 1], ["port": 80, "name": true]
23
+ ]
24
+ for options in cases {
25
+ let result = try invoke("startBroadcast", options: options, perform: plugin.startBroadcast)
26
+ XCTAssertEqual(result["error"] as? Bool, true, "\(options)")
27
+ XCTAssertEqual(result["publishing"] as? Bool, false)
28
+ XCTAssertEqual(result["name"] as? String, "")
29
+ XCTAssertFalse(try XCTUnwrap(result["errorMessage"] as? String).isEmpty)
30
+ }
31
+ }
32
+
33
+ func testDiscoveryRejectsInvalidRawBridgeValues() throws {
34
+ let plugin = mDNSPlugin()
35
+ let cases: [[String: Any]] = [
36
+ ["timeout": true], ["timeout": "50"], ["timeout": Double.nan],
37
+ ["timeout": Double.infinity], ["type": 1], ["name": true]
38
+ ]
39
+ for options in cases {
40
+ let result = try invoke("discover", options: options, perform: plugin.discover)
41
+ XCTAssertEqual(result["error"] as? Bool, true)
42
+ XCTAssertEqual(result["servicesFound"] as? Int, 0)
43
+ XCTAssertTrue(try XCTUnwrap(result["services"] as? [Any]).isEmpty)
44
+ }
45
+ }
46
+ }