@annadata/capacitor-mqtt-quic 0.1.7 → 0.1.9

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 (83) hide show
  1. package/AnnadataCapacitorMqttQuic.podspec +2 -2
  2. package/Package.swift +59 -0
  3. package/README.md +57 -17
  4. package/android/NGTCP2_BUILD_INSTRUCTIONS.md +1 -1
  5. package/android/app/src/main/assets/capacitor.config.json +1 -1
  6. package/android/build-wolfssl.sh +8 -2
  7. package/android/build.gradle +4 -1
  8. package/android/install/nghttp3-android/arm64-v8a/lib/libnghttp3.a +0 -0
  9. package/android/install/nghttp3-android/arm64-v8a/lib/libnghttp3.so +0 -0
  10. package/android/install/nghttp3-android/arm64-v8a/lib/pkgconfig/libnghttp3.pc +4 -4
  11. package/android/install/nghttp3-android/armeabi-v7a/lib/libnghttp3.a +0 -0
  12. package/android/install/nghttp3-android/armeabi-v7a/lib/libnghttp3.so +0 -0
  13. package/android/install/nghttp3-android/armeabi-v7a/lib/pkgconfig/libnghttp3.pc +4 -4
  14. package/android/install/nghttp3-android/x86_64/lib/libnghttp3.a +0 -0
  15. package/android/install/nghttp3-android/x86_64/lib/libnghttp3.so +0 -0
  16. package/android/install/nghttp3-android/x86_64/lib/pkgconfig/libnghttp3.pc +4 -4
  17. package/android/install/ngtcp2-android/arm64-v8a/lib/libngtcp2.a +0 -0
  18. package/android/install/ngtcp2-android/arm64-v8a/lib/libngtcp2.so +0 -0
  19. package/android/install/ngtcp2-android/arm64-v8a/lib/libngtcp2_crypto_wolfssl.a +0 -0
  20. package/android/install/ngtcp2-android/arm64-v8a/lib/libngtcp2_crypto_wolfssl.so +0 -0
  21. package/android/install/ngtcp2-android/arm64-v8a/lib/pkgconfig/libngtcp2.pc +4 -4
  22. package/android/install/ngtcp2-android/arm64-v8a/lib/pkgconfig/libngtcp2_crypto_wolfssl.pc +4 -4
  23. package/android/install/ngtcp2-android/armeabi-v7a/lib/libngtcp2.a +0 -0
  24. package/android/install/ngtcp2-android/armeabi-v7a/lib/libngtcp2.so +0 -0
  25. package/android/install/ngtcp2-android/armeabi-v7a/lib/libngtcp2_crypto_wolfssl.a +0 -0
  26. package/android/install/ngtcp2-android/armeabi-v7a/lib/libngtcp2_crypto_wolfssl.so +0 -0
  27. package/android/install/ngtcp2-android/armeabi-v7a/lib/pkgconfig/libngtcp2.pc +4 -4
  28. package/android/install/ngtcp2-android/armeabi-v7a/lib/pkgconfig/libngtcp2_crypto_wolfssl.pc +4 -4
  29. package/android/install/ngtcp2-android/x86_64/lib/libngtcp2.a +0 -0
  30. package/android/install/ngtcp2-android/x86_64/lib/libngtcp2.so +0 -0
  31. package/android/install/ngtcp2-android/x86_64/lib/libngtcp2_crypto_wolfssl.a +0 -0
  32. package/android/install/ngtcp2-android/x86_64/lib/libngtcp2_crypto_wolfssl.so +0 -0
  33. package/android/install/ngtcp2-android/x86_64/lib/pkgconfig/libngtcp2.pc +4 -4
  34. package/android/install/ngtcp2-android/x86_64/lib/pkgconfig/libngtcp2_crypto_wolfssl.pc +4 -4
  35. package/android/install/wolfssl-android/arm64-v8a/bin/wolfssl-config +1 -1
  36. package/android/install/wolfssl-android/arm64-v8a/lib/libwolfssl.a +0 -0
  37. package/android/install/wolfssl-android/arm64-v8a/lib/libwolfssl.la +1 -1
  38. package/android/install/wolfssl-android/arm64-v8a/lib/pkgconfig/wolfssl.pc +1 -1
  39. package/android/install/wolfssl-android/armeabi-v7a/bin/wolfssl-config +1 -1
  40. package/android/install/wolfssl-android/armeabi-v7a/lib/libwolfssl.a +0 -0
  41. package/android/install/wolfssl-android/armeabi-v7a/lib/libwolfssl.la +1 -1
  42. package/android/install/wolfssl-android/armeabi-v7a/lib/pkgconfig/wolfssl.pc +1 -1
  43. package/android/install/wolfssl-android/x86_64/bin/wolfssl-config +1 -1
  44. package/android/install/wolfssl-android/x86_64/lib/libwolfssl.a +0 -0
  45. package/android/install/wolfssl-android/x86_64/lib/libwolfssl.la +1 -1
  46. package/android/install/wolfssl-android/x86_64/lib/pkgconfig/wolfssl.pc +1 -1
  47. package/android/src/main/cpp/CMakeLists.txt +19 -5
  48. package/android/src/main/cpp/ngtcp2_jni.cpp +119 -32
  49. package/android/src/main/kotlin/ai/annadata/mqttquic/MqttQuicPlugin.kt +60 -5
  50. package/android/src/main/kotlin/ai/annadata/mqttquic/client/MQTTClient.kt +233 -84
  51. package/android/src/main/kotlin/ai/annadata/mqttquic/mqtt/MQTTProtocol.kt +36 -5
  52. package/android/src/main/kotlin/ai/annadata/mqttquic/mqtt/MQTTTypes.kt +15 -15
  53. package/android/src/main/kotlin/ai/annadata/mqttquic/quic/NGTCP2Client.kt +26 -15
  54. package/android/src/main/kotlin/ai/annadata/mqttquic/quic/QuicClientStub.kt +1 -1
  55. package/android/src/main/kotlin/ai/annadata/mqttquic/quic/QuicTypes.kt +1 -1
  56. package/android/src/main/kotlin/ai/annadata/mqttquic/transport/QUICStreamAdapter.kt +80 -5
  57. package/android/src/main/kotlin/ai/annadata/mqttquic/transport/StreamTransport.kt +4 -0
  58. package/docs/IMPLEMENTATION_SUMMARY.md +1 -1
  59. package/docs/NGTCP2_IMPLEMENTATION_STATUS.md +1 -1
  60. package/docs/PRODUCTION_PUBLISH_STEPS.md +9 -3
  61. package/docs/diff-node_modules-vs-standalone-android-src.patch +1031 -0
  62. package/ios/{MqttQuicPlugin.podspec → AnnadataCapacitorMqttQuic.podspec} +4 -4
  63. package/ios/App/App/capacitor.config.json +1 -1
  64. package/ios/NGTCP2_BUILD_INSTRUCTIONS.md +3 -3
  65. package/ios/Package.swift +7 -8
  66. package/ios/Tests/MQTTProtocolTests.swift +1 -1
  67. package/ios/build-wolfssl.sh +8 -3
  68. package/ios/libs/libnghttp3.a +0 -0
  69. package/ios/libs/libngtcp2.a +0 -0
  70. package/ios/libs/libngtcp2_crypto_wolfssl.a +0 -0
  71. package/ios/libs/libwolfssl.a +0 -0
  72. package/ios/libs-simulator/libnghttp3.a +0 -0
  73. package/ios/libs-simulator/libngtcp2.a +0 -0
  74. package/ios/libs-simulator/libngtcp2_crypto_wolfssl.a +0 -0
  75. package/ios/libs-simulator/libwolfssl.a +0 -0
  76. package/ios/libs-simulator-x86_64/libnghttp3.a +0 -0
  77. package/ios/libs-simulator-x86_64/libngtcp2.a +0 -0
  78. package/ios/libs-simulator-x86_64/libngtcp2_crypto_wolfssl.a +0 -0
  79. package/ios/libs-simulator-x86_64/libwolfssl.a +0 -0
  80. package/package.json +5 -2
  81. package/ios/libs/MqttQuicLibs.xcframework/Info.plist +0 -44
  82. package/ios/libs/MqttQuicLibs.xcframework/ios-arm64/libmqttquic_native_device.a +0 -0
  83. package/ios/libs/MqttQuicLibs.xcframework/ios-arm64_x86_64-simulator/libmqttquic_native_simulator.a +0 -0
