@annadata/capacitor-mqtt-quic 0.2.0 → 0.2.1

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.
@@ -46,6 +46,8 @@ public final class MQTTClient {
46
46
  private var subackContinuation: CheckedContinuation<Data, Error>?
47
47
  /// Handoff from message loop to unsubscribe() for UNSUBACK.
48
48
  private var unsubackContinuation: CheckedContinuation<Data, Error>?
49
+ /// Pending PINGRESP for sendMqttPing(). Message loop completes when PINGRESP is read.
50
+ private var pendingPingresp: CheckedContinuation<Void, Error>?
49
51
  private let lock = NSLock()
50
52
 
51
53
  public init(protocolVersion: ProtocolVersion = .auto) {
@@ -345,6 +347,8 @@ public final class MQTTClient {
345
347
  subackContinuation = nil
346
348
  let unsubCont = unsubackContinuation
347
349
  unsubackContinuation = nil
350
+ let pingCont = pendingPingresp
351
+ pendingPingresp = nil
348
352
  let w = writer
349
353
  let version = activeProtocolVersion
350
354
  quicClient = nil
@@ -358,6 +362,7 @@ public final class MQTTClient {
358
362
  lock.unlock()
359
363
  subCont?.resume(throwing: MQTTProtocolError.insufficientData("disconnected"))
360
364
  unsubCont?.resume(throwing: MQTTProtocolError.insufficientData("disconnected"))
365
+ pingCont?.resume(throwing: MQTTProtocolError.insufficientData("disconnected"))
361
366
 
362
367
  if let w = w {
363
368
  let data: Data
@@ -403,6 +408,55 @@ public final class MQTTClient {
403
408
  return (fixed[0], rem, fixed)
404
409
  }
405
410
 
411
+ /// Send MQTT PINGREQ and wait for PINGRESP. Resets server's idle timer. Returns true if PINGRESP received within timeout.
412
+ public func sendMqttPing(timeoutMs: UInt64 = 5000) async -> Bool {
413
+ guard case .connected = getState() else { return false }
414
+ lock.lock()
415
+ if pendingPingresp != nil {
416
+ lock.unlock()
417
+ return false
418
+ }
419
+ let w = writer
420
+ lock.unlock()
421
+ do {
422
+ try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
423
+ lock.lock()
424
+ pendingPingresp = cont
425
+ lock.unlock()
426
+ Task {
427
+ try? await Task.sleep(nanoseconds: timeoutMs * 1_000_000)
428
+ self.lock.lock()
429
+ if let c = self.pendingPingresp {
430
+ self.pendingPingresp = nil
431
+ c.resume(throwing: MQTTProtocolError.insufficientData("PINGRESP timeout"))
432
+ }
433
+ self.lock.unlock()
434
+ }
435
+ if let w = w {
436
+ Task {
437
+ do {
438
+ try await w.write(MQTTProtocol.buildPingreq())
439
+ try await w.drain()
440
+ } catch {
441
+ self.lock.lock()
442
+ if let c = self.pendingPingresp {
443
+ self.pendingPingresp = nil
444
+ c.resume(throwing: error)
445
+ }
446
+ self.lock.unlock()
447
+ }
448
+ }
449
+ }
450
+ }
451
+ return true
452
+ } catch {
453
+ lock.lock()
454
+ pendingPingresp = nil
455
+ lock.unlock()
456
+ return false
457
+ }
458
+ }
459
+
406
460
  /// Send PINGREQ at effectiveKeepalive interval so server sees activity and does not close (idle/keepalive). [MQTT-3.1.2-20]
407
461
  private func startKeepaliveLoop() {
408
462
  keepaliveTask?.cancel()
@@ -493,6 +547,12 @@ public final class MQTTClient {
493
547
  try await w.write(Data(pr))
494
548
  try await w.drain()
495
549
  }
550
+ case MQTTMessageType.PINGRESP.rawValue:
551
+ self.lock.lock()
552
+ let pingCont = self.pendingPingresp
553
+ self.pendingPingresp = nil
554
+ self.lock.unlock()
555
+ pingCont?.resume()
496
556
  case MQTTMessageType.PUBLISH.rawValue:
497
557
  let qos = (msgType >> 1) & 0x03
498
558
  let topic: String
@@ -550,6 +610,8 @@ public final class MQTTClient {
550
610
  subackContinuation = nil
551
611
  unsubackContinuation?.resume(throwing: error)
552
612
  unsubackContinuation = nil
613
+ pendingPingresp?.resume(throwing: error)
614
+ pendingPingresp = nil
553
615
  state = .disconnected
554
616
  lock.unlock()
555
617
  }
@@ -15,6 +15,7 @@ public class MqttQuicPlugin: CAPPlugin, CAPBridgedPlugin {
15
15
  public let jsName = "MqttQuic"
16
16
  public let pluginMethods: [CAPPluginMethod] = [
17
17
  CAPPluginMethod(name: "ping", returnType: CAPPluginReturnPromise),
18
+ CAPPluginMethod(name: "sendKeepalive", returnType: CAPPluginReturnPromise),
18
19
  CAPPluginMethod(name: "connect", returnType: CAPPluginReturnPromise),
19
20
  CAPPluginMethod(name: "disconnect", returnType: CAPPluginReturnPromise),
20
21
  CAPPluginMethod(name: "publish", returnType: CAPPluginReturnPromise),
@@ -27,6 +28,19 @@ public class MqttQuicPlugin: CAPPlugin, CAPBridgedPlugin {
27
28
 
28
29
  @objc override public func load() {}
29
30
 
31
+ @objc func sendKeepalive(_ call: CAPPluginCall) {
32
+ let timeoutMs = UInt64(call.getInt("timeoutMs") ?? 5000)
33
+ let clamped = min(max(timeoutMs, 1000), 15000)
34
+ Task {
35
+ do {
36
+ let ok = await client.sendMqttPing(timeoutMs: clamped)
37
+ DispatchQueue.main.async { call.resolve(["ok": ok]) }
38
+ } catch {
39
+ DispatchQueue.main.async { call.reject("\(error)") }
40
+ }
41
+ }
42
+ }
43
+
30
44
  @objc func ping(_ call: CAPPluginCall) {
31
45
  let host = call.getString("host") ?? ""
32
46
  let port = call.getInt("port") ?? 1884
@@ -120,14 +134,12 @@ public class MqttQuicPlugin: CAPPlugin, CAPBridgedPlugin {
120
134
  // Forward every incoming PUBLISH to JS so addListener('message', ...) receives topic + payload (matches Android)
121
135
  client.onPublish = { [weak self] topic, payload in
122
136
  guard let self = self else { return }
123
- let payloadStr: String
124
- if let str = String(data: payload, encoding: .utf8) {
125
- payloadStr = str
126
- } else {
127
- payloadStr = payload.base64EncodedString()
128
- }
137
+ let payloadStr: String = {
138
+ if let str = String(data: payload, encoding: .utf8) { return str }
139
+ return payload.base64EncodedString()
140
+ }()
129
141
  DispatchQueue.main.async {
130
- self.notifyListeners("message", data: ["topic": topic, "payload": payloadStr])
142
+ self.notifyListeners("message", data: ["topic": topic as String, "payload": payloadStr as String])
131
143
  }
132
144
  }
133
145
  try await client.connect(
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@annadata/capacitor-mqtt-quic",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "MQTT-over-QUIC client for Capacitor (iOS, Android, Web). Native: ngtcp2+WolfSSL; Web: MQTT over WebSocket (WSS), same API.",
6
6
  "main": "dist/plugin.cjs.js",