@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
@@ -0,0 +1,490 @@
1
+ import XCTest
2
+ import Network
3
+ @testable import mDNSPlugin
4
+
5
+ // MARK: - Fakes
6
+
7
+ /// `NetService` that never touches the network; tests drive the delegate callbacks by hand.
8
+ private final class FakeNetService: NetService {
9
+ var resolveCalls = 0
10
+ var publishCalls = 0
11
+ var stopCalls = 0
12
+ var fakePort = 8080
13
+ var fakeAddresses: [Data]?
14
+ var fakeTXT: Data?
15
+
16
+ override func resolve(withTimeout timeout: TimeInterval) { resolveCalls += 1 }
17
+ override func publish() { publishCalls += 1 }
18
+ override func stop() { stopCalls += 1 }
19
+ override var port: Int { fakePort }
20
+ override var addresses: [Data]? { fakeAddresses }
21
+ override func txtRecordData() -> Data? { fakeTXT }
22
+ }
23
+
24
+ private final class FakeBrowser: NetServiceBrowser {
25
+ var stopCalls = 0
26
+ override func searchForServices(ofType type: String, inDomain domainString: String) {}
27
+ override func stop() { stopCalls += 1 }
28
+ }
29
+
30
+ private final class WatchProbe: MDNSWatchDelegate {
31
+ var found: [[String: Any]] = []
32
+ var lost: [[String: Any]] = []
33
+ var errors: [(String, Bool)] = []
34
+ func mdns(_ mdns: MDNS, didFindService service: [String: Any]) { found.append(service) }
35
+ func mdns(_ mdns: MDNS, didLoseService service: [String: Any]) { lost.append(service) }
36
+ func mdns(_ mdns: MDNS, watchError type: String, message: String, watching: Bool) { errors.append((message, watching)) }
37
+ }
38
+
39
+ private final class Rig {
40
+ let mdns = MDNS()
41
+ var services: [FakeNetService] = []
42
+ var browsers: [FakeBrowser] = []
43
+
44
+ init(settleMs: Int = 30, publishTimeoutMs: Int = 5000) {
45
+ mdns.publishTimeoutMs = publishTimeoutMs
46
+ mdns.makeBrowser = { [weak self] in
47
+ let browser = FakeBrowser()
48
+ self?.browsers.append(browser)
49
+ return browser
50
+ }
51
+ mdns.makeService = { [weak self] domain, type, name, port in
52
+ let s = port.map { FakeNetService(domain: domain, type: type, name: name, port: $0) }
53
+ ?? FakeNetService(domain: domain, type: type, name: name)
54
+ self?.services.append(s)
55
+ return s
56
+ }
57
+ }
58
+
59
+ /// Start a discovery on the fallback browser and record its outcome.
60
+ func discover(name: String? = nil, timeoutMs: Int = 2000, outcome: @escaping (Result<[[String: Any]], Error>) -> Void) {
61
+ mdns.discover(type: "_http._tcp.", name: name, timeoutMs: timeoutMs, useNW: false, completion: outcome)
62
+ }
63
+
64
+ func found(_ name: String, type: String = "_http._tcp.", domain: String = "local.") {
65
+ mdns.handleServiceFound(name: name, type: type, domain: domain)
66
+ }
67
+
68
+ func resolve(_ service: FakeNetService, host: String = "192.168.1.5") {
69
+ var addr = sockaddr_in()
70
+ addr.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
71
+ addr.sin_family = sa_family_t(AF_INET)
72
+ inet_pton(AF_INET, host, &addr.sin_addr)
73
+ service.fakeAddresses = [withUnsafeBytes(of: &addr) { Data($0) }]
74
+ mdns.netServiceDidResolveAddress(service)
75
+ }
76
+ }
77
+
78
+ // MARK: - Tests
79
+
80
+ final class MDNSTests: XCTestCase {
81
+
82
+ private func pause(_ seconds: TimeInterval) {
83
+ let e = expectation(description: "delay")
84
+ DispatchQueue.main.asyncAfter(deadline: .now() + seconds) { e.fulfill() }
85
+ wait(for: [e], timeout: seconds + 2)
86
+ }
87
+
88
+ func testBrowsingContinuesForTheEntireTimeoutAfterAnIdleResult() {
89
+ let rig = Rig()
90
+ var outcome: Result<[[String: Any]], Error>?
91
+ let done = expectation(description: "full timeout")
92
+ rig.discover(timeoutMs: 300) { outcome = $0; done.fulfill() }
93
+ rig.found("First")
94
+ rig.resolve(rig.services[0])
95
+ pause(0.15)
96
+ XCTAssertNil(outcome)
97
+ rig.found("Late")
98
+ rig.resolve(rig.services[1])
99
+ wait(for: [done], timeout: 1)
100
+ guard case .success(let services)? = outcome else { return XCTFail("expected success") }
101
+ XCTAssertEqual(services.count, 2)
102
+ }
103
+ func testNameMatchingTreatsDotsAndHyphensEqually() {
104
+ let rig = Rig()
105
+ var outcome: Result<[[String: Any]], Error>?
106
+ rig.discover(name: "My-Device") { outcome = $0 }
107
+ rig.found("My.Device")
108
+ rig.resolve(rig.services[0])
109
+ guard case .success(let services)? = outcome else { return XCTFail("expected early match") }
110
+ XCTAssertEqual(services.first?["name"] as? String, "My.Device")
111
+ }
112
+ func testValidationEnforcesDnsAndUtf8LimitsWithoutStoppingPublisher() {
113
+ let rig = Rig()
114
+ rig.mdns.broadcast(type: "_http._tcp.", name: "Good", port: 80, txt: nil) { _ in }
115
+ for type in ["_123._tcp.", "_ab--cd._tcp.", "_abcdefghijklmnop._tcp.", "http"] {
116
+ XCTAssertThrowsError(try MDNS.validatedType(type))
117
+ }
118
+ XCTAssertEqual(try? MDNS.validatedType(" _HTTP._TCP "), "_http._tcp.")
119
+ XCTAssertThrowsError(try MDNS.validatedName(String(repeating: "é", count: 32)))
120
+ for txt in [["": "x"], ["a=b": "x"], ["a": String(repeating: "é", count: 127)], ["A": "", "a": ""], Dictionary(uniqueKeysWithValues: (0..<6).map { ("k\($0)", String(repeating: "x", count: 250)) })] {
121
+ rig.mdns.broadcast(type: "_http._tcp.", name: "Bad", port: 81, txt: txt) { result in
122
+ guard case .failure = result else { return XCTFail("invalid TXT accepted") }
123
+ }
124
+ XCTAssertEqual(rig.services[0].stopCalls, 0)
125
+ }
126
+ }
127
+ // MARK: Discovery
128
+
129
+ func testSlowResolveDoesNotFinishBeforeTheServiceIsResolved() {
130
+ let rig = Rig(settleMs: 30)
131
+ var outcome: Result<[[String: Any]], Error>?
132
+ let done = expectation(description: "discovery")
133
+ rig.discover { outcome = $0; done.fulfill() }
134
+
135
+ rig.found("A")
136
+ pause(0.15) // several debounce windows pass while the resolver is still outstanding
137
+ XCTAssertNil(outcome, "must not settle while a resolver is pending")
138
+
139
+ rig.resolve(rig.services[0])
140
+ wait(for: [done], timeout: 2)
141
+
142
+ guard case .success(let services)? = outcome else { return XCTFail("expected success") }
143
+ XCTAssertEqual(services.count, 1)
144
+ XCTAssertEqual(services.first?["name"] as? String, "A")
145
+ XCTAssertEqual(services.first?["port"] as? Int, 8080)
146
+ XCTAssertEqual(services.first?["hosts"] as? [String], ["192.168.1.5"])
147
+ }
148
+
149
+ func testRepeatedBrowseResultsDoNotSpawnDuplicateResolvers() {
150
+ let rig = Rig()
151
+ rig.discover { _ in }
152
+
153
+ // NWBrowser redelivers its full result set on every change, with un-normalized type/domain.
154
+ rig.found("A", type: "_http._tcp", domain: "local")
155
+ rig.found("A", type: "_http._tcp", domain: "local")
156
+ rig.found("A", type: "_http._tcp.", domain: "local.")
157
+ rig.found("A", type: "_http._tcp", domain: "")
158
+
159
+ XCTAssertEqual(rig.services.count, 1)
160
+ XCTAssertEqual(rig.services[0].resolveCalls, 1)
161
+ }
162
+
163
+ func testResolvedServiceWithoutAddressesKeepsWaitingForAddresses() {
164
+ let rig = Rig()
165
+ let done = expectation(description: "discovery")
166
+ rig.discover(name: "A", timeoutMs: 80) { outcome in
167
+ guard case .success(let services) = outcome else {
168
+ done.fulfill()
169
+ return XCTFail("expected success")
170
+ }
171
+ XCTAssertTrue(services.isEmpty)
172
+ done.fulfill()
173
+ }
174
+ rig.found("A")
175
+ rig.mdns.netServiceDidResolveAddress(rig.services[0])
176
+ wait(for: [done], timeout: 2)
177
+ }
178
+
179
+ func testTargetNameFiltersCandidatesAndFinishesEarly() {
180
+ let rig = Rig()
181
+ var outcome: Result<[[String: Any]], Error>?
182
+ let done = expectation(description: "discovery")
183
+ rig.discover(name: "Foo", timeoutMs: 10_000) { outcome = $0; done.fulfill() }
184
+
185
+ rig.found("Other")
186
+ XCTAssertEqual(rig.services.count, 0, "non-matching candidates must not be resolved")
187
+
188
+ rig.found("Foo (2)")
189
+ XCTAssertEqual(rig.services.count, 1)
190
+ rig.resolve(rig.services[0])
191
+ wait(for: [done], timeout: 1) // early exit, long before the 10s hard timeout
192
+
193
+ guard case .success(let services)? = outcome else { return XCTFail("expected success") }
194
+ XCTAssertEqual(services.compactMap { $0["name"] as? String }, ["Foo (2)"])
195
+ }
196
+
197
+ func testHardTimeoutReturnsOnlyResolvedServices() {
198
+ let rig = Rig()
199
+ var outcome: Result<[[String: Any]], Error>?
200
+ let done = expectation(description: "discovery")
201
+ rig.discover(timeoutMs: 80) { outcome = $0; done.fulfill() }
202
+
203
+ rig.found("NeverResolves")
204
+ wait(for: [done], timeout: 2)
205
+
206
+ guard case .success(let services)? = outcome else { return XCTFail("expected success") }
207
+ XCTAssertTrue(services.isEmpty)
208
+ }
209
+
210
+ func testUnresolvableServiceDoesNotBlockSettling() {
211
+ let rig = Rig(settleMs: 30)
212
+ var outcome: Result<[[String: Any]], Error>?
213
+ let done = expectation(description: "discovery")
214
+ rig.discover { outcome = $0; done.fulfill() }
215
+
216
+ rig.found("Bad")
217
+ rig.found("Good")
218
+ rig.mdns.netService(rig.services[0], didNotResolve: [:])
219
+ rig.resolve(rig.services[1])
220
+ wait(for: [done], timeout: 2)
221
+
222
+ guard case .success(let services)? = outcome else { return XCTFail("expected success") }
223
+ XCTAssertEqual(services.compactMap { $0["name"] as? String }, ["Good"])
224
+ }
225
+
226
+ func testNewDiscoveryReplacesTheOldOneAndIgnoresItsStaleCallbacks() {
227
+ let rig = Rig()
228
+ var first: Result<[[String: Any]], Error>?
229
+ var second: Result<[[String: Any]], Error>?
230
+ rig.discover { first = $0 }
231
+ rig.found("Old")
232
+ let stale = rig.services[0]
233
+
234
+ rig.discover { second = $0 }
235
+ guard case .failure(let err)? = first else { return XCTFail("first discovery must fail as replaced") }
236
+ XCTAssertTrue(err.localizedDescription.contains("replaced"))
237
+
238
+ rig.resolve(stale) // callback from the previous session
239
+ pause(0.1)
240
+ XCTAssertNil(second, "a stale resolver must not complete the new discovery")
241
+ }
242
+
243
+ // MARK: Publishing
244
+
245
+ func testPublishSuccessReportsServiceName() {
246
+ let rig = Rig()
247
+ var outcome: Result<String, Error>?
248
+ rig.mdns.broadcast(type: "_http._tcp.", name: "Svc", port: 8080, txt: ["k": "v"]) { outcome = $0 }
249
+
250
+ let svc = rig.services[0]
251
+ XCTAssertEqual(svc.publishCalls, 1)
252
+ rig.mdns.netServiceDidPublish(svc)
253
+
254
+ guard case .success(let name)? = outcome else { return XCTFail("expected success") }
255
+ XCTAssertEqual(name, "Svc")
256
+ }
257
+
258
+ func testPublishTimeoutFailsAndStopsThePublisher() {
259
+ let rig = Rig(publishTimeoutMs: 50)
260
+ let done = expectation(description: "publish")
261
+ var outcome: Result<String, Error>?
262
+ rig.mdns.broadcast(type: "_http._tcp.", name: "Svc", port: 8080, txt: nil) { outcome = $0; done.fulfill() }
263
+ wait(for: [done], timeout: 2)
264
+
265
+ guard case .failure(let err)? = outcome else { return XCTFail("expected failure") }
266
+ XCTAssertTrue(err.localizedDescription.contains("Timed out"))
267
+ XCTAssertEqual(rig.services[0].stopCalls, 1)
268
+ }
269
+
270
+ func testNewBroadcastFailsThePreviousPendingOne() {
271
+ let rig = Rig()
272
+ var first: Result<String, Error>?
273
+ var second: Result<String, Error>?
274
+ rig.mdns.broadcast(type: "_http._tcp.", name: "A", port: 1, txt: nil) { first = $0 }
275
+ rig.mdns.broadcast(type: "_http._tcp.", name: "B", port: 2, txt: nil) { second = $0 }
276
+
277
+ guard case .failure(let err)? = first else { return XCTFail("first broadcast must fail as replaced") }
278
+ XCTAssertTrue(err.localizedDescription.contains("Replaced"))
279
+ XCTAssertNil(second)
280
+ XCTAssertEqual(rig.services[0].stopCalls, 1)
281
+
282
+ rig.mdns.netServiceDidPublish(rig.services[0]) // stale publisher: ignored
283
+ XCTAssertNil(second)
284
+ rig.mdns.netServiceDidPublish(rig.services[1])
285
+ guard case .success(let name)? = second else { return XCTFail("expected success") }
286
+ XCTAssertEqual(name, "B")
287
+ }
288
+
289
+ func testDidNotPublishReportsErrorAndAllowsRepublishing() {
290
+ let rig = Rig()
291
+ var first: Result<String, Error>?
292
+ rig.mdns.broadcast(type: "_http._tcp.", name: "A", port: 1, txt: nil) { first = $0 }
293
+ rig.mdns.netService(rig.services[0], didNotPublish: [NetService.errorCode: NSNumber(value: -72001)])
294
+
295
+ guard case .failure(let err)? = first else { return XCTFail("expected failure") }
296
+ XCTAssertEqual((err as NSError).code, -72001)
297
+
298
+ var second: Result<String, Error>?
299
+ rig.mdns.broadcast(type: "_http._tcp.", name: "A", port: 1, txt: nil) { second = $0 }
300
+ rig.mdns.netServiceDidPublish(rig.services[1])
301
+ guard case .success? = second else { return XCTFail("expected success") }
302
+ }
303
+
304
+ func testStopBroadcastIsIdempotentAndFailsAPendingPublish() throws {
305
+ let rig = Rig()
306
+ var outcome: Result<String, Error>?
307
+ rig.mdns.broadcast(type: "_http._tcp.", name: "A", port: 1, txt: nil) { outcome = $0 }
308
+
309
+ try rig.mdns.stopBroadcast()
310
+ try rig.mdns.stopBroadcast()
311
+
312
+ guard case .failure? = outcome else { return XCTFail("pending publish must be failed by stop") }
313
+ XCTAssertEqual(rig.services[0].stopCalls, 1)
314
+ }
315
+
316
+ func testInvalidPortFailsImmediately() {
317
+ let rig = Rig()
318
+ for port in [0, -1, 65536] {
319
+ var outcome: Result<String, Error>?
320
+ rig.mdns.broadcast(type: "_http._tcp.", name: "A", port: port, txt: nil) { outcome = $0 }
321
+ guard case .failure? = outcome else { return XCTFail("port \(port) must be rejected") }
322
+ }
323
+ XCTAssertTrue(rig.services.isEmpty)
324
+ }
325
+
326
+ // MARK: Helpers
327
+
328
+ func testParseHostsHandlesIPv4AndIPv6() {
329
+ var v4 = sockaddr_in()
330
+ v4.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
331
+ v4.sin_family = sa_family_t(AF_INET)
332
+ inet_pton(AF_INET, "10.1.2.3", &v4.sin_addr)
333
+
334
+ var v6 = sockaddr_in6()
335
+ v6.sin6_len = UInt8(MemoryLayout<sockaddr_in6>.size)
336
+ v6.sin6_family = sa_family_t(AF_INET6)
337
+ inet_pton(AF_INET6, "fe80::1", &v6.sin6_addr)
338
+
339
+ let hosts = parseHosts([withUnsafeBytes(of: &v6) { Data($0) }, withUnsafeBytes(of: &v4) { Data($0) }])
340
+ XCTAssertEqual(hosts, ["10.1.2.3", "fe80::1"])
341
+ XCTAssertEqual(parseHosts(nil), [])
342
+ XCTAssertEqual(parseHosts([]), [])
343
+
344
+ v6.sin6_scope_id = 7
345
+ XCTAssertEqual(parseHosts([withUnsafeBytes(of: &v6) { Data($0) }]), ["fe80::1%7"])
346
+ }
347
+
348
+ func testLocalNetworkDeniedDetection() {
349
+ XCTAssertTrue(MDNS.isLocalNetworkDenied(.dns(-65570)))
350
+ XCTAssertFalse(MDNS.isLocalNetworkDenied(.dns(-65537)))
351
+ XCTAssertFalse(MDNS.isLocalNetworkDenied(.posix(.ECONNREFUSED)))
352
+ }
353
+ }
354
+
355
+ // MARK: - Continuous discovery regressions
356
+
357
+ extension MDNSTests {
358
+ private func watch(_ rig: Rig, _ probe: WatchProbe, types: [String] = ["_http._tcp."]) {
359
+ rig.mdns.watchDelegate = probe
360
+ rig.mdns.watchServices(types: types, useNW: false) { result in
361
+ guard case .success? = Optional(result) else { return XCTFail("watch failed: \(result)") }
362
+ }
363
+ }
364
+
365
+ func testWatchDeduplicatesTypesAndReturnsFullLostSnapshot() {
366
+ let rig = Rig()
367
+ let probe = WatchProbe()
368
+ watch(rig, probe, types: [" _http._tcp", "_http._tcp.", "_ipp._tcp."])
369
+ XCTAssertEqual(rig.browsers.count, 2)
370
+ let browser = rig.browsers[0]
371
+ let found = FakeNetService(domain: "local.", type: "_http._tcp.", name: "A")
372
+ rig.mdns.netServiceBrowser(browser, didFind: found, moreComing: false)
373
+ rig.mdns.netServiceBrowser(browser, didFind: found, moreComing: false)
374
+ XCTAssertEqual(rig.services.count, 1)
375
+ rig.resolve(rig.services[0])
376
+ XCTAssertEqual(probe.found.count, 1)
377
+ XCTAssertEqual(probe.found[0]["hosts"] as? [String], ["192.168.1.5"])
378
+ rig.mdns.netServiceBrowser(browser, didRemove: found, moreComing: false)
379
+ XCTAssertEqual(probe.lost.count, 1)
380
+ XCTAssertEqual(probe.lost[0] as NSDictionary, probe.found[0] as NSDictionary)
381
+ rig.mdns.stopWatching()
382
+ XCTAssertTrue(rig.browsers.allSatisfy { $0.stopCalls == 1 })
383
+ }
384
+
385
+ func testWatchLossAndStopIgnoreLateResolveAndBrowserCallbacks() {
386
+ let rig = Rig()
387
+ let probe = WatchProbe()
388
+ watch(rig, probe)
389
+ rig.mdns.handleWatchFound(name: "A", type: "_http._tcp", domain: "local")
390
+ let oldResolver = rig.services[0]
391
+ rig.mdns.handleWatchLost(name: "A", type: "_http._tcp.", domain: "local.")
392
+ rig.resolve(oldResolver)
393
+ XCTAssertTrue(probe.found.isEmpty)
394
+ XCTAssertTrue(probe.lost.isEmpty)
395
+ rig.mdns.handleWatchFound(name: "B", type: "_http._tcp.", domain: "local.")
396
+ let oldBrowser = rig.browsers[0]
397
+ rig.mdns.stopWatching()
398
+ watch(rig, probe)
399
+ rig.resolve(rig.services[1])
400
+ rig.mdns.netServiceBrowser(oldBrowser, didFind: oldResolver, moreComing: false)
401
+ rig.mdns.netServiceBrowser(oldBrowser, didNotSearch: [:])
402
+ XCTAssertTrue(probe.found.isEmpty)
403
+ XCTAssertTrue(probe.errors.isEmpty)
404
+ XCTAssertEqual(rig.services.count, 2)
405
+ rig.mdns.stopWatching()
406
+ }
407
+
408
+ func testWatchRetriesFailedResolveAndAllowsRediscoveryAfterExhaustion() {
409
+ let rig = Rig()
410
+ let probe = WatchProbe()
411
+ watch(rig, probe)
412
+ rig.mdns.handleWatchFound(name: "A", type: "_http._tcp.", domain: "local.")
413
+ for attempt in 0..<4 {
414
+ rig.mdns.netService(rig.services[attempt], didNotResolve: [:])
415
+ if attempt < 3 { pause(0.2) }
416
+ }
417
+ XCTAssertEqual(probe.errors.count, 1)
418
+ XCTAssertEqual(probe.errors[0].1, true)
419
+ rig.mdns.handleWatchFound(name: "A", type: "_http._tcp.", domain: "local.")
420
+ rig.resolve(rig.services[4])
421
+ XCTAssertEqual(probe.found.count, 1)
422
+ rig.mdns.stopWatching()
423
+ }
424
+
425
+ func testWatchRetriesWhenResolverNeverCallsBackAndCancelsRetryOnStop() {
426
+ let rig = Rig()
427
+ rig.mdns.resolveTimeoutSeconds = 0.03
428
+ watch(rig, WatchProbe())
429
+ rig.mdns.handleWatchFound(name: "A", type: "_http._tcp.", domain: "local.")
430
+ pause(0.22)
431
+ XCTAssertGreaterThanOrEqual(rig.services.count, 2)
432
+ rig.mdns.stopWatching()
433
+ let count = rig.services.count
434
+ pause(0.25)
435
+ XCTAssertEqual(rig.services.count, count)
436
+ }
437
+
438
+ func testWatchAndDiscoverStayIndependentAndInvalidReplacementKeepsWatch() {
439
+ let rig = Rig()
440
+ let probe = WatchProbe()
441
+ watch(rig, probe)
442
+ var discovery: Result<[[String: Any]], Error>?
443
+ rig.discover(name: "Once") { discovery = $0 }
444
+ rig.found("Once")
445
+ rig.mdns.handleWatchFound(name: "Watched", type: "_http._tcp.", domain: "local.")
446
+ rig.resolve(rig.services[1])
447
+ XCTAssertEqual(probe.found.count, 1)
448
+ XCTAssertNil(discovery)
449
+ for types in [[], [" "], ["http"]] {
450
+ rig.mdns.watchServices(types: types, useNW: false) { result in
451
+ guard case .failure = result else { return XCTFail("invalid watch accepted") }
452
+ }
453
+ }
454
+ XCTAssertEqual(rig.browsers[0].stopCalls, 0)
455
+ rig.mdns.stopWatching()
456
+ rig.resolve(rig.services[0])
457
+ pause(0.05)
458
+ guard case .success(let services)? = discovery else { return XCTFail("discovery failed") }
459
+ XCTAssertEqual(services.compactMap { $0["name"] as? String }, ["Once"])
460
+ }
461
+
462
+ func testNetworkWatchSnapshotsDeduplicateAndLossCancelsPendingResolution() {
463
+ let rig = Rig()
464
+ let probe = WatchProbe()
465
+ watch(rig, probe)
466
+ let values = [BrowseService(name: "A", type: "_http._tcp", domain: "local")]
467
+ rig.mdns.handleWatchResults(values, type: "_http._tcp.")
468
+ rig.mdns.handleWatchResults(values, type: "_http._tcp.")
469
+ XCTAssertEqual(rig.services.count, 1)
470
+ rig.mdns.handleWatchResults([], type: "_http._tcp.")
471
+ rig.resolve(rig.services[0])
472
+ XCTAssertTrue(probe.found.isEmpty)
473
+ XCTAssertTrue(probe.lost.isEmpty)
474
+ rig.mdns.stopWatching()
475
+ }
476
+
477
+ func testDeniedNetworkPermissionStopsWatchAndReportsError() {
478
+ let rig = Rig()
479
+ let probe = WatchProbe()
480
+ watch(rig, probe)
481
+ rig.mdns.handleWatchState(.waiting(.dns(-65570)), type: "_http._tcp.")
482
+ XCTAssertEqual(probe.errors.count, 1)
483
+ XCTAssertFalse(probe.errors[0].1)
484
+ XCTAssertTrue(probe.errors[0].0.contains("denied"))
485
+ XCTAssertEqual(rig.browsers[0].stopCalls, 1)
486
+ rig.mdns.handleWatchFound(name: "Late", type: "_http._tcp.", domain: "local.")
487
+ XCTAssertTrue(rig.services.isEmpty)
488
+ }
489
+
490
+ }
package/package.json CHANGED
@@ -1,16 +1,43 @@
1
1
  {
2
2
  "name": "@devioarts/capacitor-mdns",
3
- "version": "0.0.3",
3
+ "version": "0.2.0",
4
4
  "description": "mDNS plugin for CapacitorJS",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
7
7
  "types": "dist/esm/index.d.ts",
8
8
  "unpkg": "dist/plugin.js",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/esm/index.d.ts",
12
+ "import": "./dist/esm/index.js",
13
+ "require": "./dist/plugin.cjs.js",
14
+ "default": "./dist/esm/index.js"
15
+ },
16
+ "./electron": {
17
+ "types": "./electron/types-public/index.d.ts",
18
+ "require": "./electron/dist/plugin.cjs.js",
19
+ "default": "./electron/dist/plugin.cjs.js"
20
+ },
21
+ "./electron/settings": {
22
+ "types": "./electron/types-public/index.d.ts",
23
+ "require": "./electron/dist/plugin-settings.js",
24
+ "default": "./electron/dist/plugin-settings.js"
25
+ },
26
+ "./electron/mdns": {
27
+ "types": "./electron/types-public/index.d.ts",
28
+ "require": "./electron/dist/plugin.cjs.js",
29
+ "default": "./electron/dist/plugin.cjs.js"
30
+ },
31
+ "./electron/mdns-bridge.cjs": "./electron/mdns-bridge.cjs",
32
+ "./package.json": "./package.json"
33
+ },
9
34
  "files": [
35
+ "CHANGELOG.md",
10
36
  "android/src/main/",
11
37
  "android/build.gradle",
12
38
  "dist/",
13
39
  "electron/",
40
+ "docs/*.md",
14
41
  "ios/Sources",
15
42
  "ios/Tests",
16
43
  "Package.swift",
@@ -45,38 +72,46 @@
45
72
  "verify:web": "npm run build",
46
73
  "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
47
74
  "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
48
- "eslint": "eslint . --ext ts",
75
+ "eslint": "eslint src electron --ext ts",
49
76
  "prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
50
77
  "swiftlint": "node-swiftlint",
51
78
  "docgen": "docgen --api mDNSPlugin --output-readme README.md --output-json dist/docs.json",
52
- "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
53
- "clean": "rimraf ./dist",
79
+ "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs && npm run build-electron",
80
+ "build-electron": "tsc --project electron/tsconfig.json && rollup -c electron/rollup.config.mjs && rimraf ./electron/build",
81
+ "clean": "rimraf ./dist ./electron/dist ./electron/build",
54
82
  "watch": "tsc --watch",
55
- "test": "echo \"No test specified\"",
83
+ "test": "npm run test:e2e",
84
+ "test:e2e": "rimraf .tmp/e2e-build && tsc -p tsconfig.e2e.json && node --test test/e2e/*.e2e.cjs",
56
85
  "prepublishOnly": "npm run build",
57
86
  "release": "npm publish --access public"
58
87
  },
