@cnhongwei/react-native-mqtt 0.0.0-stage → 0.1.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 (58) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +169 -2
  3. package/README.zh-CN.md +100 -0
  4. package/ReactNativeMqtt.podspec +28 -0
  5. package/THIRD_PARTY_NOTICES.md +17 -0
  6. package/android/build.gradle +70 -0
  7. package/android/consumer-rules.pro +10 -0
  8. package/android/src/main/AndroidManifest.xml +2 -0
  9. package/android/src/main/java/com/cnhongwei/reactnativemqtt/MqttOptionsParser.kt +176 -0
  10. package/android/src/main/java/com/cnhongwei/reactnativemqtt/MqttSession.kt +1006 -0
  11. package/android/src/main/java/com/cnhongwei/reactnativemqtt/PahoLogger.java +44 -0
  12. package/android/src/main/java/com/cnhongwei/reactnativemqtt/PahoMqttTransport.kt +399 -0
  13. package/android/src/main/java/com/cnhongwei/reactnativemqtt/ReactNativeMqttModule.kt +619 -0
  14. package/android/src/main/java/com/cnhongwei/reactnativemqtt/ReactNativeMqttPackage.kt +31 -0
  15. package/ios/ReactNativeMqtt.h +15 -0
  16. package/ios/ReactNativeMqtt.mm +1184 -0
  17. package/ios/ReactNativeMqttCocoaTransport.swift +782 -0
  18. package/ios/ReactNativeMqttConnAckProbe.swift +87 -0
  19. package/ios/ReactNativeMqttSession.swift +420 -0
  20. package/lib/module/NativeReactNativeMqtt.js +5 -0
  21. package/lib/module/NativeReactNativeMqtt.js.map +1 -0
  22. package/lib/module/client.js +555 -0
  23. package/lib/module/client.js.map +1 -0
  24. package/lib/module/errors.js +20 -0
  25. package/lib/module/errors.js.map +1 -0
  26. package/lib/module/helpers.js +116 -0
  27. package/lib/module/helpers.js.map +1 -0
  28. package/lib/module/index.js +8 -0
  29. package/lib/module/index.js.map +1 -0
  30. package/lib/module/package.json +1 -0
  31. package/lib/module/types.js +2 -0
  32. package/lib/module/types.js.map +1 -0
  33. package/lib/module/validation.js +306 -0
  34. package/lib/module/validation.js.map +1 -0
  35. package/lib/typescript/package.json +1 -0
  36. package/lib/typescript/src/NativeReactNativeMqtt.d.ts +154 -0
  37. package/lib/typescript/src/NativeReactNativeMqtt.d.ts.map +1 -0
  38. package/lib/typescript/src/client.d.ts +3 -0
  39. package/lib/typescript/src/client.d.ts.map +1 -0
  40. package/lib/typescript/src/errors.d.ts +18 -0
  41. package/lib/typescript/src/errors.d.ts.map +1 -0
  42. package/lib/typescript/src/helpers.d.ts +5 -0
  43. package/lib/typescript/src/helpers.d.ts.map +1 -0
  44. package/lib/typescript/src/index.d.ts +9 -0
  45. package/lib/typescript/src/index.d.ts.map +1 -0
  46. package/lib/typescript/src/types.d.ts +161 -0
  47. package/lib/typescript/src/types.d.ts.map +1 -0
  48. package/lib/typescript/src/validation.d.ts +13 -0
  49. package/lib/typescript/src/validation.d.ts.map +1 -0
  50. package/package.json +188 -4
  51. package/react-native.config.js +1 -0
  52. package/src/NativeReactNativeMqtt.ts +173 -0
  53. package/src/client.ts +859 -0
  54. package/src/errors.ts +51 -0
  55. package/src/helpers.ts +151 -0
  56. package/src/index.ts +30 -0
  57. package/src/types.ts +184 -0
  58. package/src/validation.ts +735 -0
