@annadata/capacitor-mqtt-quic 0.1.9 → 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.
Files changed (32) hide show
  1. package/README.md +90 -9
  2. package/android/install/wolfssl-android/arm64-v8a/lib/libwolfssl.a +0 -0
  3. package/android/install/wolfssl-android/armeabi-v7a/lib/libwolfssl.a +0 -0
  4. package/android/install/wolfssl-android/x86_64/lib/libwolfssl.a +0 -0
  5. package/android/src/main/kotlin/ai/annadata/mqttquic/MqttQuicPlugin.kt +17 -0
  6. package/android/src/main/kotlin/ai/annadata/mqttquic/client/MQTTClient.kt +40 -0
  7. package/dist/esm/definitions.d.ts +8 -0
  8. package/dist/esm/definitions.d.ts.map +1 -1
  9. package/dist/esm/web.d.ts +5 -1
  10. package/dist/esm/web.d.ts.map +1 -1
  11. package/dist/esm/web.js +6 -0
  12. package/dist/esm/web.js.map +1 -1
  13. package/dist/plugin.cjs.js +6 -0
  14. package/dist/plugin.cjs.js.map +1 -1
  15. package/dist/plugin.js +6 -0
  16. package/dist/plugin.js.map +1 -1
  17. package/ios/Sources/MqttQuicPlugin/Client/MQTTClient.swift +66 -0
  18. package/ios/Sources/MqttQuicPlugin/MqttQuicPlugin.swift +26 -8
  19. package/ios/Sources/MqttQuicPlugin/Transport/QUICStreamAdapter.swift +2 -7
  20. package/ios/libs/libnghttp3.a +0 -0
  21. package/ios/libs/libngtcp2.a +0 -0
  22. package/ios/libs/libngtcp2_crypto_wolfssl.a +0 -0
  23. package/ios/libs/libwolfssl.a +0 -0
  24. package/ios/libs-simulator/libnghttp3.a +0 -0
  25. package/ios/libs-simulator/libngtcp2.a +0 -0
  26. package/ios/libs-simulator/libngtcp2_crypto_wolfssl.a +0 -0
  27. package/ios/libs-simulator/libwolfssl.a +0 -0
  28. package/ios/libs-simulator-x86_64/libnghttp3.a +0 -0
  29. package/ios/libs-simulator-x86_64/libngtcp2.a +0 -0
  30. package/ios/libs-simulator-x86_64/libngtcp2_crypto_wolfssl.a +0 -0
  31. package/ios/libs-simulator-x86_64/libwolfssl.a +0 -0
  32. package/package.json +1 -1
