@cappitolian/network-discovery 0.0.13 → 0.0.14

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.
@@ -4,55 +4,168 @@ import Network
4
4
  @objc public class NetworkDiscovery: NSObject {
5
5
  private var listener: NWListener?
6
6
  private var browser: NWBrowser?
7
+ private var activeConnections: [NWConnection] = []
7
8
 
8
9
  @objc public func startPublishing(name: String, type: String, port: Int, metadata: [String: String]) throws {
10
+ stopServer()
9
11
  let nwPort = NWEndpoint.Port(integerLiteral: UInt16(port))
10
- // Limpieza de tipo para Bonjour: _ssspos._tcp
11
12
  let cleanType = type.contains("_") ? type : "_\(type)._tcp"
12
13
 
13
- let service = NWListener.Service(name: name, type: cleanType)
14
- var txt = TXTRecord()
15
- for (key, value) in metadata { txt[key] = value }
16
- service.txtRecord = txt
14
+ let ipForName = metadata["ip"] ?? ""
15
+ let displayName = ipForName.isEmpty ? name : "\(name)-\(ipForName)"
17
16
 
18
- listener = try NWListener(using: .tcp, on: nwPort)
17
+ let mappedMetadata = metadata.mapValues { $0.data(using: .utf8)! }
18
+ let txtData = NetService.data(fromTXTRecord: mappedMetadata)
19
+
20
+ // --- CONFIGURACIÓN MEJORADA PARA COMPATIBILIDAD CROSS-PLATFORM ---
21
+ let params = NWParameters.tcp
22
+ params.includePeerToPeer = true
23
+ params.allowLocalEndpointReuse = true // Permite reutilización del puerto
24
+ params.acceptLocalOnly = false // Acepta conexiones de toda la red local
25
+
26
+ // Configuración adicional para que Android pueda resolver
27
+ if let tcpOptions = params.defaultProtocolStack.internetProtocol as? NWProtocolIP.Options {
28
+ tcpOptions.version = .any // IPv4 e IPv6
29
+ }
30
+
31
+ let service = NWListener.Service(name: displayName, type: cleanType, domain: nil, txtRecord: txtData)
32
+
33
+ listener = try NWListener(using: params, on: nwPort)
19
34
  listener?.service = service
35
+
36
+ // --- HANDLER CRÍTICO: Aceptar conexiones entrantes ---
37
+ listener?.newConnectionHandler = { [weak self] connection in
38
+ print("NSD_LOG: iOS recibió conexión de: \(connection.endpoint)")
39
+
40
+ // Iniciar la conexión para que Android pueda resolver el servicio
41
+ connection.start(queue: .main)
42
+ self?.activeConnections.append(connection)
43
+
44
+ // Configurar handlers básicos
45
+ connection.stateUpdateHandler = { state in
46
+ if case .ready = state {
47
+ print("NSD_LOG: Conexión establecida con Android")
48
+ }
49
+ }
50
+
51
+ // Limpieza automática cuando la conexión termine
52
+ connection.receiveMessage { _, _, _, error in
53
+ if error != nil {
54
+ connection.cancel()
55
+ self?.activeConnections.removeAll { $0 === connection }
56
+ }
57
+ }
58
+ }
59
+
60
+ listener?.stateUpdateHandler = { state in
61
+ switch state {
62
+ case .ready:
63
+ if let port = self.listener?.port {
64
+ print("NSD_LOG: ✅ iOS Servidor LISTO en puerto \(port): \(displayName)")
65
+ }
66
+ case .failed(let error):
67
+ print("NSD_LOG: ❌ iOS Servidor falló: \(error)")
68
+ case .waiting(let error):
69
+ print("NSD_LOG: ⏳ iOS Servidor esperando: \(error)")
70
+ default:
71
+ break
72
+ }
73
+ }
74
+
20
75
  listener?.start(queue: .main)
76
+
77
+ // Pequeño delay para asegurar que el servicio esté completamente publicado
78
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
79
+ print("NSD_LOG: Servicio iOS completamente inicializado")
80
+ }
21
81
  }