@@ -24,6 +24,7 @@
24
24
  #include <algorithm>
25
25
  #include <atomic>
26
26
  #include <chrono>
27
+ #include <cinttypes>
27
28
  #include <cstdarg>
28
29
  #include <cstdlib>
29
30
  #include <cstdio>
@@ -73,7 +74,11 @@ struct OutgoingChunk {
73
74
  class QuicClient {
74
75
  public:
75
76
  QuicClient(std::string host, uint16_t port)
76
- : host_(std::move(host)),
77
+ : QuicClient(std::move(host), "", port) {}
78
+
79
+ QuicClient(std::string host_for_tls, std::string connect_addr, uint16_t port)
80
+ : host_(std::move(host_for_tls)),
81
+ connect_addr_(connect_addr.empty() ? host_ : std::move(connect_addr)),
77
82
  port_(port),
78
83
  fd_(-1),
79
84
  ssl_ctx_(nullptr),
@@ -181,6 +186,9 @@ class QuicClient {
181
186
  buffer[i] = state.recv_buf.front();
182
187
  state.recv_buf.pop_front();
183
188
  }
189
+ if (n > 0) {
190
+ LOGI("read_stream stream_id=%" PRId64 " returning %zu bytes", (int64_t)stream_id, n);
191
+ }
184
192
  return (ssize_t)n;
185
193
  }
186
194
 
@@ -188,7 +196,7 @@ class QuicClient {
188
196
  if (!conn_) {
189
197
  return 0;
190
198
  }
191
- int rv = ngtcp2_conn_shutdown_stream_write(conn_, stream_id, 0);
199
+ int rv = ngtcp2_conn_shutdown_stream_write(conn_, 0, stream_id, 0);
192
200
  if (rv != 0) {
193
201
  setError(ngtcp2_strerror(rv));
194
202
  return -1;
@@ -223,6 +231,8 @@ class QuicClient {
223
231
  std::lock_guard<std::mutex> lock(stream_mutex_);
224
232
  StreamState &state = streams_[stream_id];
225
233
  state.recv_buf.insert(state.recv_buf.end(), data, data + datalen);
234
+ LOGI("recv stream data stream_id=%" PRId64 " len=%zu recv_buf_total=%zu",
235
+ (int64_t)stream_id, datalen, state.recv_buf.size());
226
236
  if (flags & NGTCP2_STREAM_DATA_FLAG_FIN) {
227
237
  state.fin_received = true;
228
238
  }
@@ -254,7 +264,8 @@ class QuicClient {
254
264
 
255
265
  char port_str[16];
256
266
  snprintf(port_str, sizeof(port_str), "%u", port_);
257
- int rv = getaddrinfo(host_.c_str(), port_str, &hints, &res);
267
+ const char *resolve_host = connect_addr_.empty() ? host_.c_str() : connect_addr_.c_str();
268
+ int rv = getaddrinfo(resolve_host, port_str, &hints, &res);
258
269
  if (rv != 0) {
259
270
  setError(gai_strerror(rv));
260
271
  return -1;
@@ -266,12 +277,19 @@ class QuicClient {
266
277
  if (fd == -1) {
267
278
  continue;
268
279
  }
269
- if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) {
280
+ if (::connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) {
270
281
  memcpy(&remote_addr_, rp->ai_addr, rp->ai_addrlen);
271
282
  remote_addrlen_ = (socklen_t)rp->ai_addrlen;
283
+ char buf[INET6_ADDRSTRLEN];
284
+ const void *src = (rp->ai_family == AF_INET)
285
+ ? (void *)&((struct sockaddr_in *)rp->ai_addr)->sin_addr
286
+ : (void *)&((struct sockaddr_in6 *)rp->ai_addr)->sin6_addr;
287
+ if (inet_ntop(rp->ai_family, src, buf, sizeof(buf))) {
288
+ resolved_address_ = buf;
289
+ }
272
290
  break;
273
291
  }
274
- close(fd);
292
+ ::close(fd);
275
293
  fd = -1;
276
294
  }
277
295
  freeaddrinfo(res);
@@ -284,7 +302,7 @@ class QuicClient {
284
302
  if (getsockname(fd, (struct sockaddr *)&local_addr_, &local_addrlen_) !=
285
303
  0) {
286
304
  setError("getsockname failed");
287
- close(fd);
305
+ ::close(fd);
288
306
  return -1;
289
307
  }
290
308
 
@@ -333,17 +351,21 @@ class QuicClient {
333
351
  bool ca_loaded = false;
334
352
  const char *ca_file = std::getenv("MQTT_QUIC_CA_FILE");
335
353
  const char *ca_path = std::getenv("MQTT_QUIC_CA_PATH");
336
- if ((ca_file && ca_file[0] != '\0') || (ca_path && ca_path[0] != '\0')) {
337
- if (wolfSSL_CTX_load_verify_locations(ssl_ctx_, ca_file, ca_path) != 1) {
354
+ const char *file_arg = (ca_file && ca_file[0] != '\0') ? ca_file : nullptr;
355
+ const char *path_arg = (ca_path && ca_path[0] != '\0') ? ca_path : nullptr;
356
+ if (file_arg || path_arg) {
357
+ if (wolfSSL_CTX_load_verify_locations(ssl_ctx_, file_arg, path_arg) == 1) {
358
+ ca_loaded = true;
359
+ } else {
338
360
  setError("Failed to load CA bundle from MQTT_QUIC_CA_FILE/CA_PATH");
339
361
  return -1;
340
362
  }
363
+ }
364
+ if (!ca_loaded && wolfSSL_CTX_set_default_verify_paths(ssl_ctx_) == 1) {
341
365
  ca_loaded = true;
342
366
  }
343
- if (!ca_loaded) {
344
- if (wolfSSL_CTX_set_default_verify_paths(ssl_ctx_) == 1) {
345
- ca_loaded = true;
346
- }
367
+ if (!ca_loaded && wolfSSL_CTX_load_system_CA_certs(ssl_ctx_) == 1) {
368
+ ca_loaded = true;
347
369
  }
348
370
  if (!ca_loaded) {
349
371
  setError("No CA bundle available for TLS verification");
@@ -647,30 +669,44 @@ class QuicClient {
647
669
  }
648
670
 
649
671
  void cleanup() {
650
- if (conn_) {
651
- ngtcp2_conn_del(conn_);
672
+ ngtcp2_conn *conn_to_del = nullptr;
673
+ void *ssl_to_free = nullptr;
674
+ void *ssl_ctx_to_free = nullptr;
675
+ int fd_to_close = -1;
676
+ int wake0 = -1, wake1 = -1;
677
+ {
678
+ std::lock_guard<std::mutex> lock(cleanup_mutex_);
679
+ conn_to_del = conn_;
652
680
  conn_ = nullptr;
653
- }
654
- if (ssl_) {
655
- wolfSSL_free(ssl_);
681
+ ssl_to_free = ssl_;
656
682
  ssl_ = nullptr;
657
- }
658
- if (ssl_ctx_) {
659
- wolfSSL_CTX_free(ssl_ctx_);
683
+ ssl_ctx_to_free = ssl_ctx_;
660
684
  ssl_ctx_ = nullptr;
661
- }
662
- if (fd_ != -1) {
663
- close(fd_);
685
+ fd_to_close = fd_;
664
686
  fd_ = -1;
665
- }
666
- if (wakeup_fds_[0] != -1) {
667
- close(wakeup_fds_[0]);
687
+ wake0 = wakeup_fds_[0];
688
+ wake1 = wakeup_fds_[1];
668
689
  wakeup_fds_[0] = -1;
669
- }
670
- if (wakeup_fds_[1] != -1) {
671
- close(wakeup_fds_[1]);
672
690
  wakeup_fds_[1] = -1;
673
691
  }
692
+ if (conn_to_del) {
693
+ ngtcp2_conn_del(conn_to_del);
694
+ }
695
+ if (ssl_to_free) {
696
+ wolfSSL_free(static_cast<WOLFSSL *>(ssl_to_free));
697
+ }
698
+ if (ssl_ctx_to_free) {
699
+ wolfSSL_CTX_free(static_cast<WOLFSSL_CTX *>(ssl_ctx_to_free));
700
+ }
701
+ if (fd_to_close != -1) {
702
+ ::close(fd_to_close);
703
+ }
704
+ if (wake0 != -1) {
705
+ ::close(wake0);
706
+ }
707
+ if (wake1 != -1) {
708
+ ::close(wake1);
709
+ }
674
710
  }
675
711
 
676
712
  void clearError() {
@@ -740,10 +776,11 @@ class QuicClient {
740
776
  return 0;
741
777
  }
742
778
 
743
- static int stream_close_cb(ngtcp2_conn *conn, int64_t stream_id,
744
- uint64_t app_error_code, void *user_data,
745
- void *stream_user_data) {
779
+ static int stream_close_cb(ngtcp2_conn *conn, uint32_t flags,
780
+ int64_t stream_id, uint64_t app_error_code,
781
+ void *user_data, void *stream_user_data) {
746
782
  (void)conn;
783
+ (void)flags;
747
784
  (void)app_error_code;
748
785
  (void)stream_user_data;
749
786
  auto *client = static_cast<QuicClient *>(user_data);
@@ -761,9 +798,14 @@ class QuicClient {
761
798
  return client->on_handshake_completed();
762
799
  }
763
800
 
801
+ public:
802
+ const std::string &resolved_address() const { return resolved_address_; }
803
+
764
804
  private:
765
805
  std::string host_;
806
+ std::string connect_addr_;
766
807
  uint16_t port_;
808
+ std::string resolved_address_;
767
809
 
768
810
  int fd_;
769
811
  struct sockaddr_storage remote_addr_;
@@ -795,6 +837,8 @@ class QuicClient {
795
837
 
796
838
  mutable std::mutex err_mutex_;
797
839
  std::string last_error_str_;
840
+
841
+ std::mutex cleanup_mutex_;
798
842
  };
799
843
 
800
844
  static std::map<jlong, std::unique_ptr<QuicClient>> connections;
@@ -822,6 +866,27 @@ Java_ai_annadata_mqttquic_quic_NGTCP2Client_nativeCreateConnection(
822
866
  return handle;
823
867
  }
824
868
 
869
+ JNIEXPORT jlong JNICALL
870
+ Java_ai_annadata_mqttquic_quic_NGTCP2Client_nativeCreateConnectionWithAddress(
871
+ JNIEnv *env, jobject thiz, jstring hostnameForTls, jstring connectAddress, jint port) {
872
+ const char *tls_str = env->GetStringUTFChars(hostnameForTls, nullptr);
873
+ const char *addr_str = env->GetStringUTFChars(connectAddress, nullptr);
874
+ if (!tls_str || !addr_str) {
875
+ if (tls_str) env->ReleaseStringUTFChars(hostnameForTls, tls_str);
876
+ return 0;
877
+ }
878
+ std::string host_for_tls(tls_str);
879
+ std::string connect_addr(addr_str);
880
+ env->ReleaseStringUTFChars(hostnameForTls, tls_str);
881
+ env->ReleaseStringUTFChars(connectAddress, addr_str);
882
+
883
+ auto client = std::make_unique<QuicClient>(host_for_tls, connect_addr, (uint16_t)port);
884
+ std::lock_guard<std::mutex> lock(connections_mutex);
885
+ jlong handle = next_handle++;
886
+ connections[handle] = std::move(client);
887
+ return handle;
888
+ }
889
+
825
890
  JNIEXPORT jint JNICALL
826
891
  Java_ai_annadata_mqttquic_quic_NGTCP2Client_nativeConnect(
827
892
  JNIEnv *env, jobject thiz, jlong connHandle) {
@@ -926,4 +991,26 @@ Java_ai_annadata_mqttquic_quic_NGTCP2Client_nativeGetLastError(
926
991
  return env->NewStringUTF(it->second->last_error());
927
992
  }
928
993
 
994
+ JNIEXPORT jstring JNICALL
995
+ Java_ai_annadata_mqttquic_quic_NGTCP2Client_nativeGetLastResolvedAddress(
996
+ JNIEnv *env, jobject thiz, jlong connHandle) {
997
+ std::lock_guard<std::mutex> lock(connections_mutex);
998
+ auto it = connections.find(connHandle);
999
+ if (it == connections.end()) {
1000
+ return nullptr;
1001
+ }
1002
+ const std::string &addr = it->second->resolved_address();
1003
+ if (addr.empty()) {
1004
+ return nullptr;
1005
+ }
1006
+ return env->NewStringUTF(addr.c_str());
1007
+ }
1008
+
1009
+ // Debug-build alias: Kotlin/AGP can mangle the method name to include the module suffix.
1010
+ JNIEXPORT jstring JNICALL
1011
+ Java_ai_annadata_mqttquic_quic_NGTCP2Client_nativeGetLastError_00024annadata_1capacitor_1mqtt_1quic_1debug__J(
1012
+ JNIEnv *env, jobject thiz, jlong connHandle) {
1013
+ return Java_ai_annadata_mqttquic_quic_NGTCP2Client_nativeGetLastError(env, thiz, connHandle);
1014
+ }
1015
+
929
1016
  } // extern "C"
@@ -13,10 +13,13 @@ import android.system.Os
13
13
  import android.util.Base64
14
14
  import kotlinx.coroutines.CoroutineScope
15
15
  import kotlinx.coroutines.Dispatchers
16
+ import kotlinx.coroutines.delay
16
17
  import kotlinx.coroutines.launch
18
+ import kotlinx.coroutines.withContext
17
19
  import java.nio.charset.StandardCharsets
18
20
  import java.io.File
19
21
  import java.io.IOException
22
+ import java.net.InetAddress
20
23
 
21
24
  /**
22
25
  * Capacitor plugin bridge. Phase 3: connect/publish/subscribe call into MQTTClient.
@@ -27,6 +30,29 @@ class MqttQuicPlugin : Plugin() {
27
30
  private var client = MQTTClient(MQTTClient.ProtocolVersion.AUTO)
28
31
  private val scope = CoroutineScope(Dispatchers.Main)
29
32
 
33
+ /** Last resolved IP per host (used when DNS fails on reconnect). */
34
+ @Volatile
35
+ private var lastResolvedHost: String? = null
36
+ @Volatile
37
+ private var lastResolvedIp: String? = null
38
+
39
+ /** Resolve hostname to IP for socket connect (avoids "No address" on reconnect). Returns null if resolution fails. */
40
+ private fun resolveHostToIp(host: String): String? {
41
+ return try {
42
+ InetAddress.getAllByName(host).firstOrNull()?.hostAddress?.also { ip ->
43
+ lastResolvedHost = host
44
+ lastResolvedIp = ip
45
+ }
46
+ } catch (e: Exception) {
47
+ null
48
+ }
49
+ }
50
+
51
+ /** Use cached IP for this host if fresh resolve failed (e.g. on reconnect). */
52
+ private fun resolveOrCachedIp(host: String): String? {
53
+ return resolveHostToIp(host) ?: if (host == lastResolvedHost) lastResolvedIp else null
54
+ }
55
+
30
56
  private fun bundledCaFilePath(): String? {
31
57
  return try {
32
58
  val assetName = "mqttquic_ca.pem"
@@ -100,9 +126,38 @@ class MqttQuicPlugin : Plugin() {
100
126
  notifyListeners("message", data)
101
127
  }
102
128
  }
103
- client.connect(host, port, clientId, username, password, cleanSession, keepalive, sessionExpiryInterval)
104
- call.resolve(JSObject().put("connected", true))
105
- notifyListeners("connected", JSObject().put("connected", true))
129
+ // Resolve host to IP on IO so native getaddrinfo gets an IP (avoids "No address associated with hostname" on reconnect)
130
+ val noAddressMsg = "No address associated with hostname"
131
+ var lastException: Exception? = null
132
+ for (attempt in 1..2) {
133
+ try {
134
+ withContext(Dispatchers.IO) {
135
+ val resolvedIp = resolveOrCachedIp(host)
136
+ client.connect(host, port, clientId, username, password, cleanSession ?: true, keepalive ?: 20, sessionExpiryInterval, connectAddress = resolvedIp)
137
+ }
138
+ // Cache resolved IP from native so reconnect can use it when Java DNS fails
139
+ client.getLastResolvedAddress()?.let { ip ->
140
+ lastResolvedHost = host
141
+ lastResolvedIp = ip
142
+ }
143
+ call.resolve(JSObject().put("connected", true))
144
+ notifyListeners("connected", JSObject().put("connected", true))
145
+ return@launch
146
+ } catch (e: Exception) {
147
+ // Cache resolved IP from native even on failure (e.g. CONNACK timeout) so reconnect can use it
148
+ client.getLastResolvedAddress()?.let { ip ->
149
+ lastResolvedHost = host
150
+ lastResolvedIp = ip
151
+ }
152
+ lastException = e
153
+ if (attempt == 1 && e.message?.contains(noAddressMsg, ignoreCase = true) == true) {
154
+ delay(2000L)
155
+ continue
156
+ }
157
+ break
158
+ }
159
+ }
160
+ call.reject(lastException?.message ?: "Connection failed")
106
161
  } catch (e: Exception) {
107
162
  call.reject(e.message ?: "Connection failed")
108
163
  }
@@ -199,7 +254,7 @@ class MqttQuicPlugin : Plugin() {
199
254
  val properties = mutableMapOf<Int, Any>()
200
255
  messageExpiryInterval?.let { properties[MQTT5PropertyType.MESSAGE_EXPIRY_INTERVAL.toInt()] = it }
201
256
  contentType?.let { properties[MQTT5PropertyType.CONTENT_TYPE.toInt()] = it }
202
- client.publish(topic, data, minOf(qos, 2), if (properties.isNotEmpty()) properties else null)
257
+ client.publish(topic, data, minOf(qos ?: 0, 2), if (properties.isNotEmpty()) properties else null)
203
258
  call.resolve(JSObject().put("success", true))
204
259
  } catch (e: Exception) {
205
260
  val msg = e.message ?: "Publish failed"
@@ -226,7 +281,7 @@ class MqttQuicPlugin : Plugin() {
226
281
 
227
282
  scope.launch {
228
283
  try {
229
- client.subscribe(topic, minOf(qos, 2), subscriptionIdentifier)
284
+ client.subscribe(topic, minOf(qos ?: 0, 2), subscriptionIdentifier)
230
285
  call.resolve(JSObject().put("success", true))
231
286
  } catch (e: Exception) {
232
287
  call.reject(e.message ?: "Subscribe failed")