@@ -0,0 +1,782 @@
1
+ import CocoaMQTT
2
+ import Foundation
3
+ import Security
4
+
5
+ private extension Array {
6
+ subscript(safe index: Int) -> Element? {
7
+ indices.contains(index) ? self[index] : nil
8
+ }
9
+ }
10
+
11
+ private func mqttReasonError(_ message: String, reasonCode: Int, reasonString: String? = nil) -> NSError {
12
+ NSError(
13
+ domain: "ReactNativeMqtt",
14
+ code: reasonCode,
15
+ userInfo: [
16
+ NSLocalizedDescriptionKey: message,
17
+ "reasonCode": reasonCode,
18
+ "reasonString": reasonString ?? message,
19
+ ]
20
+ )
21
+ }
22
+
23
+ struct ReactNativeMqttCocoaConnAck {
24
+ let sessionPresent: Bool
25
+ let reasonCode: UInt8
26
+ let reasonString: String?
27
+ let assignedClientIdentifier: String?
28
+ let userProperties: [CocoaMQTTUserProperty]
29
+ }
30
+
31
+ private func configureCocoaSocket(_ socket: CocoaMQTTSocketProtocol, serverName: String, caBase64: String?, p12Base64: String?, p12Password: String?, insecure: Bool) throws -> [SecCertificate] {
32
+ guard caBase64 != nil || p12Base64 != nil || insecure else { return [] }
33
+ var certificates = [SecCertificate]()
34
+ if let caBase64 {
35
+ guard let data = Data(base64Encoded: caBase64), let certificate = SecCertificateCreateWithData(nil, data as CFData) else {
36
+ throw NSError(domain: "ReactNativeMqtt", code: 40, userInfo: [NSLocalizedDescriptionKey: "tls.caBase64 is not a valid certificate"])
37
+ }
38
+ certificates.append(certificate)
39
+ }
40
+ var identity: CocoaMQTTClientIdentity?
41
+ if let p12Base64 {
42
+ guard let data = Data(base64Encoded: p12Base64) else {
43
+ throw NSError(domain: "ReactNativeMqtt", code: 41, userInfo: [NSLocalizedDescriptionKey: "tls.p12Base64 is not valid base64"])
44
+ }
45
+ var imported: CFArray?
46
+ let status = SecPKCS12Import(data as NSData, [kSecImportExportPassphrase as String: p12Password ?? ""] as NSDictionary, &imported)
47
+ guard status == errSecSuccess, let items = imported as? [[String: Any]], let rawIdentity = items.first?[kSecImportItemIdentity as String] else {
48
+ throw NSError(domain: "ReactNativeMqtt", code: 42, userInfo: [NSLocalizedDescriptionKey: "tls.p12Base64 or tls.p12Password is invalid"])
49
+ }
50
+ guard CFGetTypeID(rawIdentity as CFTypeRef) == SecIdentityGetTypeID() else {
51
+ throw NSError(domain: "ReactNativeMqtt", code: 42, userInfo: [NSLocalizedDescriptionKey: "tls.p12Base64 did not contain a client identity"])
52
+ }
53
+ // Bridge the checked Core Foundation object, not the Swift `Any`
54
+ // existential (which has a different representation on arm64).
55
+ let secIdentity = unsafeBitCast(rawIdentity as CFTypeRef, to: SecIdentity.self)
56
+ identity = CocoaMQTTClientIdentity(identity: secIdentity)
57
+ }
58
+ if let raw = socket as? CocoaMQTTSocket {
59
+ raw.tlsServerName = serverName
60
+ raw.trustedServerCertificates = certificates
61
+ raw.clientIdentity = identity
62
+ raw.usesSystemTrustStore = !insecure && certificates.isEmpty
63
+ raw.manuallyEvaluateTrust = insecure
64
+ } else if let webSocket = socket as? CocoaMQTTWebSocket {
65
+ webSocket.tlsServerName = serverName
66
+ webSocket.trustedServerCertificates = certificates
67
+ webSocket.clientIdentity = identity
68
+ webSocket.usesSystemTrustStore = !insecure && certificates.isEmpty
69
+ webSocket.manuallyEvaluateTrust = insecure
70
+ }
71
+ return certificates
72
+ }
73
+
74
+ private func commonName(_ certificate: SecCertificate) -> String? {
75
+ var value: CFString?
76
+ guard SecCertificateCopyCommonName(certificate, &value) == errSecSuccess else { return nil }
77
+ return value as String?
78
+ }
79
+
80
+ private func hasSubjectAlternativeNames(_ certificate: SecCertificate) -> Bool {
81
+ let data = SecCertificateCopyData(certificate) as Data
82
+ let oid: [UInt8] = [0x06, 0x03, 0x55, 0x1d, 0x11]
83
+ return data.withUnsafeBytes { bytes in
84
+ guard let base = bytes.baseAddress else { return false }
85
+ guard data.count >= oid.count else { return false }
86
+ let values = base.assumingMemoryBound(to: UInt8.self)
87
+ return (0...(data.count - oid.count)).contains { index in
88
+ oid.enumerated().allSatisfy { values[index + $0.offset] == $0.element }
89
+ }
90
+ }
91
+ }
92
+
93
+ private func evaluateConfiguredServerTrust(_ trust: SecTrust, serverName: String, certificates: [SecCertificate], allowInvalidHostnameForTesting: Bool = false) -> Bool {
94
+ let leaf = SecTrustGetCertificateAtIndex(trust, 0)
95
+ let cnFallback = allowInvalidHostnameForTesting && serverName.caseInsensitiveCompare("test.mosquitto.org") == .orderedSame && leaf.map { !hasSubjectAlternativeNames($0) && commonName($0)?.caseInsensitiveCompare(serverName) == .orderedSame } == true
96
+ guard SecTrustSetPolicies(trust, cnFallback ? SecPolicyCreateBasicX509() : SecPolicyCreateSSL(true, serverName as CFString)) == errSecSuccess else { return false }
97
+ if !certificates.isEmpty {
98
+ guard SecTrustSetAnchorCertificates(trust, certificates as CFArray) == errSecSuccess,
99
+ SecTrustSetAnchorCertificatesOnly(trust, true) == errSecSuccess else { return false }
100
+ }
101
+ var error: CFError?
102
+ return SecTrustEvaluateWithError(trust, &error)
103
+ }
104
+
105
+ private protocol ReactNativeMqttCocoaTransporting: AnyObject {
106
+ func connect(completion: @escaping (Result<ReactNativeMqttCocoaConnAck, Error>) -> Void)
107
+ func disconnect()
108
+ func disconnect(completion: @escaping (Error?) -> Void)
109
+ func setDisconnectedHandler(_ handler: @escaping (NSNumber?, String?) -> Void)
110
+ func setMessageHandler(_ handler: @escaping (String, NSData, NSNumber, Bool, NSDictionary?) -> Void)
111
+ func subscribe(filter: String, qos: UInt8, noLocal: Bool, retainAsPublished: Bool, retainHandling: UInt8, completion: @escaping (Result<NSNumber, Error>) -> Void)
112
+ func subscribeBatch(_ subscriptions: [(filter: String, qos: UInt8, noLocal: Bool, retainAsPublished: Bool, retainHandling: UInt8)], completion: @escaping (Result<[NSNumber], Error>) -> Void)
113
+ func unsubscribe(filter: String, completion: @escaping (Result<Void, Error>) -> Void)
114
+ func publish(topic: String, payload: [UInt8], qos: UInt8, retained: Bool, properties: NSDictionary?, completion: @escaping (Result<Void, Error>) -> Void)
115
+ }
116
+
117
+ /// A single-owner MQTT 5 transport used by the native session.
118
+ /// CocoaMQTT's automatic reconnect is deliberately disabled; retry ownership
119
+ /// remains in the Session state machine.
120
+ final class ReactNativeMqttCocoaTransport: NSObject, CocoaMQTT5Delegate, ReactNativeMqttCocoaTransporting {
121
+ private let probe: ReactNativeMqttConnAckProbe
122
+ private let mqtt: CocoaMQTT5
123
+ private let callbackQueue = DispatchQueue(label: "com.cnhongwei.reactnativemqtt.cocoa-callback")
124
+ private var connectCompletion: ((Result<ReactNativeMqttCocoaConnAck, Error>) -> Void)?
125
+ private var subscribeCompletion: ((Result<NSNumber, Error>) -> Void)?
126
+ private var subscribeBatchCompletion: ((Result<[NSNumber], Error>) -> Void)?
127
+ private var pendingSubscribeFilters: [String] = []
128
+ private var unsubscribeCompletion: ((Result<Void, Error>) -> Void)?
129
+ private var publishCompletions: [UInt16: (Result<Void, Error>) -> Void] = [:]
130
+ private var messageHandler: ((String, NSData, NSNumber, Bool, NSDictionary?) -> Void)?
131
+ private var disconnectedHandler: ((NSNumber?, String?) -> Void)?
132
+ private var serverDisconnectReasonCode: NSNumber?
133
+ private var remoteDisconnectNotified = false
134
+ private var disconnectCompletion: ((Error?) -> Void)?
135
+ private var suppressDisconnectHandler = false
136
+ private var configurationError: Error?
137
+ private let insecureTrust: Bool
138
+ private let allowInvalidHostnameForTesting: Bool
139
+ private let tlsServerName: String
140
+ private var configuredServerCertificates = [SecCertificate]()
141
+
142
+ init(clientId: String, host: String, port: UInt16, transport: String = "tcp", path: String = "/mqtt", username: String? = nil, password: String? = nil, caBase64: String? = nil, p12Base64: String? = nil, p12Password: String? = nil, insecure: Bool = false, allowInvalidHostnameForTesting: Bool = false, keepAlive: UInt16 = 60, cleanStart: Bool = true, sessionExpiryInterval: UInt32 = 0, receiveMaximum: UInt16 = UInt16.max, userProperties: [NSDictionary] = [], willTopic: String? = nil, willPayloadBase64: String? = nil, willQos: UInt8 = 0, willRetained: Bool = false) {
143
+ var socket: CocoaMQTTSocketProtocol = (transport == "ws" || transport == "wss") ? CocoaMQTTWebSocket(uri: path) : CocoaMQTTSocket()
144
+ socket.enableSSL = transport == "tls" || transport == "wss"
145
+ probe = ReactNativeMqttConnAckProbe(socket: socket)
146
+ mqtt = CocoaMQTT5(clientID: clientId, host: host, port: port, socket: probe)
147
+ insecureTrust = insecure
148
+ self.allowInvalidHostnameForTesting = allowInvalidHostnameForTesting
149
+ tlsServerName = host
150
+ super.init()
151
+ do { configuredServerCertificates = try configureCocoaSocket(socket, serverName: host, caBase64: caBase64, p12Base64: p12Base64, p12Password: p12Password, insecure: insecure) } catch { configurationError = error }
152
+ mqtt.username = username
153
+ mqtt.password = password
154
+ mqtt.keepAlive = keepAlive
155
+ mqtt.cleanSession = cleanStart
156
+ let connectProperties = MqttConnectProperties()
157
+ connectProperties.sessionExpiryInterval = sessionExpiryInterval
158
+ connectProperties.receiveMaximum = receiveMaximum
159
+ connectProperties.userPropertyList = userProperties.compactMap { pair in
160
+ guard let key = pair["key"] as? String, let value = pair["value"] as? String else { return nil }
161
+ return CocoaMQTTUserProperty(key: key, value: value)
162
+ }
163
+ mqtt.connectProperties = connectProperties
164
+ if let willTopic, let willPayloadBase64, let willPayload = Data(base64Encoded: willPayloadBase64) {
165
+ mqtt.willMessage = CocoaMQTT5Message(topic: willTopic, payload: [UInt8](willPayload), qos: CocoaMQTTQoS(rawValue: willQos) ?? .qos0, retained: willRetained)
166
+ } else if willTopic != nil || willPayloadBase64 != nil {
167
+ configurationError = NSError(domain: "ReactNativeMqtt", code: 43, userInfo: [NSLocalizedDescriptionKey: "willPayloadBase64 is invalid"])
168
+ }
169
+ mqtt.autoReconnect = false
170
+ mqtt.delegate = self
171
+ mqtt.delegateQueue = callbackQueue
172
+ }
173
+
174
+ func connect(completion: @escaping (Result<ReactNativeMqttCocoaConnAck, Error>) -> Void) {
175
+ if let configurationError { completion(.failure(configurationError)); return }
176
+ remoteDisconnectNotified = false
177
+ connectCompletion = completion
178
+ guard mqtt.connect() else {
179
+ connectCompletion = nil
180
+ completion(.failure(NSError(domain: "ReactNativeMqtt", code: 1, userInfo: [NSLocalizedDescriptionKey: "CocoaMQTT rejected connect before socket I/O"])))
181
+ return
182
+ }
183
+ }
184
+
185
+ func disconnect() {
186
+ suppressDisconnectHandler = true
187
+ mqtt.disconnect()
188
+ }
189
+
190
+ func disconnect(completion: @escaping (Error?) -> Void) {
191
+ suppressDisconnectHandler = true
192
+ disconnectCompletion = completion
193
+ mqtt.disconnect()
194
+ }
195
+
196
+ func setDisconnectedHandler(_ handler: @escaping (NSNumber?, String?) -> Void) {
197
+ disconnectedHandler = handler
198
+ }
199
+
200
+ func setMessageHandler(_ handler: @escaping (String, NSData, NSNumber, Bool, NSDictionary?) -> Void) {
201
+ messageHandler = handler
202
+ }
203
+
204
+ func subscribe(filter: String, qos: UInt8, noLocal: Bool, retainAsPublished: Bool, retainHandling: UInt8, completion: @escaping (Result<NSNumber, Error>) -> Void) {
205
+ subscribeBatch([(filter, qos, noLocal, retainAsPublished, retainHandling)]) { result in
206
+ switch result {
207
+ case .success(let granted): completion(.success(granted[0]))
208
+ case .failure(let error): completion(.failure(error))
209
+ }
210
+ }
211
+ }
212
+
213
+ func subscribeBatch(_ subscriptions: [(filter: String, qos: UInt8, noLocal: Bool, retainAsPublished: Bool, retainHandling: UInt8)], completion: @escaping (Result<[NSNumber], Error>) -> Void) {
214
+ guard !subscriptions.isEmpty else { completion(.success([])); return }
215
+ let parsed = subscriptions.map { item -> MqttSubscription? in
216
+ guard let qos = CocoaMQTTQoS(rawValue: item.qos), qos != .FAILURE,
217
+ let retainHandling = CocoaRetainHandlingOption(rawValue: item.retainHandling) else { return nil }
218
+ let subscription = MqttSubscription(topic: item.filter, qos: qos)
219
+ subscription.noLocal = item.noLocal
220
+ subscription.retainAsPublished = item.retainAsPublished
221
+ subscription.retainHandling = retainHandling
222
+ return subscription
223
+ }
224
+ guard parsed.allSatisfy({ $0 != nil }) else {
225
+ completion(.failure(NSError(domain: "ReactNativeMqtt", code: 10, userInfo: [NSLocalizedDescriptionKey: "Invalid subscription options"])))
226
+ return
227
+ }
228
+ pendingSubscribeFilters = subscriptions.map(\.filter)
229
+ subscribeBatchCompletion = completion
230
+ mqtt.subscribe(parsed.compactMap { $0 })
231
+ }
232
+
233
+ private func completeSubscribeBatch(success: NSDictionary, failed: [String], reasonCodes: [UInt8] = []) {
234
+ guard let completion = subscribeBatchCompletion else { return }
235
+ let results = pendingSubscribeFilters.enumerated().map { index, filter -> NSNumber in
236
+ if let granted = success[filter] as? NSNumber { return granted }
237
+ if let reason = reasonCodes[safe: index] { return NSNumber(value: reason) }
238
+ // A missing or malformed per-filter ACK is a protocol failure; never
239
+ // turn an unaccounted entry into a successful subscription.
240
+ return NSNumber(value: 0x80)
241
+ }
242
+ subscribeBatchCompletion = nil
243
+ pendingSubscribeFilters = []
244
+ completion(.success(results))
245
+ }
246
+
247
+ private func failSubscribeBatch(_ error: Error) {
248
+ subscribeBatchCompletion?(.failure(error))
249
+ subscribeBatchCompletion = nil
250
+ pendingSubscribeFilters = []
251
+ }
252
+
253
+ func unsubscribe(filter: String, completion: @escaping (Result<Void, Error>) -> Void) {
254
+ unsubscribeCompletion = completion
255
+ mqtt.unsubscribe(filter)
256
+ }
257
+
258
+ func publish(topic: String, payload: [UInt8], qos: UInt8, retained: Bool, properties: NSDictionary?, completion: @escaping (Result<Void, Error>) -> Void) {
259
+ guard let qos = CocoaMQTTQoS(rawValue: qos), qos != .FAILURE else {
260
+ completion(.failure(NSError(domain: "ReactNativeMqtt", code: 11, userInfo: [NSLocalizedDescriptionKey: "Invalid publish QoS"])))
261
+ return
262
+ }
263
+ let message = CocoaMQTT5Message(topic: topic, payload: payload, qos: qos, retained: retained)
264
+ let publishProperties = MqttPublishProperties()
265
+ if let properties {
266
+ publishProperties.contentType = properties["contentType"] as? String
267
+ publishProperties.responseTopic = properties["responseTopic"] as? String
268
+ if let correlationData = properties["correlationData"] as? NSData {
269
+ publishProperties.correlationData = [UInt8](correlationData)
270
+ }
271
+ if let expiry = properties["messageExpiryIntervalSeconds"] as? NSNumber { publishProperties.messageExpiryInterval = expiry.uint32Value }
272
+ if let indicator = properties["payloadFormatIndicator"] as? NSNumber { publishProperties.payloadFormatIndicator = PayloadFormatIndicator(rawValue: indicator.uint8Value) }
273
+ if let userProperties = properties["userProperties"] as? [NSDictionary] {
274
+ publishProperties.userProperties = userProperties.compactMap { pair in
275
+ guard let key = pair["key"] as? String, let value = pair["value"] as? String else { return nil }
276
+ return CocoaMQTTUserProperty(key: key, value: value)
277
+ }
278
+ }
279
+ }
280
+ let packetId = UInt16(truncatingIfNeeded: mqtt.publish(message, properties: publishProperties))
281
+ if qos == .qos0 {
282
+ completion(.success(()))
283
+ } else {
284
+ publishCompletions[packetId] = completion
285
+ }
286
+ }
287
+
288
+ func mqtt5(_ mqtt5: CocoaMQTT5, didConnectAck ack: CocoaMQTTCONNACKReasonCode, connAckData: MqttDecodeConnAck?) {
289
+ guard let sessionPresent = probe.sessionPresent else {
290
+ connectCompletion?(.failure(NSError(domain: "ReactNativeMqtt", code: 2, userInfo: [NSLocalizedDescriptionKey: "CocoaMQTT did not expose CONNACK sessionPresent"])))
291
+ connectCompletion = nil
292
+ return
293
+ }
294
+ let result = ReactNativeMqttCocoaConnAck(sessionPresent: sessionPresent, reasonCode: ack.rawValue, reasonString: connAckData?.reasonString, assignedClientIdentifier: connAckData?.assignedClientIdentifier, userProperties: connAckData?.userProperties ?? [])
295
+ connectCompletion?(.success(result))
296
+ connectCompletion = nil
297
+ }
298
+
299
+ func mqtt5(_ mqtt5: CocoaMQTT5, didPublishMessage message: CocoaMQTT5Message, id: UInt16) {}
300
+ func mqtt5(_ mqtt5: CocoaMQTT5, didPublishAck id: UInt16, pubAckData: MqttDecodePubAck?) {
301
+ guard pubAckData?.reasonCode == nil || pubAckData?.reasonCode == .success else {
302
+ let reason = pubAckData?.reasonCode?.rawValue ?? 0x80
303
+ publishCompletions.removeValue(forKey: id)?(.failure(mqttReasonError("PUBACK rejected (reason 0x\(String(reason, radix: 16)))", reasonCode: Int(reason), reasonString: pubAckData?.reasonString)))
304
+ return
305
+ }
306
+ publishCompletions.removeValue(forKey: id)?(.success(()))
307
+ }
308
+ func mqtt5(_ mqtt5: CocoaMQTT5, didPublishRec id: UInt16, pubRecData: MqttDecodePubRec?) {
309
+ guard pubRecData?.reasonCode == nil || pubRecData?.reasonCode == .success else {
310
+ let reason = pubRecData?.reasonCode?.rawValue ?? 0x80
311
+ publishCompletions.removeValue(forKey: id)?(.failure(mqttReasonError("PUBREC rejected (reason 0x\(String(reason, radix: 16)))", reasonCode: Int(reason), reasonString: pubRecData?.reasonString)))
312
+ return
313
+ }
314
+ // QoS 2 is complete only after PUBCOMP; PUBREC merely advances the exchange.
315
+ }
316
+ func mqtt5(_ mqtt5: CocoaMQTT5, didPublishComplete id: UInt16, pubCompData: MqttDecodePubComp?) {
317
+ guard pubCompData?.reasonCode == nil || pubCompData?.reasonCode == .success else {
318
+ let reason = pubCompData?.reasonCode?.rawValue ?? 0x80
319
+ publishCompletions.removeValue(forKey: id)?(.failure(mqttReasonError("PUBCOMP rejected (reason 0x\(String(reason, radix: 16)))", reasonCode: Int(reason), reasonString: pubCompData?.reasonString)))
320
+ return
321
+ }
322
+ publishCompletions.removeValue(forKey: id)?(.success(()))
323
+ }
324
+ func mqtt5(_ mqtt5: CocoaMQTT5, didReceiveMessage message: CocoaMQTT5Message, id: UInt16, publishData: MqttDecodePublish?) {
325
+ var metadata: [String: Any] = [:]
326
+ if let publishData {
327
+ metadata["userProperties"] = publishData.userProperties.map { ["key": $0.key, "value": $0.value] }
328
+ if let contentType = publishData.contentType { metadata["contentType"] = contentType }
329
+ if let responseTopic = publishData.responseTopic { metadata["responseTopic"] = responseTopic }
330
+ if let correlationData = publishData.correlationData { metadata["correlationData"] = NSData(bytes: correlationData, length: correlationData.count) }
331
+ if let expiry = publishData.messageExpiryInterval { metadata["messageExpiryIntervalSeconds"] = NSNumber(value: expiry) }
332
+ if let indicator = publishData.payloadFormatIndicator { metadata["payloadFormatIndicator"] = NSNumber(value: indicator.rawValue) }
333
+ }
334
+ messageHandler?(message.topic, NSData(bytes: message.payload, length: message.payload.count), NSNumber(value: message.qos.rawValue), message.retained, metadata as NSDictionary)
335
+ }
336
+ func mqtt5(_ mqtt5: CocoaMQTT5, didSubscribeTopics success: NSDictionary, failed: [String], subAckData: MqttDecodeSubAck?) {
337
+ completeSubscribeBatch(success: success, failed: failed, reasonCodes: subAckData?.reasonCodes.map(\.rawValue) ?? [])
338
+ subscribeCompletion = nil
339
+ }
340
+ func mqtt5(_ mqtt5: CocoaMQTT5, didUnsubscribeTopics topics: [String], unsubAckData: MqttDecodeUnsubAck?) {
341
+ if let reasonCode = unsubAckData?.reasonCodes.first?.rawValue, reasonCode >= 0x80 {
342
+ unsubscribeCompletion?(.failure(mqttReasonError("UNSUBACK rejected", reasonCode: Int(reasonCode), reasonString: unsubAckData?.reasonString)))
343
+ } else {
344
+ unsubscribeCompletion?(.success(()))
345
+ }
346
+ unsubscribeCompletion = nil
347
+ }
348
+ func mqtt5(_ mqtt5: CocoaMQTT5, didReceiveDisconnectReasonCode reasonCode: CocoaMQTTDISCONNECTReasonCode) {
349
+ serverDisconnectReasonCode = NSNumber(value: reasonCode.rawValue)
350
+ // The MQTT DISCONNECT frame is already authoritative at this point. Do
351
+ // not wait for CocoaMQTT's later socket callback: on some transports that
352
+ // callback is delayed or omitted after a broker-initiated disconnect.
353
+ if !suppressDisconnectHandler && !remoteDisconnectNotified {
354
+ remoteDisconnectNotified = true
355
+ disconnectedHandler?(serverDisconnectReasonCode, nil)
356
+ }
357
+ }
358
+ func mqtt5(_ mqtt5: CocoaMQTT5, didReceiveAuthReasonCode reasonCode: CocoaMQTTAUTHReasonCode) {}
359
+ func mqtt5(_ mqtt5: CocoaMQTT5, didReceive trust: SecTrust, completionHandler: @escaping (Bool) -> Void) {
360
+ completionHandler(insecureTrust || (!configuredServerCertificates.isEmpty && evaluateConfiguredServerTrust(trust, serverName: tlsServerName, certificates: configuredServerCertificates, allowInvalidHostnameForTesting: allowInvalidHostnameForTesting)))
361
+ }
362
+ func mqtt5UrlSession(_ mqtt: CocoaMQTT5, didReceiveTrust trust: SecTrust, didReceiveChallenge challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
363
+ if insecureTrust {
364
+ completionHandler(.useCredential, URLCredential(trust: trust))
365
+ } else if !configuredServerCertificates.isEmpty {
366
+ let accepted = evaluateConfiguredServerTrust(trust, serverName: tlsServerName, certificates: configuredServerCertificates, allowInvalidHostnameForTesting: allowInvalidHostnameForTesting)
367
+ completionHandler(accepted ? .useCredential : .cancelAuthenticationChallenge, accepted ? URLCredential(trust: trust) : nil)
368
+ } else {
369
+ completionHandler(.performDefaultHandling, nil)
370
+ }
371
+ }
372
+ func mqtt5DidPing(_ mqtt5: CocoaMQTT5) {}
373
+ func mqtt5DidReceivePong(_ mqtt5: CocoaMQTT5) {}
374
+ func mqtt5DidDisconnect(_ mqtt5: CocoaMQTT5, withError err: Error?) {
375
+ if let completion = connectCompletion {
376
+ completion(.failure(err ?? NSError(domain: "ReactNativeMqtt", code: 3, userInfo: [NSLocalizedDescriptionKey: "CocoaMQTT disconnected before CONNACK"])))
377
+ connectCompletion = nil
378
+ }
379
+ let error = err ?? NSError(domain: "ReactNativeMqtt", code: 3, userInfo: [NSLocalizedDescriptionKey: "CocoaMQTT disconnected"])
380
+ failSubscribeBatch(error)
381
+ subscribeCompletion?(.failure(error))
382
+ unsubscribeCompletion?(.failure(error))
383
+ publishCompletions.values.forEach { $0(.failure(error)) }
384
+ subscribeCompletion = nil
385
+ unsubscribeCompletion = nil
386
+ publishCompletions.removeAll()
387
+ disconnectCompletion?(err)
388
+ disconnectCompletion = nil
389
+ // CocoaMQTT records a remote DISCONNECT before invoking this callback.
390
+ // Read the SDK-owned value here as the source of truth: the separate
391
+ // didReceiveDisconnectReasonCode delegate callback can be delivered out
392
+ // of order relative to the disconnect callback on some transports.
393
+ if let reason = mqtt5.lastDisconnectReason,
394
+ reason.source == .remote {
395
+ serverDisconnectReasonCode = NSNumber(value: reason.reasonCode.rawValue)
396
+ }
397
+ let suppress = suppressDisconnectHandler
398
+ let alreadyNotified = remoteDisconnectNotified
399
+ suppressDisconnectHandler = false
400
+ if !suppress && !alreadyNotified { disconnectedHandler?(serverDisconnectReasonCode, nil) }
401
+ remoteDisconnectNotified = false
402
+ serverDisconnectReasonCode = nil
403
+ }
404
+ }
405
+
406
+ /// MQTT 3.1.1 adapter. CocoaMQTT does not expose a session-present bit for
407
+ /// MQTT 3.1.1, so its CONNACK is reported with the protocol-defined false value.
408
+ final class ReactNativeMqttCocoa3Transport: NSObject, CocoaMQTTDelegate, ReactNativeMqttCocoaTransporting {
409
+ private let mqtt: CocoaMQTT
410
+ private let callbackQueue = DispatchQueue(label: "com.cnhongwei.reactnativemqtt.cocoa3-callback")
411
+ private var connectCompletion: ((Result<ReactNativeMqttCocoaConnAck, Error>) -> Void)?
412
+ private var subscribeCompletion: ((Result<NSNumber, Error>) -> Void)?
413
+ private var subscribeBatchCompletion: ((Result<[NSNumber], Error>) -> Void)?
414
+ private var pendingSubscribeFilters: [String] = []
415
+ private var unsubscribeCompletion: ((Result<Void, Error>) -> Void)?
416
+ private var publishCompletions: [UInt16: (Result<Void, Error>) -> Void] = [:]
417
+ private var messageHandler: ((String, NSData, NSNumber, Bool, NSDictionary?) -> Void)?
418
+ private var disconnectedHandler: ((NSNumber?, String?) -> Void)?
419
+ private var disconnectCompletion: ((Error?) -> Void)?
420
+ private var suppressDisconnectHandler = false
421
+ private var configurationError: Error?
422
+ private let insecureTrust: Bool
423
+ private let allowInvalidHostnameForTesting: Bool
424
+ private let tlsServerName: String
425
+ private var configuredServerCertificates = [SecCertificate]()
426
+
427
+ init(clientId: String, host: String, port: UInt16, transport: String = "tcp", path: String = "/mqtt", username: String? = nil, password: String? = nil, caBase64: String? = nil, p12Base64: String? = nil, p12Password: String? = nil, insecure: Bool = false, allowInvalidHostnameForTesting: Bool = false, keepAlive: UInt16 = 60, cleanSession: Bool = true, willTopic: String? = nil, willPayloadBase64: String? = nil, willQos: UInt8 = 0, willRetained: Bool = false) {
428
+ var socket: CocoaMQTTSocketProtocol = (transport == "ws" || transport == "wss") ? CocoaMQTTWebSocket(uri: path) : CocoaMQTTSocket()
429
+ socket.enableSSL = transport == "tls" || transport == "wss"
430
+ mqtt = CocoaMQTT(clientID: clientId, host: host, port: port, socket: socket)
431
+ insecureTrust = insecure
432
+ self.allowInvalidHostnameForTesting = allowInvalidHostnameForTesting
433
+ tlsServerName = host
434
+ super.init()
435
+ do { configuredServerCertificates = try configureCocoaSocket(socket, serverName: host, caBase64: caBase64, p12Base64: p12Base64, p12Password: p12Password, insecure: insecure) } catch { configurationError = error }
436
+ mqtt.username = username
437
+ mqtt.password = password
438
+ mqtt.keepAlive = keepAlive
439
+ mqtt.cleanSession = cleanSession
440
+ if let willTopic, let willPayloadBase64, let willPayload = Data(base64Encoded: willPayloadBase64) {
441
+ mqtt.willMessage = CocoaMQTTMessage(topic: willTopic, payload: [UInt8](willPayload), qos: CocoaMQTTQoS(rawValue: willQos) ?? .qos0, retained: willRetained)
442
+ } else if willTopic != nil || willPayloadBase64 != nil {
443
+ configurationError = NSError(domain: "ReactNativeMqtt", code: 43, userInfo: [NSLocalizedDescriptionKey: "willPayloadBase64 is invalid"])
444
+ }
445
+ mqtt.autoReconnect = false
446
+ mqtt.delegate = self
447
+ mqtt.delegateQueue = callbackQueue
448
+ }
449
+
450
+ func connect(completion: @escaping (Result<ReactNativeMqttCocoaConnAck, Error>) -> Void) {
451
+ if let configurationError { completion(.failure(configurationError)); return }
452
+ connectCompletion = completion
453
+ guard mqtt.connect() else {
454
+ connectCompletion = nil
455
+ completion(.failure(NSError(domain: "ReactNativeMqtt", code: 21, userInfo: [NSLocalizedDescriptionKey: "CocoaMQTT rejected connect before socket I/O"])))
456
+ return
457
+ }
458
+ }
459
+
460
+ func disconnect() { suppressDisconnectHandler = true; mqtt.disconnect() }
461
+
462
+ func disconnect(completion: @escaping (Error?) -> Void) {
463
+ suppressDisconnectHandler = true
464
+ disconnectCompletion = completion
465
+ mqtt.disconnect()
466
+ }
467
+
468
+ func setDisconnectedHandler(_ handler: @escaping (NSNumber?, String?) -> Void) {
469
+ disconnectedHandler = handler
470
+ }
471
+
472
+ func setMessageHandler(_ handler: @escaping (String, NSData, NSNumber, Bool, NSDictionary?) -> Void) {
473
+ messageHandler = handler
474
+ }
475
+
476
+ func subscribe(filter: String, qos: UInt8, noLocal: Bool, retainAsPublished: Bool, retainHandling: UInt8, completion: @escaping (Result<NSNumber, Error>) -> Void) {
477
+ subscribeBatch([(filter, qos, noLocal, retainAsPublished, retainHandling)]) { result in
478
+ switch result {
479
+ case .success(let granted): completion(.success(granted[0]))
480
+ case .failure(let error): completion(.failure(error))
481
+ }
482
+ }
483
+ }
484
+
485
+ func subscribeBatch(_ subscriptions: [(filter: String, qos: UInt8, noLocal: Bool, retainAsPublished: Bool, retainHandling: UInt8)], completion: @escaping (Result<[NSNumber], Error>) -> Void) {
486
+ guard subscriptions.allSatisfy({ $0.noLocal == false && $0.retainAsPublished == false && $0.retainHandling == 0 }) else {
487
+ completion(.failure(NSError(domain: "ReactNativeMqtt", code: 22, userInfo: [NSLocalizedDescriptionKey: "MQTT 3.1.1 does not support the requested subscription options"])))
488
+ return
489
+ }
490
+ let qosValues = subscriptions.map { CocoaMQTTQoS(rawValue: $0.qos) }
491
+ guard qosValues.allSatisfy({ $0 != nil && $0! != .FAILURE }) else {
492
+ completion(.failure(NSError(domain: "ReactNativeMqtt", code: 22, userInfo: [NSLocalizedDescriptionKey: "Invalid subscription QoS"])))
493
+ return
494
+ }
495
+ pendingSubscribeFilters = subscriptions.map(\.filter)
496
+ subscribeBatchCompletion = completion
497
+ mqtt.subscribe(zip(subscriptions, qosValues).map { ($0.0.filter, $0.1!) })
498
+ }
499
+
500
+ private func completeSubscribeBatch(success: NSDictionary, failed: [String]) {
501
+ guard let completion = subscribeBatchCompletion else { return }
502
+ let results = pendingSubscribeFilters.map { filter -> NSNumber in
503
+ if let granted = success[filter] as? NSNumber { return granted }
504
+ return NSNumber(value: failed.contains(filter) ? 0x80 : 0x80)
505
+ }
506
+ subscribeBatchCompletion = nil
507
+ pendingSubscribeFilters = []
508
+ completion(.success(results))
509
+ }
510
+
511
+ private func failSubscribeBatch(_ error: Error) {
512
+ subscribeBatchCompletion?(.failure(error))
513
+ subscribeBatchCompletion = nil
514
+ pendingSubscribeFilters = []
515
+ }
516
+
517
+ func unsubscribe(filter: String, completion: @escaping (Result<Void, Error>) -> Void) {
518
+ unsubscribeCompletion = completion
519
+ mqtt.unsubscribe(filter)
520
+ }
521
+
522
+ func publish(topic: String, payload: [UInt8], qos: UInt8, retained: Bool, properties: NSDictionary?, completion: @escaping (Result<Void, Error>) -> Void) {
523
+ guard let qos = CocoaMQTTQoS(rawValue: qos), qos != .FAILURE else {
524
+ completion(.failure(NSError(domain: "ReactNativeMqtt", code: 23, userInfo: [NSLocalizedDescriptionKey: "Invalid publish QoS"])))
525
+ return
526
+ }
527
+ let packetId = mqtt.publish(CocoaMQTTMessage(topic: topic, payload: payload, qos: qos, retained: retained))
528
+ guard packetId >= 0 else {
529
+ completion(.failure(NSError(domain: "ReactNativeMqtt", code: 24, userInfo: [NSLocalizedDescriptionKey: "CocoaMQTT rejected publish"])))
530
+ return
531
+ }
532
+ if qos == .qos0 { completion(.success(())) }
533
+ else { publishCompletions[UInt16(packetId)] = completion }
534
+ }
535
+
536
+ func mqtt(_ mqtt: CocoaMQTT, didConnectAck ack: CocoaMQTTConnAck) {
537
+ let result = ReactNativeMqttCocoaConnAck(sessionPresent: false, reasonCode: ack.rawValue, reasonString: nil, assignedClientIdentifier: nil, userProperties: [])
538
+ connectCompletion?(.success(result))
539
+ connectCompletion = nil
540
+ }
541
+ func mqtt(_ mqtt: CocoaMQTT, didReceive trust: SecTrust, completionHandler: @escaping (Bool) -> Void) {
542
+ completionHandler(insecureTrust || (!configuredServerCertificates.isEmpty && evaluateConfiguredServerTrust(trust, serverName: tlsServerName, certificates: configuredServerCertificates, allowInvalidHostnameForTesting: allowInvalidHostnameForTesting)))
543
+ }
544
+ func mqttUrlSession(_ mqtt: CocoaMQTT, didReceiveTrust trust: SecTrust, didReceiveChallenge challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
545
+ if insecureTrust {
546
+ completionHandler(.useCredential, URLCredential(trust: trust))
547
+ } else if !configuredServerCertificates.isEmpty {
548
+ let accepted = evaluateConfiguredServerTrust(trust, serverName: tlsServerName, certificates: configuredServerCertificates)
549
+ completionHandler(accepted ? .useCredential : .cancelAuthenticationChallenge, accepted ? URLCredential(trust: trust) : nil)
550
+ } else {
551
+ completionHandler(.performDefaultHandling, nil)
552
+ }
553
+ }
554
+ func mqtt(_ mqtt: CocoaMQTT, didPublishMessage message: CocoaMQTTMessage, id: UInt16) {}
555
+ func mqtt(_ mqtt: CocoaMQTT, didPublishAck id: UInt16) { publishCompletions.removeValue(forKey: id)?(.success(())) }
556
+ func mqtt(_ mqtt: CocoaMQTT, didPublishComplete id: UInt16) { publishCompletions.removeValue(forKey: id)?(.success(())) }
557
+ func mqtt(_ mqtt: CocoaMQTT, didReceiveMessage message: CocoaMQTTMessage, id: UInt16) {
558
+ messageHandler?(message.topic, NSData(bytes: message.payload, length: message.payload.count), NSNumber(value: message.qos.rawValue), message.retained, nil)
559
+ }
560
+ func mqtt(_ mqtt: CocoaMQTT, didSubscribeTopics success: NSDictionary, failed: [String]) {
561
+ completeSubscribeBatch(success: success, failed: failed)
562
+ subscribeCompletion = nil
563
+ }
564
+ func mqtt(_ mqtt: CocoaMQTT, didUnsubscribeTopics topics: [String]) { unsubscribeCompletion?(.success(())); unsubscribeCompletion = nil }
565
+ func mqttDidPing(_ mqtt: CocoaMQTT) {}
566
+ func mqttDidReceivePong(_ mqtt: CocoaMQTT) {}
567
+ func mqttDidDisconnect(_ mqtt: CocoaMQTT, withError err: Error?) {
568
+ let error = err ?? NSError(domain: "ReactNativeMqtt", code: 26, userInfo: [NSLocalizedDescriptionKey: "CocoaMQTT disconnected"])
569
+ connectCompletion?(.failure(error)); connectCompletion = nil
570
+ failSubscribeBatch(error)
571
+ subscribeCompletion?(.failure(error)); subscribeCompletion = nil
572
+ unsubscribeCompletion?(.failure(error)); unsubscribeCompletion = nil
573
+ publishCompletions.values.forEach { $0(.failure(error)) }
574
+ publishCompletions.removeAll()
575
+ disconnectCompletion?(err)
576
+ disconnectCompletion = nil
577
+ let suppress = suppressDisconnectHandler
578
+ suppressDisconnectHandler = false
579
+ if !suppress { disconnectedHandler?(nil, nil) }
580
+ }
581
+ }
582
+
583
+ /// Objective-C boundary for the TurboModule. CocoaMQTT types never cross this
584
+ /// boundary, so the ObjC++ session only receives Foundation values.
585
+ private final class ReactNativeMqttSessionTransportAdapter: ReactNativeMqttSessionTransport {
586
+ let underlying: ReactNativeMqttCocoaTransporting
587
+ private var sessionDisconnectedHandler: ((Error?) -> Void)?
588
+ var publicDisconnectedHandler: ((Error?) -> Void)?
589
+
590
+ init(underlying: ReactNativeMqttCocoaTransporting) {
591
+ self.underlying = underlying
592
+ }
593
+
594
+ func connect(completion: @escaping (Result<Any, Error>) -> Void) {
595
+ underlying.connect { result in
596
+ switch result {
597
+ case .success(let ack): completion(.success(ack))
598
+ case .failure(let error): completion(.failure(error))
599
+ }
600
+ }
601
+ }
602
+
603
+ func disconnect(completion: @escaping (Error?) -> Void) {
604
+ underlying.disconnect(completion: completion)
605
+ }
606
+
607
+ func setDisconnectedHandler(_ handler: @escaping (Error?) -> Void) {
608
+ sessionDisconnectedHandler = handler
609
+ underlying.setDisconnectedHandler { [weak self] reasonCode, reasonString in
610
+ var userInfo: [String: Any] = [:]
611
+ if let reasonCode { userInfo["reasonCode"] = reasonCode }
612
+ if let reasonString { userInfo["reasonString"] = reasonString }
613
+ let error = userInfo.isEmpty ? nil : NSError(domain: "ReactNativeMqtt", code: 0, userInfo: userInfo)
614
+ self?.sessionDisconnectedHandler?(error)
615
+ }
616
+ }
617
+ }
618
+
619
+ @objc(ReactNativeMqttCocoaBridge)
620
+ public final class ReactNativeMqttCocoaBridge: NSObject {
621
+ private let transport: ReactNativeMqttCocoaTransporting
622
+ private let session: ReactNativeMqttSession
623
+ private let sessionTransport: ReactNativeMqttSessionTransportAdapter
624
+ private let operationLedger: ReactNativeMqttOperationLedger
625
+ private var sessionEventHandler: ((String, NSNumber, NSNumber, NSDictionary?) -> Void)?
626
+
627
+ @objc(initWithClientId:host:port:protocolVersion:transport:path:username:password:caBase64:p12Base64:p12Password:insecure:allowInvalidHostnameForTesting:keepAlive:cleanStart:cleanSession:sessionExpiryInterval:receiveMaximum:userProperties:willTopic:willPayloadBase64:willQos:willRetained:reconnectEnabled:reconnectInitialDelayMs:reconnectMaxDelayMs:reconnectJitterRatio:connectTimeoutMs:disconnectTimeoutMs:)
628
+ public init(clientId: String, host: String, port: NSNumber, protocolVersion: NSNumber, transport: String, path: String, username: String?, password: String?, caBase64: String?, p12Base64: String?, p12Password: String?, insecure: Bool, allowInvalidHostnameForTesting: Bool, keepAlive: NSNumber, cleanStart: Bool, cleanSession: Bool, sessionExpiryInterval: NSNumber, receiveMaximum: NSNumber, userProperties: [NSDictionary], willTopic: String?, willPayloadBase64: String?, willQos: NSNumber, willRetained: Bool, reconnectEnabled: Bool, reconnectInitialDelayMs: NSNumber, reconnectMaxDelayMs: NSNumber, reconnectJitterRatio: NSNumber, connectTimeoutMs: NSNumber, disconnectTimeoutMs: NSNumber) {
629
+ if protocolVersion.intValue == 4 {
630
+ self.transport = ReactNativeMqttCocoa3Transport(clientId: clientId, host: host, port: UInt16(truncating: port), transport: transport, path: path, username: username, password: password, caBase64: caBase64, p12Base64: p12Base64, p12Password: p12Password, insecure: insecure, allowInvalidHostnameForTesting: allowInvalidHostnameForTesting, keepAlive: UInt16(truncating: keepAlive), cleanSession: cleanSession, willTopic: willTopic, willPayloadBase64: willPayloadBase64, willQos: UInt8(truncating: willQos), willRetained: willRetained)
631
+ } else {
632
+ self.transport = ReactNativeMqttCocoaTransport(clientId: clientId, host: host, port: UInt16(truncating: port), transport: transport, path: path, username: username, password: password, caBase64: caBase64, p12Base64: p12Base64, p12Password: p12Password, insecure: insecure, allowInvalidHostnameForTesting: allowInvalidHostnameForTesting, keepAlive: UInt16(truncating: keepAlive), cleanStart: cleanStart, sessionExpiryInterval: UInt32(truncating: sessionExpiryInterval), receiveMaximum: UInt16(truncating: receiveMaximum), userProperties: userProperties, willTopic: willTopic, willPayloadBase64: willPayloadBase64, willQos: UInt8(truncating: willQos), willRetained: willRetained)
633
+ }
634
+ self.sessionTransport = ReactNativeMqttSessionTransportAdapter(underlying: self.transport)
635
+ self.session = ReactNativeMqttSession(
636
+ transport: self.sessionTransport,
637
+ queueLabel: "com.cnhongwei.reactnativemqtt.cocoa-session",
638
+ reconnectEnabled: reconnectEnabled,
639
+ reconnectInitialDelayMs: reconnectInitialDelayMs.uint64Value,
640
+ reconnectMaxDelayMs: reconnectMaxDelayMs.uint64Value,
641
+ reconnectJitterRatio: reconnectJitterRatio.doubleValue,
642
+ connectTimeoutMs: connectTimeoutMs.uint64Value,
643
+ disconnectTimeoutMs: disconnectTimeoutMs.uint64Value,
644
+ disconnectHandler: { [weak sessionTransport] error in sessionTransport?.publicDisconnectedHandler?(error) }
645
+ )
646
+ self.operationLedger = ReactNativeMqttOperationLedger(queueLabel: "com.cnhongwei.reactnativemqtt.cocoa-operations")
647
+ super.init()
648
+ session.setSnapshotHandler { [weak self] snapshot in
649
+ guard snapshot.state != .connected else { return }
650
+ self?.sessionEventHandler?(snapshot.state.rawValue, NSNumber(value: snapshot.revision), NSNumber(value: snapshot.generation), nil)
651
+ }
652
+ session.setConnectHandler { [weak self] snapshot, value in
653
+ guard let ack = value as? ReactNativeMqttCocoaConnAck else { return }
654
+ self?.sessionEventHandler?(snapshot.state.rawValue, NSNumber(value: snapshot.revision), NSNumber(value: snapshot.generation), [
655
+ "sessionPresent": ack.sessionPresent,
656
+ "reasonCode": Int(ack.reasonCode),
657
+ "reasonString": ack.reasonString as Any,
658
+ "assignedClientId": ack.assignedClientIdentifier as Any,
659
+ "userProperties": ack.userProperties.map { ["key": $0.key, "value": $0.value] },
660
+ ])
661
+ }
662
+ }
663
+
664
+ @objc(connectWithCompletion:)
665
+ public func connect(completion: @escaping ([String: Any]?, NSError?) -> Void) {
666
+ session.connect { result in
667
+ switch result {
668
+ case .success(let value):
669
+ guard let ack = value as? ReactNativeMqttCocoaConnAck else {
670
+ completion(nil, NSError(domain: "ReactNativeMqtt", code: 28, userInfo: [NSLocalizedDescriptionKey: "Invalid CONNACK result"]))
671
+ return
672
+ }
673
+ completion([
674
+ "sessionPresent": ack.sessionPresent,
675
+ "reasonCode": Int(ack.reasonCode),
676
+ "reasonString": ack.reasonString as Any,
677
+ "assignedClientId": ack.assignedClientIdentifier as Any,
678
+ "userProperties": ack.userProperties.map { ["key": $0.key, "value": $0.value] },
679
+ ], nil)
680
+ case .failure(let error):
681
+ completion(nil, error as NSError)
682
+ }
683
+ }
684
+ }
685
+
686
+ @objc public func disconnect() {
687
+ session.disconnect { _ in }
688
+ }
689
+
690
+ @objc(disconnectWithCompletion:)
691
+ public func disconnect(completion: @escaping (NSError?) -> Void) {
692
+ session.disconnect { error in completion(error as NSError?) }
693
+ }
694
+
695
+ @objc(disposeWithCompletion:)
696
+ public func dispose(completion: @escaping (NSError?) -> Void) {
697
+ session.dispose { error in completion(error as NSError?) }
698
+ }
699
+
700
+ @objc(setDisconnectedHandler:)
701
+ public func setDisconnectedHandler(_ handler: @escaping (NSNumber?, String?) -> Void) {
702
+ sessionTransport.publicDisconnectedHandler = { error in
703
+ let info = (error as NSError?)?.userInfo
704
+ handler(info?["reasonCode"] as? NSNumber, info?["reasonString"] as? String)
705
+ }
706
+ }
707
+
708
+ @objc(setSessionEventHandler:)
709
+ public func setSessionEventHandler(_ handler: @escaping (String, NSNumber, NSNumber, NSDictionary?) -> Void) {
710
+ sessionEventHandler = handler
711
+ }
712
+
713
+ @objc(setMessageHandler:)
714
+ public func setMessageHandler(_ handler: @escaping (String, NSData, NSNumber, Bool, NSDictionary?) -> Void) {
715
+ transport.setMessageHandler(handler)
716
+ }
717
+
718
+ @objc(subscribeFilter:qos:noLocal:retainAsPublished:retainHandling:completion:)
719
+ public func subscribe(filter: String, qos: NSNumber, noLocal: Bool, retainAsPublished: Bool, retainHandling: NSNumber, completion: @escaping (NSNumber?, NSError?) -> Void) {
720
+ operationLedger.submit(topic: filter, generation: session.snapshot.generation, timeoutMs: 30_000, execute: { [transport] finish in
721
+ transport.subscribe(filter: filter, qos: qos.uint8Value, noLocal: noLocal, retainAsPublished: retainAsPublished, retainHandling: retainHandling.uint8Value) { result in
722
+ switch result {
723
+ case .success(let granted): finish(.success(granted))
724
+ case .failure(let error): finish(.failure(error))
725
+ }
726
+ }
727
+ }, completion: { result in
728
+ switch result {
729
+ case .success(let value): completion(value as? NSNumber, nil)
730
+ case .failure(let error): completion(nil, error as NSError)
731
+ }
732
+ })
733
+ }
734
+
735
+ @objc(subscribeBatch:completion:)
736
+ public func subscribeBatch(_ subscriptions: [NSDictionary], completion: @escaping ([NSNumber]?, NSError?) -> Void) {
737
+ let parsed = subscriptions.compactMap { item -> (filter: String, qos: UInt8, noLocal: Bool, retainAsPublished: Bool, retainHandling: UInt8)? in
738
+ guard let filter = item["filter"] as? String,
739
+ let qos = item["qos"] as? NSNumber,
740
+ let noLocal = item["noLocal"] as? NSNumber,
741
+ let retainAsPublished = item["retainAsPublished"] as? NSNumber,
742
+ let retainHandling = item["retainHandling"] as? NSNumber else { return nil }
743
+ return (filter, qos.uint8Value, noLocal.boolValue, retainAsPublished.boolValue, retainHandling.uint8Value)
744
+ }
745
+ guard parsed.count == subscriptions.count else {
746
+ completion(nil, NSError(domain: "ReactNativeMqtt", code: 27, userInfo: [NSLocalizedDescriptionKey: "Invalid subscription batch"])); return
747
+ }
748
+ transport.subscribeBatch(parsed) { result in
749
+ switch result {
750
+ case .success(let granted): completion(granted, nil)
751
+ case .failure(let error): completion(nil, error as NSError)
752
+ }
753
+ }
754
+ }
755
+
756
+ @objc(unsubscribeFilter:completion:)
757
+ public func unsubscribe(filter: String, completion: @escaping (NSError?) -> Void) {
758
+ operationLedger.submit(topic: filter, generation: session.snapshot.generation, timeoutMs: 30_000, execute: { [transport] finish in
759
+ transport.unsubscribe(filter: filter) { result in
760
+ switch result {
761
+ case .success: finish(.success(NSNull()))
762
+ case .failure(let error): finish(.failure(error))
763
+ }
764
+ }
765
+ }, completion: { result in
766
+ switch result {
767
+ case .success: completion(nil)
768
+ case .failure(let error): completion(error as NSError)
769
+ }
770
+ })
771
+ }
772
+
773
+ @objc(publishTopic:payload:qos:retained:properties:completion:)
774
+ public func publish(topic: String, payload: NSData, qos: NSNumber, retained: Bool, properties: NSDictionary?, completion: @escaping (NSError?) -> Void) {
775
+ transport.publish(topic: topic, payload: [UInt8](payload as Data), qos: qos.uint8Value, retained: retained, properties: properties) { result in
776
+ switch result {
777
+ case .success: completion(nil)
778
+ case .failure(let error): completion(error as NSError)
779
+ }
780
+ }
781
+ }
782
+ }