22
82
 
23
83
  @objc public func findService(name: String, type: String, timeout: TimeInterval, completion: @escaping ([String: Any]?) -> Void) {
84
+ stopDiscovery()
24
85
  let cleanType = type.contains("_") ? type : "_\(type)._tcp"
25
- let parameters = NWParameters()
26
- parameters.includePeerToPeer = true
27
86
 
28
- browser = NWBrowser(for: .bonjour(type: cleanType, domain: nil), using: parameters)
87
+ // Configuración mejorada para el browser
88
+ let params = NWParameters()
89
+ params.includePeerToPeer = true
29
90
 
30
- browser?.browseResultsChangedHandler = { results, changes in
91
+ let browser = NWBrowser(for: .bonjour(type: cleanType, domain: nil), using: params)
92
+ self.browser = browser
93
+ var finished = false
94
+
95
+ browser.browseResultsChangedHandler = { [weak self] results, _ in
31
96
  for result in results {
32
- if case let .service(foundName, _, _, _) = result.endpoint, foundName == name {
33
- // En iOS, el resolve de IP requiere un paso adicional de conexión
34
- // o extraerlo del TXTRecord si lo envías ahí.
35
- self.stopBrowsing()
36
- completion(["ip": "vía-mDNS", "port": 0]) // Placeholder lógico
97
+ if case let .service(foundName, _, _, _) = result.endpoint, foundName.contains(name) {
98
+ if !finished {
99
+ finished = true
100
+
101
+ print("NSD_LOG: iOS Cliente encontró servicio: \(foundName)")
102
+
103
+ var meta: [String: String] = [:]
104
+ if case let .bonjour(txt) = result.metadata {
105
+ for (k, v) in txt.dictionary {
106
+ if let dataValue = v as? Data {
107
+ meta[k] = String(data: dataValue, encoding: .utf8) ?? ""
108
+ }
109
+ }
110
+ }
111
+
112
+ // --- LÓGICA DE EXTRACCIÓN ROBUSTA ---
113
+ var ip = meta["ip"] ?? ""
114
+
115
+ if (ip.isEmpty || ip == "0.0.0.0") && foundName.contains("-") {
116
+ ip = foundName.components(separatedBy: "-").last ?? ""
117
+ }
118
+
119
+ if ip.isEmpty || ip == "0.0.0.0" {
120
+ if case let .hostPort(host, _) = result.endpoint {
121
+ ip = "\(host)".components(separatedBy: "%").first ?? "\(host)"
122
+ }
123
+ }
124
+
125
+ print("NSD_LOG: IP extraída: \(ip)")
126
+
127
+ self?.stopDiscovery()
128
+ completion(["ip": ip, "port": 8081, "metadata": meta])
129
+ }
37
130
  return
38
131
  }
39
132
  }
40
133
  }
41
134
 
42
- browser?.start(queue: .main)
43
- DispatchQueue.main.asyncAfter(deadline: .now() + timeout) {
44
- self.stopBrowsing()
45
- completion(nil)
135
+ browser.stateUpdateHandler = { state in
136
+ switch state {
137
+ case .ready:
138
+ print("NSD_LOG: iOS Browser listo para buscar")
139
+ case .failed(let error):
140
+ print("NSD_LOG: iOS Browser falló: \(error)")
141
+ default:
142
+ break
143
+ }
144
+ }
145
+
146
+ browser.start(queue: .main)
147
+ DispatchQueue.main.asyncAfter(deadline: .now() + timeout) { [weak self] in
148
+ if !finished {
149
+ finished = true
150
+ print("NSD_LOG: iOS Búsqueda timeout")
151
+ self?.stopDiscovery()
152
+ completion(nil)
153
+ }
46
154
  }
47
155
  }
48
156
 
49
- private func stopBrowsing() {
50
- browser?.cancel()
51
- browser = nil
52
- }
53
-
54
- @objc public func stopPublishing() {
157
+ @objc public func stopServer() {
158
+ // Limpiar todas las conexiones activas
159
+ activeConnections.forEach { $0.cancel() }
160
+ activeConnections.removeAll()
161
+
55
162
  listener?.cancel()
56
163
  listener = nil
164
+ print("NSD_LOG: iOS Servidor detenido")
165
+ }
166
+
167
+ @objc public func stopDiscovery() {
168
+ browser?.cancel()
169
+ browser = nil
57
170
  }
58
171
  }
@@ -1,16 +1,10 @@
1
1
  import Foundation
2
2
  import Capacitor
3
3
 
4
- /**
5
- * Registro moderno de Capacitor para Swift.
6
- * 'NetworkDiscovery' es el nombre que usas en registerPlugin en TS.
7
- */
8
4
  @objc(NetworkDiscoveryPlugin)
9
5
  public class NetworkDiscoveryPlugin: CAPPlugin, CAPBridgedPlugin {
10
- public let identifier = "NetworkDiscoveryPlugin"
11
- public let jsName = "NetworkDiscovery"
12
-
13
- // Mapeo de métodos para el Bridge
6
+ public let identifier = "NetworkDiscoveryPlugin"
7
+ public let jsName = "NetworkDiscovery"
14
8
  public let pluginMethods: [CAPPluginMethod] = [
15
9
  CAPPluginMethod(name: "startServer", returnType: CAPPluginReturnPromise),
16
10
  CAPPluginMethod(name: "stopServer", returnType: CAPPluginReturnPromise),
@@ -22,44 +16,28 @@ public class NetworkDiscoveryPlugin: CAPPlugin, CAPBridgedPlugin {
22
16
  @objc func startServer(_ call: CAPPluginCall) {
23
17
  guard let name = call.getString("serviceName"),
24
18
  let type = call.getString("serviceType"),
25
- let port = call.getInt("port") else {
26
- call.reject("Faltan parámetros obligatorios")
27
- return
28
- }
19
+ let port = call.getInt("port") else { return call.reject("Faltan parámetros") }
29
20
 
30
21
  let metadata = call.getObject("metadata") as? [String: String] ?? [:]
31
-
32
22
  do {
33
23
  try implementation.startPublishing(name: name, type: type, port: port, metadata: metadata)
34
24
  call.resolve()
35
- } catch {
36
- call.reject("Error iniciando servidor mDNS: \(error.localizedDescription)")
37
- }
25
+ } catch { call.reject(error.localizedDescription) }
38
26
  }
39
27
 
40
28
  @objc func stopServer(_ call: CAPPluginCall) {
41
- implementation.stopPublishing()
29
+ implementation.stopServer()
42
30
  call.resolve()
43
31
  }
44
32
 
45
33
  @objc func findServer(_ call: CAPPluginCall) {
46
34
  guard let name = call.getString("serviceName"),
47
- let type = call.getString("serviceType") else {
48
- call.reject("Nombre o tipo de servicio no proporcionado")
49
- return
50
- }
51
- let timeout = Double(call.getInt("timeout") ?? 5000) / 1000.0
52
-
35
+ let type = call.getString("serviceType") else { return call.reject("Faltan parámetros") }
36
+
37
+ let timeout = Double(call.getInt("timeout") ?? 10000) / 1000.0
53
38
  implementation.findService(name: name, type: type, timeout: timeout) { result in
54
- if let result = result {
55
- call.resolve([
56
- "ip": result.ip,
57
- "port": result.port,
58
- "metadata": result.metadata
59
- ])
60
- } else {
61
- call.reject("No se encontró el servidor tras \(timeout) segundos")
62
- }
39
+ if let data = result { call.resolve(data) }
40
+ else { call.reject("TIMEOUT_ERROR") }
63
41
  }
64
42
  }
65
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cappitolian/network-discovery",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "description": "A Capacitor plugin for network service discovery using mDNS/Bonjour. Allows automatic server-client connection without manual IP configuration.",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",