@@ -38,12 +38,16 @@ public final class MQTTClient {
38
38
  private var keepaliveTask: Task<Void, Never>?
39
39
  private var nextPacketId: UInt16 = 1
40
40
  private var subscribedTopics: [String: (Data) -> Void] = [:]
41
+ /// Optional global callback for every incoming PUBLISH (topic, payload). Used by plugin to forward to JS. Matches Android.
42
+ var onPublish: ((String, Data) -> Void)?
41
43
  /// Per-connection Topic Alias map (alias -> topic name) for MQTT 5.0 incoming PUBLISH.
42
44
  private var topicAliasMap: [Int: String] = [:]
43
45
  /// Handoff from message loop to subscribe() so SUBACK is not consumed by the loop (avoids race and "insufficientData(SUBACK packet ID)").
44
46
  private var subackContinuation: CheckedContinuation<Data, Error>?
45
47
  /// Handoff from message loop to unsubscribe() for UNSUBACK.
46
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>?
47
51
  private let lock = NSLock()
48
52
 
49
53
  public init(protocolVersion: ProtocolVersion = .auto) {
@@ -343,6 +347,8 @@ public final class MQTTClient {
343
347
  subackContinuation = nil
344
348
  let unsubCont = unsubackContinuation
345
349
  unsubackContinuation = nil
350
+ let pingCont = pendingPingresp
351
+ pendingPingresp = nil
346
352
  let w = writer
347
353
  let version = activeProtocolVersion
348
354
  quicClient = nil
@@ -356,6 +362,7 @@ public final class MQTTClient {
356
362
  lock.unlock()
357
363
  subCont?.resume(throwing: MQTTProtocolError.insufficientData("disconnected"))
358
364
  unsubCont?.resume(throwing: MQTTProtocolError.insufficientData("disconnected"))
365
+ pingCont?.resume(throwing: MQTTProtocolError.insufficientData("disconnected"))
359
366
 
360
367
  if let w = w {
361
368
  let data: Data
@@ -401,6 +408,55 @@ public final class MQTTClient {
401
408
  return (fixed[0], rem, fixed)
402
409
  }
403
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
+
404
460
  /// Send PINGREQ at effectiveKeepalive interval so server sees activity and does not close (idle/keepalive). [MQTT-3.1.2-20]
405
461
  private func startKeepaliveLoop() {
406
462
  keepaliveTask?.cancel()
@@ -491,6 +547,12 @@ public final class MQTTClient {
491
547
  try await w.write(Data(pr))
492
548
  try await w.drain()
493
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()
494
556
  case MQTTMessageType.PUBLISH.rawValue:
495
557
  let qos = (msgType >> 1) & 0x03
496
558
  let topic: String
@@ -515,7 +577,9 @@ public final class MQTTClient {
515
577
 
516
578
  self.lock.lock()
517
579
  let cb = self.subscribedTopics[topic]
580
+ let globalCb = self.onPublish
518
581
  self.lock.unlock()
582
+ globalCb?(topic, payload)
519
583
  cb?(payload)
520
584
 
521
585
  if qos >= 1, let pid = packetId {
@@ -546,6 +610,8 @@ public final class MQTTClient {
546
610
  subackContinuation = nil
547
611
  unsubackContinuation?.resume(throwing: error)
548
612
  unsubackContinuation = nil
613
+ pendingPingresp?.resume(throwing: error)
614
+ pendingPingresp = nil
549
615
  state = .disconnected
550
616
  lock.unlock()
551
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
@@ -117,6 +131,17 @@ public class MqttQuicPlugin: CAPPlugin, CAPBridgedPlugin {
117
131
  break
118
132
  }
119
133
  client = MQTTClient(protocolVersion: protocolVersion)
134
+ // Forward every incoming PUBLISH to JS so addListener('message', ...) receives topic + payload (matches Android)
135
+ client.onPublish = { [weak self] topic, payload in
136
+ guard let self = self else { return }
137
+ let payloadStr: String = {
138
+ if let str = String(data: payload, encoding: .utf8) { return str }
139
+ return payload.base64EncodedString()
140
+ }()
141
+ DispatchQueue.main.async {
142
+ self.notifyListeners("message", data: ["topic": topic as String, "payload": payloadStr as String])
143
+ }
144
+ }
120
145
  try await client.connect(
121
146
  host: host,
122
147
  port: UInt16(port),
@@ -257,14 +282,7 @@ public class MqttQuicPlugin: CAPPlugin, CAPBridgedPlugin {
257
282
  Task {
258
283
  do {
259
284
  try await client.subscribe(topic: topic, qos: UInt8(min(qos, 2)), subscriptionIdentifier: subscriptionIdentifier)
260
- // Deliver incoming PUBLISH for this topic to JS (avoids message loop having no handler; prevents stuck state)
261
- client.onMessage(topic) { [weak self] payload in
262
- guard let self = self else { return }
263
- let str = String(data: payload, encoding: .utf8) ?? ""
264
- DispatchQueue.main.async {
265
- self.notifyListeners("message", data: ["topic": topic, "payload": str])
266
- }
267
- }
285
+ // Incoming PUBLISH delivered via onPublish set in connect(); no per-topic handler needed
268
286
  DispatchQueue.main.async { call.resolve(["success": true]) }
269
287
  } catch {
270
288
  DispatchQueue.main.async { call.reject("\(error)") }
@@ -19,21 +19,16 @@ public final class QUICStreamReader: MQTTStreamReaderProtocol {
19
19
  try await stream.read(maxBytes: maxBytes)
20
20
  }
21
21
 
22
+ /// Read exactly n bytes. Waits indefinitely for data (matches Android behavior).
23
+ /// Previously had a 300ms limit which caused message loop to throw and disconnect before PUBLISH packets arrived.
22
24
  public func readexactly(_ n: Int) async throws -> Data {
23
25
  var acc = Data()
24
- var emptyCount = 0
25
- let maxEmptyRetries = 60 // ~300ms total (60 * 5ms) so SUBACK can arrive
26
26
  while acc.count < n {
27
27
  let chunk = try await stream.read(maxBytes: n - acc.count)
28
28
  if chunk.isEmpty {
29
- emptyCount += 1
30
- if emptyCount >= maxEmptyRetries {
31
- throw MQTTProtocolError.insufficientData("readexactly")
32
- }
33
29
  try await Task.sleep(nanoseconds: 5_000_000) // 5ms
34
30
  continue
35
31
  }
36
- emptyCount = 0
37
32
  acc.append(chunk)
38
33
  }
39
34
  return acc
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.1.9",
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",