@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.
- package/README.md +90 -9
- package/android/install/wolfssl-android/arm64-v8a/lib/libwolfssl.a +0 -0
- package/android/install/wolfssl-android/armeabi-v7a/lib/libwolfssl.a +0 -0
- package/android/install/wolfssl-android/x86_64/lib/libwolfssl.a +0 -0
- package/android/src/main/kotlin/ai/annadata/mqttquic/MqttQuicPlugin.kt +17 -0
- package/android/src/main/kotlin/ai/annadata/mqttquic/client/MQTTClient.kt +40 -0
- package/dist/esm/definitions.d.ts +8 -0
- package/dist/esm/definitions.d.ts.map +1 -1
- package/dist/esm/web.d.ts +5 -1
- package/dist/esm/web.d.ts.map +1 -1
- package/dist/esm/web.js +6 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +6 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +6 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/MqttQuicPlugin/Client/MQTTClient.swift +66 -0
- package/ios/Sources/MqttQuicPlugin/MqttQuicPlugin.swift +26 -8
- package/ios/Sources/MqttQuicPlugin/Transport/QUICStreamAdapter.swift +2 -7
- package/ios/libs/libnghttp3.a +0 -0
- package/ios/libs/libngtcp2.a +0 -0
- package/ios/libs/libngtcp2_crypto_wolfssl.a +0 -0
- package/ios/libs/libwolfssl.a +0 -0
- package/ios/libs-simulator/libnghttp3.a +0 -0
- package/ios/libs-simulator/libngtcp2.a +0 -0
- package/ios/libs-simulator/libngtcp2_crypto_wolfssl.a +0 -0
- package/ios/libs-simulator/libwolfssl.a +0 -0
- package/ios/libs-simulator-x86_64/libnghttp3.a +0 -0
- package/ios/libs-simulator-x86_64/libngtcp2.a +0 -0
- package/ios/libs-simulator-x86_64/libngtcp2_crypto_wolfssl.a +0 -0
- package/ios/libs-simulator-x86_64/libwolfssl.a +0 -0
- 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
|
-
//
|
|
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
|
package/ios/libs/libnghttp3.a
CHANGED
|
Binary file
|
package/ios/libs/libngtcp2.a
CHANGED
|
Binary file
|
|
Binary file
|
package/ios/libs/libwolfssl.a
CHANGED
|
Binary file
|
|
Binary file
|
|
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
|
|
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",
|