@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.
- package/LICENSE +20 -0
- package/README.md +169 -2
- package/README.zh-CN.md +100 -0
- package/ReactNativeMqtt.podspec +28 -0
- package/THIRD_PARTY_NOTICES.md +17 -0
- package/android/build.gradle +70 -0
- package/android/consumer-rules.pro +10 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/cnhongwei/reactnativemqtt/MqttOptionsParser.kt +176 -0
- package/android/src/main/java/com/cnhongwei/reactnativemqtt/MqttSession.kt +1006 -0
- package/android/src/main/java/com/cnhongwei/reactnativemqtt/PahoLogger.java +44 -0
- package/android/src/main/java/com/cnhongwei/reactnativemqtt/PahoMqttTransport.kt +399 -0
- package/android/src/main/java/com/cnhongwei/reactnativemqtt/ReactNativeMqttModule.kt +619 -0
- package/android/src/main/java/com/cnhongwei/reactnativemqtt/ReactNativeMqttPackage.kt +31 -0
- package/ios/ReactNativeMqtt.h +15 -0
- package/ios/ReactNativeMqtt.mm +1184 -0
- package/ios/ReactNativeMqttCocoaTransport.swift +782 -0
- package/ios/ReactNativeMqttConnAckProbe.swift +87 -0
- package/ios/ReactNativeMqttSession.swift +420 -0
- package/lib/module/NativeReactNativeMqtt.js +5 -0
- package/lib/module/NativeReactNativeMqtt.js.map +1 -0
- package/lib/module/client.js +555 -0
- package/lib/module/client.js.map +1 -0
- package/lib/module/errors.js +20 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/helpers.js +116 -0
- package/lib/module/helpers.js.map +1 -0
- package/lib/module/index.js +8 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/validation.js +306 -0
- package/lib/module/validation.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeReactNativeMqtt.d.ts +154 -0
- package/lib/typescript/src/NativeReactNativeMqtt.d.ts.map +1 -0
- package/lib/typescript/src/client.d.ts +3 -0
- package/lib/typescript/src/client.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +18 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/helpers.d.ts +5 -0
- package/lib/typescript/src/helpers.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +9 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +161 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/validation.d.ts +13 -0
- package/lib/typescript/src/validation.d.ts.map +1 -0
- package/package.json +188 -4
- package/react-native.config.js +1 -0
- package/src/NativeReactNativeMqtt.ts +173 -0
- package/src/client.ts +859 -0
- package/src/errors.ts +51 -0
- package/src/helpers.ts +151 -0
- package/src/index.ts +30 -0
- package/src/types.ts +184 -0
- package/src/validation.ts +735 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import CocoaMQTT
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/// Captures the MQTT CONNACK flags at the public socket boundary.
|
|
5
|
+
/// CocoaMQTT 2.4.0 keeps this bit inside its private FrameConnAck, so the
|
|
6
|
+
/// adapter observes the already-framed header/payload callbacks and forwards
|
|
7
|
+
/// every callback unchanged to CocoaMQTT.
|
|
8
|
+
final class ReactNativeMqttConnAckProbe: NSObject, CocoaMQTTSocketProtocol, CocoaMQTTSocketDelegate {
|
|
9
|
+
private var socket: CocoaMQTTSocketProtocol
|
|
10
|
+
private weak var downstream: CocoaMQTTSocketDelegate?
|
|
11
|
+
private var downstreamQueue: DispatchQueue?
|
|
12
|
+
private var isConnAck = false
|
|
13
|
+
private(set) var sessionPresent: Bool?
|
|
14
|
+
|
|
15
|
+
init(socket: CocoaMQTTSocketProtocol) {
|
|
16
|
+
self.socket = socket
|
|
17
|
+
super.init()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
var enableSSL: Bool {
|
|
21
|
+
get { socket.enableSSL }
|
|
22
|
+
set { socket.enableSSL = newValue }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func setDelegate(_ delegate: CocoaMQTTSocketDelegate?, delegateQueue: DispatchQueue?) {
|
|
26
|
+
downstream = delegate
|
|
27
|
+
downstreamQueue = delegateQueue
|
|
28
|
+
socket.setDelegate(self, delegateQueue: delegateQueue)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func connect(toHost host: String, onPort port: UInt16) throws {
|
|
32
|
+
try socket.connect(toHost: host, onPort: port)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func connect(toHost host: String, onPort port: UInt16, withTimeout timeout: TimeInterval) throws {
|
|
36
|
+
try socket.connect(toHost: host, onPort: port, withTimeout: timeout)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func disconnect() { socket.disconnect() }
|
|
40
|
+
|
|
41
|
+
func readData(toLength length: UInt, withTimeout timeout: TimeInterval, tag: Int) {
|
|
42
|
+
socket.readData(toLength: length, withTimeout: timeout, tag: tag)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func write(_ data: Data, withTimeout timeout: TimeInterval, tag: Int) {
|
|
46
|
+
socket.write(data, withTimeout: timeout, tag: tag)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func socketConnected(_ socket: CocoaMQTTSocketProtocol) {
|
|
50
|
+
forward { $0.socketConnected(self) }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func socket(_ socket: CocoaMQTTSocketProtocol, didReceive trust: SecTrust, completionHandler: @escaping (Bool) -> Void) {
|
|
54
|
+
forward { $0.socket(self, didReceive: trust, completionHandler: completionHandler) }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func socketUrlSession(_ socket: CocoaMQTTSocketProtocol, didReceiveTrust trust: SecTrust, didReceiveChallenge challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
|
58
|
+
forward { $0.socketUrlSession(self, didReceiveTrust: trust, didReceiveChallenge: challenge, completionHandler: completionHandler) }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
func socket(_ socket: CocoaMQTTSocketProtocol, didWriteDataWithTag tag: Int) {
|
|
62
|
+
forward { $0.socket(self, didWriteDataWithTag: tag) }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func socket(_ socket: CocoaMQTTSocketProtocol, didRead data: Data, withTag tag: Int) {
|
|
66
|
+
if tag == 0, let header = data.first {
|
|
67
|
+
isConnAck = header >> 4 == 2
|
|
68
|
+
} else if tag == 2, isConnAck, let flags = data.first {
|
|
69
|
+
sessionPresent = flags & 1 == 1
|
|
70
|
+
isConnAck = false
|
|
71
|
+
}
|
|
72
|
+
forward { $0.socket(self, didRead: data, withTag: tag) }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func socketDidDisconnect(_ socket: CocoaMQTTSocketProtocol, withError error: Error?) {
|
|
76
|
+
forward { $0.socketDidDisconnect(self, withError: error) }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private func forward(_ body: @escaping (CocoaMQTTSocketDelegate) -> Void) {
|
|
80
|
+
guard let downstream else { return }
|
|
81
|
+
if let downstreamQueue {
|
|
82
|
+
downstreamQueue.async { body(downstream) }
|
|
83
|
+
} else {
|
|
84
|
+
body(downstream)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum ReactNativeMqttSessionState: String {
|
|
4
|
+
case idle
|
|
5
|
+
case connecting
|
|
6
|
+
case connected
|
|
7
|
+
case reconnecting
|
|
8
|
+
case disconnecting
|
|
9
|
+
case disconnected
|
|
10
|
+
case closed
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
struct ReactNativeMqttSessionSnapshot {
|
|
14
|
+
let state: ReactNativeMqttSessionState
|
|
15
|
+
let revision: UInt64
|
|
16
|
+
let generation: UInt64
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
protocol ReactNativeMqttSessionTransport: AnyObject {
|
|
20
|
+
func connect(completion: @escaping (Result<Any, Error>) -> Void)
|
|
21
|
+
func disconnect(completion: @escaping (Error?) -> Void)
|
|
22
|
+
func setDisconnectedHandler(_ handler: @escaping (Error?) -> Void)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Owns one transport lifecycle. All mutable state is accessed on `queue`.
|
|
26
|
+
/// Transport callbacks may arrive on arbitrary SDK queues and are immediately
|
|
27
|
+
/// re-enqueued, so stale generations cannot mutate a replacement connection.
|
|
28
|
+
final class ReactNativeMqttSession {
|
|
29
|
+
typealias SnapshotHandler = (ReactNativeMqttSessionSnapshot) -> Void
|
|
30
|
+
typealias ConnectHandler = (ReactNativeMqttSessionSnapshot, Any) -> Void
|
|
31
|
+
|
|
32
|
+
private let queue: DispatchQueue
|
|
33
|
+
private let transport: ReactNativeMqttSessionTransport
|
|
34
|
+
private let reconnectEnabled: Bool
|
|
35
|
+
private let reconnectInitialDelayMs: UInt64
|
|
36
|
+
private let reconnectMaxDelayMs: UInt64
|
|
37
|
+
private let reconnectJitterRatio: Double
|
|
38
|
+
private let connectTimeoutMs: UInt64
|
|
39
|
+
private let disconnectTimeoutMs: UInt64
|
|
40
|
+
private(set) var snapshot: ReactNativeMqttSessionSnapshot
|
|
41
|
+
private var connectWaiters: [(Result<Any, Error>) -> Void] = []
|
|
42
|
+
private var disconnectWaiters: [(Error?) -> Void] = []
|
|
43
|
+
private var reconnectWorkItem: DispatchWorkItem?
|
|
44
|
+
private var disconnectWorkItem: DispatchWorkItem?
|
|
45
|
+
private var reconnectAttempts: UInt64 = 0
|
|
46
|
+
private var lastConnectValue: Any?
|
|
47
|
+
private var userStopRequested = false
|
|
48
|
+
private var disposing = false
|
|
49
|
+
private var disposed = false
|
|
50
|
+
private let disconnectHandler: ((Error?) -> Void)?
|
|
51
|
+
private var snapshotHandler: SnapshotHandler?
|
|
52
|
+
private var connectHandler: ConnectHandler?
|
|
53
|
+
|
|
54
|
+
init(
|
|
55
|
+
transport: ReactNativeMqttSessionTransport,
|
|
56
|
+
queueLabel: String,
|
|
57
|
+
reconnectEnabled: Bool = true,
|
|
58
|
+
reconnectInitialDelayMs: UInt64 = 1_000,
|
|
59
|
+
reconnectMaxDelayMs: UInt64 = 30_000,
|
|
60
|
+
reconnectJitterRatio: Double = 0.2,
|
|
61
|
+
connectTimeoutMs: UInt64 = 30_000,
|
|
62
|
+
disconnectTimeoutMs: UInt64 = 5_000,
|
|
63
|
+
disconnectHandler: ((Error?) -> Void)? = nil,
|
|
64
|
+
snapshotHandler: SnapshotHandler? = nil
|
|
65
|
+
) {
|
|
66
|
+
precondition(reconnectInitialDelayMs > 0 && reconnectMaxDelayMs >= reconnectInitialDelayMs)
|
|
67
|
+
precondition((0...1).contains(reconnectJitterRatio))
|
|
68
|
+
precondition(connectTimeoutMs > 0 && disconnectTimeoutMs > 0)
|
|
69
|
+
self.queue = DispatchQueue(label: queueLabel)
|
|
70
|
+
self.transport = transport
|
|
71
|
+
self.reconnectEnabled = reconnectEnabled
|
|
72
|
+
self.reconnectInitialDelayMs = reconnectInitialDelayMs
|
|
73
|
+
self.reconnectMaxDelayMs = reconnectMaxDelayMs
|
|
74
|
+
self.reconnectJitterRatio = reconnectJitterRatio
|
|
75
|
+
self.connectTimeoutMs = connectTimeoutMs
|
|
76
|
+
self.disconnectTimeoutMs = disconnectTimeoutMs
|
|
77
|
+
self.disconnectHandler = disconnectHandler
|
|
78
|
+
self.snapshot = ReactNativeMqttSessionSnapshot(state: .idle, revision: 0, generation: 0)
|
|
79
|
+
self.snapshotHandler = snapshotHandler
|
|
80
|
+
transport.setDisconnectedHandler { [weak self] error in
|
|
81
|
+
self?.queue.async { self?.transportDisconnected(error) }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func connect(completion: @escaping (Result<Any, Error>) -> Void) {
|
|
86
|
+
queue.async { [weak self] in self?.connectOnQueue(completion) }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func disconnect(completion: @escaping (Error?) -> Void) {
|
|
90
|
+
queue.async { [weak self] in self?.disconnectOnQueue(completion) }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func dispose(completion: @escaping (Error?) -> Void) {
|
|
94
|
+
queue.async { [weak self] in self?.disposeOnQueue(completion) }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func setSnapshotHandler(_ handler: SnapshotHandler?) {
|
|
98
|
+
queue.async { [weak self] in self?.snapshotHandler = handler }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
func setConnectHandler(_ handler: ConnectHandler?) {
|
|
102
|
+
queue.async { [weak self] in self?.connectHandler = handler }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private func connectOnQueue(_ completion: @escaping (Result<Any, Error>) -> Void) {
|
|
106
|
+
guard !disposing && !disposed else {
|
|
107
|
+
completion(.failure(ReactNativeMqttSessionError.disposed))
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
userStopRequested = false
|
|
111
|
+
if snapshot.state == .connected {
|
|
112
|
+
if let lastConnectValue {
|
|
113
|
+
completion(.success(lastConnectValue))
|
|
114
|
+
} else {
|
|
115
|
+
completion(.failure(ReactNativeMqttSessionError.connectTimedOut))
|
|
116
|
+
}
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
connectWaiters.append(completion)
|
|
120
|
+
guard snapshot.state != .connecting && snapshot.state != .reconnecting else { return }
|
|
121
|
+
reconnectWorkItem?.cancel()
|
|
122
|
+
reconnectWorkItem = nil
|
|
123
|
+
beginConnectOnQueue()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private func beginConnectOnQueue() {
|
|
127
|
+
guard !disposing && !disposed else { return }
|
|
128
|
+
let generation = snapshot.generation &+ 1
|
|
129
|
+
setStateOnQueue(.connecting, generation: generation)
|
|
130
|
+
var completed = false
|
|
131
|
+
let finish: (Result<Any, Error>) -> Void = { [weak self] result in
|
|
132
|
+
self?.queue.async {
|
|
133
|
+
guard let self, !completed else { return }
|
|
134
|
+
completed = true
|
|
135
|
+
self.finishConnectOnQueue(result, generation: generation)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
transport.connect(completion: finish)
|
|
139
|
+
let timeout = DispatchWorkItem { [weak self] in
|
|
140
|
+
guard let self else { return }
|
|
141
|
+
self.queue.async {
|
|
142
|
+
guard !completed, self.snapshot.generation == generation, self.snapshot.state == .connecting else { return }
|
|
143
|
+
completed = true
|
|
144
|
+
self.transport.disconnect { _ in }
|
|
145
|
+
self.finishConnectOnQueue(.failure(ReactNativeMqttSessionError.connectTimedOut), generation: generation)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
queue.asyncAfter(deadline: .now() + .milliseconds(Int(connectTimeoutMs)), execute: timeout)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private func finishConnectOnQueue(_ result: Result<Any, Error>, generation: UInt64) {
|
|
152
|
+
guard !disposing && !disposed && snapshot.generation == generation else { return }
|
|
153
|
+
switch result {
|
|
154
|
+
case .success(let value):
|
|
155
|
+
lastConnectValue = value
|
|
156
|
+
reconnectAttempts = 0
|
|
157
|
+
setStateOnQueue(.connected, generation: generation)
|
|
158
|
+
connectHandler?(snapshot, value)
|
|
159
|
+
let waiters = connectWaiters
|
|
160
|
+
connectWaiters.removeAll()
|
|
161
|
+
waiters.forEach { $0(result) }
|
|
162
|
+
case .failure(let error):
|
|
163
|
+
setStateOnQueue(.disconnected, generation: generation)
|
|
164
|
+
let waiters = connectWaiters
|
|
165
|
+
connectWaiters.removeAll()
|
|
166
|
+
waiters.forEach { $0(.failure(error)) }
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private func transportDisconnected(_ error: Error?) {
|
|
171
|
+
guard !disposing && !disposed else { return }
|
|
172
|
+
guard snapshot.state == .connected else { return }
|
|
173
|
+
setStateOnQueue(.reconnecting, generation: snapshot.generation)
|
|
174
|
+
if reconnectEnabled && !userStopRequested {
|
|
175
|
+
scheduleReconnectOnQueue()
|
|
176
|
+
} else {
|
|
177
|
+
setStateOnQueue(.disconnected, generation: snapshot.generation)
|
|
178
|
+
}
|
|
179
|
+
disconnectHandler?(error)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private func scheduleReconnectOnQueue() {
|
|
183
|
+
reconnectAttempts &+= 1
|
|
184
|
+
let exponent = min(reconnectAttempts - 1, 31)
|
|
185
|
+
let base = min(reconnectMaxDelayMs, reconnectInitialDelayMs << exponent)
|
|
186
|
+
let jitter = Double(base) * reconnectJitterRatio
|
|
187
|
+
let random = Double.random(in: -jitter...jitter)
|
|
188
|
+
let delay = UInt64(max(0, Double(base) + random))
|
|
189
|
+
let work = DispatchWorkItem { [weak self] in
|
|
190
|
+
guard let self else { return }
|
|
191
|
+
self.queue.async {
|
|
192
|
+
guard !self.userStopRequested, !self.disposing, !self.disposed, self.snapshot.state == .reconnecting else { return }
|
|
193
|
+
self.beginConnectOnQueue()
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
reconnectWorkItem = work
|
|
197
|
+
queue.asyncAfter(deadline: .now() + .milliseconds(Int(delay)), execute: work)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private func disconnectOnQueue(_ completion: @escaping (Error?) -> Void) {
|
|
201
|
+
userStopRequested = true
|
|
202
|
+
reconnectWorkItem?.cancel()
|
|
203
|
+
reconnectWorkItem = nil
|
|
204
|
+
connectWaiters.forEach { $0(.failure(ReactNativeMqttSessionError.cancelled)) }
|
|
205
|
+
connectWaiters.removeAll()
|
|
206
|
+
guard snapshot.state != .connected && snapshot.state != .connecting && snapshot.state != .reconnecting else {
|
|
207
|
+
disconnectWaiters.append(completion)
|
|
208
|
+
if snapshot.state != .disconnecting { beginDisconnectOnQueue() }
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
setStateOnQueue(.disconnected, generation: snapshot.generation)
|
|
212
|
+
completion(nil)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private func beginDisconnectOnQueue() {
|
|
216
|
+
let generation = snapshot.generation
|
|
217
|
+
setStateOnQueue(.disconnecting, generation: generation)
|
|
218
|
+
var finished = false
|
|
219
|
+
let finish: (Error?) -> Void = { [weak self] error in
|
|
220
|
+
self?.queue.async {
|
|
221
|
+
guard let self, !finished, self.snapshot.generation == generation else { return }
|
|
222
|
+
finished = true
|
|
223
|
+
self.disconnectWorkItem?.cancel()
|
|
224
|
+
self.disconnectWorkItem = nil
|
|
225
|
+
self.setStateOnQueue(.disconnected, generation: generation)
|
|
226
|
+
let waiters = self.disconnectWaiters
|
|
227
|
+
self.disconnectWaiters.removeAll()
|
|
228
|
+
waiters.forEach { $0(error) }
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
transport.disconnect(completion: finish)
|
|
232
|
+
let timeout = DispatchWorkItem { [weak self] in
|
|
233
|
+
guard let self else { return }
|
|
234
|
+
self.queue.async {
|
|
235
|
+
guard !finished, self.snapshot.generation == generation else { return }
|
|
236
|
+
finished = true
|
|
237
|
+
self.transport.disconnect { _ in }
|
|
238
|
+
self.setStateOnQueue(.disconnected, generation: generation)
|
|
239
|
+
let waiters = self.disconnectWaiters
|
|
240
|
+
self.disconnectWaiters.removeAll()
|
|
241
|
+
waiters.forEach { $0(ReactNativeMqttSessionError.disconnectTimedOut) }
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
disconnectWorkItem = timeout
|
|
245
|
+
queue.asyncAfter(deadline: .now() + .milliseconds(Int(disconnectTimeoutMs)), execute: timeout)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private func disposeOnQueue(_ completion: @escaping (Error?) -> Void) {
|
|
249
|
+
if disposed {
|
|
250
|
+
completion(nil)
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
disposing = true
|
|
254
|
+
userStopRequested = true
|
|
255
|
+
reconnectWorkItem?.cancel()
|
|
256
|
+
reconnectWorkItem = nil
|
|
257
|
+
connectWaiters.forEach { $0(.failure(ReactNativeMqttSessionError.disposed)) }
|
|
258
|
+
connectWaiters.removeAll()
|
|
259
|
+
if snapshot.state == .connected || snapshot.state == .connecting || snapshot.state == .reconnecting {
|
|
260
|
+
disconnectWaiters.append { [weak self] error in self?.closeOnQueue(error, completion: completion) }
|
|
261
|
+
if snapshot.state != .disconnecting { beginDisconnectOnQueue() }
|
|
262
|
+
} else {
|
|
263
|
+
closeOnQueue(nil, completion: completion)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private func closeOnQueue(_ error: Error?, completion: @escaping (Error?) -> Void) {
|
|
268
|
+
guard !disposed else { completion(nil); return }
|
|
269
|
+
disposed = true
|
|
270
|
+
setStateOnQueue(.closed, generation: snapshot.generation)
|
|
271
|
+
completion(error)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private func setStateOnQueue(_ state: ReactNativeMqttSessionState, generation: UInt64) {
|
|
275
|
+
snapshot = ReactNativeMqttSessionSnapshot(state: state, revision: snapshot.revision &+ 1, generation: generation)
|
|
276
|
+
snapshotHandler?(snapshot)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
enum ReactNativeMqttSessionError: Error, Equatable {
|
|
281
|
+
case disposed
|
|
282
|
+
case cancelled
|
|
283
|
+
case connectTimedOut
|
|
284
|
+
case operationTimedOut
|
|
285
|
+
case disconnectTimedOut
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
struct ReactNativeMqttOperationToken: Equatable {
|
|
289
|
+
let operationId: UInt64
|
|
290
|
+
let generation: UInt64
|
|
291
|
+
let topic: String?
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/// Serial operation ledger for SDK callbacks that do not carry an operation
|
|
295
|
+
/// identity. Requests sharing a topic are serialized; different topics may
|
|
296
|
+
/// proceed independently. Every request has one settlement gate and one
|
|
297
|
+
/// monotonic deadline, so late ACKs and timeout races are harmless.
|
|
298
|
+
final class ReactNativeMqttOperationLedger {
|
|
299
|
+
typealias ResultValue = Result<Any, Error>
|
|
300
|
+
typealias Completion = (ResultValue) -> Void
|
|
301
|
+
typealias Executor = (@escaping Completion) -> Void
|
|
302
|
+
|
|
303
|
+
private struct Request {
|
|
304
|
+
let token: ReactNativeMqttOperationToken
|
|
305
|
+
let deadline: DispatchTime
|
|
306
|
+
let execute: Executor
|
|
307
|
+
let completion: Completion
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private let queue: DispatchQueue
|
|
311
|
+
private var nextOperationId: UInt64 = 0
|
|
312
|
+
private var queues: [String: [Request]] = [:]
|
|
313
|
+
private var active: [String: ReactNativeMqttOperationToken] = [:]
|
|
314
|
+
private var settled: Set<UInt64> = []
|
|
315
|
+
|
|
316
|
+
init(queueLabel: String) {
|
|
317
|
+
queue = DispatchQueue(label: queueLabel)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
func submit(
|
|
321
|
+
topic: String?,
|
|
322
|
+
generation: UInt64,
|
|
323
|
+
timeoutMs: UInt64,
|
|
324
|
+
execute: @escaping Executor,
|
|
325
|
+
completion: @escaping Completion
|
|
326
|
+
) {
|
|
327
|
+
let key = topic ?? "__untagged_\(UUID().uuidString)"
|
|
328
|
+
queue.async {
|
|
329
|
+
self.nextOperationId &+= 1
|
|
330
|
+
let token = ReactNativeMqttOperationToken(operationId: self.nextOperationId, generation: generation, topic: topic)
|
|
331
|
+
let request = Request(token: token, deadline: .now() + .milliseconds(Int(timeoutMs)), execute: execute, completion: completion)
|
|
332
|
+
self.queues[key, default: []].append(request)
|
|
333
|
+
self.startNextIfNeeded(key)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private func startNextIfNeeded(_ key: String) {
|
|
338
|
+
guard active[key] == nil, let request = queues[key]?.first else { return }
|
|
339
|
+
active[key] = request.token
|
|
340
|
+
var finished = false
|
|
341
|
+
let finish: Completion = { [weak self] result in
|
|
342
|
+
self?.queue.async {
|
|
343
|
+
guard let self, !finished else { return }
|
|
344
|
+
finished = true
|
|
345
|
+
self.settle(request, key: key, result: result)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
request.execute(finish)
|
|
349
|
+
let timeout = DispatchWorkItem { [weak self] in
|
|
350
|
+
guard let self else { return }
|
|
351
|
+
self.queue.async {
|
|
352
|
+
guard !finished else { return }
|
|
353
|
+
finished = true
|
|
354
|
+
self.settle(request, key: key, result: .failure(ReactNativeMqttSessionError.operationTimedOut))
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
queue.asyncAfter(deadline: request.deadline, execute: timeout)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private func settle(_ request: Request, key: String, result: ResultValue) {
|
|
361
|
+
guard settled.insert(request.token.operationId).inserted else { return }
|
|
362
|
+
request.completion(result)
|
|
363
|
+
if queues[key]?.isEmpty == false { queues[key]?.removeFirst() }
|
|
364
|
+
active.removeValue(forKey: key)
|
|
365
|
+
if queues[key]?.isEmpty == true { queues.removeValue(forKey: key) }
|
|
366
|
+
else { startNextIfNeeded(key) }
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/// Serial ownership for all native sessions. `invalidate` is a terminal epoch
|
|
371
|
+
/// barrier: after it starts no later open can create a session.
|
|
372
|
+
final class ReactNativeMqttSessionRegistry {
|
|
373
|
+
private let queue = DispatchQueue(label: "com.cnhongwei.reactnativemqtt.session-registry")
|
|
374
|
+
private var sessions: [String: ReactNativeMqttSession] = [:]
|
|
375
|
+
private(set) var epoch: UInt64 = 0
|
|
376
|
+
private var invalidated = false
|
|
377
|
+
|
|
378
|
+
func open(_ instanceId: String, make: @escaping () -> ReactNativeMqttSession, completion: @escaping (Result<ReactNativeMqttSession, Error>) -> Void) {
|
|
379
|
+
queue.async {
|
|
380
|
+
guard !self.invalidated else {
|
|
381
|
+
completion(.failure(ReactNativeMqttSessionError.disposed))
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
if let existing = self.sessions[instanceId] {
|
|
385
|
+
completion(.success(existing))
|
|
386
|
+
return
|
|
387
|
+
}
|
|
388
|
+
let session = make()
|
|
389
|
+
self.sessions[instanceId] = session
|
|
390
|
+
completion(.success(session))
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
func remove(_ instanceId: String, completion: @escaping () -> Void) {
|
|
395
|
+
queue.async {
|
|
396
|
+
guard let session = self.sessions.removeValue(forKey: instanceId) else { completion(); return }
|
|
397
|
+
session.dispose { _ in completion() }
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
func invalidate(completion: @escaping () -> Void) {
|
|
402
|
+
queue.async {
|
|
403
|
+
self.invalidated = true
|
|
404
|
+
self.epoch &+= 1
|
|
405
|
+
let sessions = Array(self.sessions.values)
|
|
406
|
+
self.sessions.removeAll()
|
|
407
|
+
guard !sessions.isEmpty else { completion(); return }
|
|
408
|
+
func disposeNext(_ index: Int) {
|
|
409
|
+
guard index < sessions.count else {
|
|
410
|
+
DispatchQueue.global(qos: .userInitiated).async(execute: completion)
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
sessions[index].dispose { _ in
|
|
414
|
+
self.queue.async { disposeNext(index + 1) }
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
disposeNext(0)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["TurboModuleRegistry","get"],"sourceRoot":"../../src","sources":["NativeReactNativeMqtt.ts"],"mappings":";;AAAA,SACEA,mBAAmB,QAGd,cAAc;AAwKrB,eAAeA,mBAAmB,CAACC,GAAG,CAAO,iBAAiB,CAAC","ignoreList":[]}
|