59
88
  "devDependencies": {
60
- "@capacitor/android": "^8.4.0",
61
- "@capacitor/core": "^8.4.0",
62
- "@capacitor/docgen": "^0.3.1",
63
- "@capacitor/ios": "^8.4.0",
89
+ "@capacitor/android": "^8.0.0",
90
+ "@capacitor/core": "^8.0.0",
91
+ "@capacitor/docgen": "^0.3.0",
92
+ "@capacitor/ios": "^8.0.0",
64
93
  "@ionic/prettier-config": "^4.0.0",
65
94
  "@ionic/swiftlint-config": "^2.0.0",
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",
95
+ "@typescript-eslint/eslint-plugin": "^8.63.0",
96
+ "@typescript-eslint/parser": "^8.63.0",
97
+ "bonjour-service": "~1.4.0",
98
+ "electron": "^43.0.0",
99
+ "eslint": "^10.6.0",
100
+ "prettier": "^3.9.0",
101
+ "prettier-plugin-java": "^2.10.0",
102
+ "rimraf": "^6.1.0",
103
+ "rollup": "^4.62.0",
75
104
  "swiftlint": "^2.0.0",
76
- "typescript": "^6.0.3"
105
+ "typescript": "^6.0.0"
77
106
  },
78
107
  "peerDependencies": {
79
- "@capacitor/core": ">=8.0.0"
108
+ "@capacitor/core": "^8.0.0",
109
+ "bonjour-service": "~1.4.0"
110
+ },
111
+ "peerDependenciesMeta": {
112
+ "bonjour-service": {
113
+ "optional": true
114
+ }
80
115
  },
81
116
  "prettier": "@ionic/prettier-config",
82
117
  "capacitor": {
@@ -1,15 +0,0 @@
1
- import XCTest
2
- @testable import mDNSPlugin
3
-
4
- class mDNSTests: XCTestCase {
5
- func testEcho() {
6
- // This is an example of a functional test case for a plugin.
7
- // Use XCTAssert and related functions to verify your tests produce the correct results.
8
-
9
- let implementation = mDNS()
10
- let value = "Hello, World!"
11
- let result = implementation.echo(value)
12
-
13
- XCTAssertEqual(value, result)
14
- }
15
- }