@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,1184 @@
|
|
|
1
|
+
#import "ReactNativeMqtt.h"
|
|
2
|
+
#import "ReactNativeMqtt-Swift.h"
|
|
3
|
+
#include <cmath>
|
|
4
|
+
|
|
5
|
+
static BOOL RNMqttValidMqttString(NSString *value, BOOL allowEmpty) {
|
|
6
|
+
if (value == nil || (!allowEmpty && value.length == 0) || [value rangeOfString:@"\0"].location != NSNotFound) return NO;
|
|
7
|
+
NSData *encoded = [value dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:NO];
|
|
8
|
+
return encoded != nil && encoded.length <= 65535;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static BOOL RNMqttValidTopicName(NSString *value) {
|
|
12
|
+
return RNMqttValidMqttString(value, NO) && [value rangeOfString:@"+"].location == NSNotFound && [value rangeOfString:@"#"].location == NSNotFound;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static BOOL RNMqttValidBase64(NSString *value) {
|
|
16
|
+
NSData *decoded = [[NSData alloc] initWithBase64EncodedString:value options:0];
|
|
17
|
+
return decoded != nil && [[decoded base64EncodedStringWithOptions:0] isEqualToString:value];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static BOOL RNMqttValidTopicFilter(NSString *value) {
|
|
21
|
+
if (!RNMqttValidMqttString(value, NO) || [value hasPrefix:@"$share/"]) return NO;
|
|
22
|
+
NSArray<NSString *> *levels = [value componentsSeparatedByString:@"/"];
|
|
23
|
+
for (NSUInteger index = 0; index < levels.count; index += 1) {
|
|
24
|
+
NSString *level = levels[index];
|
|
25
|
+
BOOL hasSingle = [level rangeOfString:@"+"].location != NSNotFound;
|
|
26
|
+
BOOL hasMulti = [level rangeOfString:@"#"].location != NSNotFound;
|
|
27
|
+
if ((hasSingle && ![level isEqualToString:@"+"]) || (hasMulti && (! [level isEqualToString:@"#"] || index + 1 != levels.count))) return NO;
|
|
28
|
+
}
|
|
29
|
+
return YES;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static NSError *RNMqttAnnotatedError(NSError *error, NSString *operation, NSNumber *generation, NSString *outcome) {
|
|
33
|
+
if (error == nil) return nil;
|
|
34
|
+
NSMutableDictionary *userInfo = [error.userInfo mutableCopy] ?: [NSMutableDictionary dictionary];
|
|
35
|
+
if (operation != nil && userInfo[@"operation"] == nil) userInfo[@"operation"] = operation;
|
|
36
|
+
if (generation != nil && userInfo[@"generation"] == nil) userInfo[@"generation"] = generation;
|
|
37
|
+
if (outcome != nil && userInfo[@"outcome"] == nil) userInfo[@"outcome"] = outcome;
|
|
38
|
+
return [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static NSArray<NSDictionary *> *RNMqttUserProperties(JS::NativeReactNativeMqtt::WireSessionOptions &options) {
|
|
42
|
+
NSMutableArray<NSDictionary *> *properties = [NSMutableArray array];
|
|
43
|
+
auto userProperties = options.userProperties();
|
|
44
|
+
if (userProperties.has_value()) {
|
|
45
|
+
for (const auto &property : *userProperties) {
|
|
46
|
+
[properties addObject:@{ @"key": property.key(), @"value": property.value() }];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return properties;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
typedef void (^RNMqttSubscriptionOperation)(dispatch_block_t finish);
|
|
53
|
+
|
|
54
|
+
@implementation ReactNativeMqtt
|
|
55
|
+
|
|
56
|
+
- (instancetype)init {
|
|
57
|
+
self = [super init];
|
|
58
|
+
if (self) {
|
|
59
|
+
_sessionQueue = dispatch_queue_create("com.cnhongwei.reactnativemqtt.sessions", DISPATCH_QUEUE_SERIAL);
|
|
60
|
+
_sessions = [NSMutableDictionary dictionary];
|
|
61
|
+
_iosBatchLedger = [NSMutableDictionary dictionary];
|
|
62
|
+
_iosStreamEpochs = [NSMutableDictionary dictionary];
|
|
63
|
+
_iosSubscriptionQueues = [NSMutableDictionary dictionary];
|
|
64
|
+
_iosBatchId = 0;
|
|
65
|
+
_nextOperationId = 0;
|
|
66
|
+
}
|
|
67
|
+
return self;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
- (NSMutableDictionary *)operationWaiterWithReject:(RCTPromiseRejectBlock)reject
|
|
71
|
+
operation:(NSString *)operation
|
|
72
|
+
topic:(NSString *)topic
|
|
73
|
+
generation:(NSNumber *)generation {
|
|
74
|
+
return [@{
|
|
75
|
+
@"reject": [reject copy],
|
|
76
|
+
@"settled": @NO,
|
|
77
|
+
@"operationId": @(++_nextOperationId),
|
|
78
|
+
@"operation": operation,
|
|
79
|
+
@"topic": topic ?: [NSNull null],
|
|
80
|
+
@"generation": generation ?: @0,
|
|
81
|
+
} mutableCopy];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
- (void)startNextSubscriptionOperationForInstance:(NSString *)instanceId filter:(NSString *)filter {
|
|
85
|
+
NSMutableDictionary *byFilter = self->_iosSubscriptionQueues[instanceId];
|
|
86
|
+
NSMutableArray *queue = byFilter[filter];
|
|
87
|
+
if (queue.count == 0) return;
|
|
88
|
+
RNMqttSubscriptionOperation operation = [queue.firstObject copy];
|
|
89
|
+
__block BOOL finished = NO;
|
|
90
|
+
dispatch_block_t finish = ^{
|
|
91
|
+
if (finished) return;
|
|
92
|
+
finished = YES;
|
|
93
|
+
dispatch_async(self->_sessionQueue, ^{
|
|
94
|
+
NSMutableDictionary *currentByFilter = self->_iosSubscriptionQueues[instanceId];
|
|
95
|
+
NSMutableArray *currentQueue = currentByFilter[filter];
|
|
96
|
+
if (currentQueue.count > 0) [currentQueue removeObjectAtIndex:0];
|
|
97
|
+
if (currentQueue.count == 0) {
|
|
98
|
+
[currentByFilter removeObjectForKey:filter];
|
|
99
|
+
} else {
|
|
100
|
+
[self startNextSubscriptionOperationForInstance:instanceId filter:filter];
|
|
101
|
+
}
|
|
102
|
+
if (currentByFilter.count == 0) [self->_iosSubscriptionQueues removeObjectForKey:instanceId];
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
operation(finish);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
- (void)enqueueSubscriptionOperation:(RNMqttSubscriptionOperation)operation instanceId:(NSString *)instanceId filter:(NSString *)filter {
|
|
109
|
+
NSMutableDictionary *byFilter = [self->_iosSubscriptionQueues[instanceId] mutableCopy] ?: [NSMutableDictionary dictionary];
|
|
110
|
+
NSMutableArray *queue = [byFilter[filter] mutableCopy] ?: [NSMutableArray array];
|
|
111
|
+
BOOL shouldStart = queue.count == 0;
|
|
112
|
+
[queue addObject:[operation copy]];
|
|
113
|
+
byFilter[filter] = queue;
|
|
114
|
+
self->_iosSubscriptionQueues[instanceId] = byFilter;
|
|
115
|
+
if (shouldStart) [self startNextSubscriptionOperationForInstance:instanceId filter:filter];
|
|
116
|
+
}
|
|
117
|
+
- (void)getCapabilities:(RCTPromiseResolveBlock)resolve
|
|
118
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
119
|
+
(void)reject;
|
|
120
|
+
resolve(@{
|
|
121
|
+
@"wireVersion": @1,
|
|
122
|
+
@"platform": @"ios",
|
|
123
|
+
@"protocolVersions": @[@4, @5],
|
|
124
|
+
@"transports": @[@"tcp", @"tls", @"ws", @"wss"],
|
|
125
|
+
// MQTT 5 exposes sessionPresent through the CocoaMQTT CONNACK probe;
|
|
126
|
+
// MQTT 3.1.1 keeps the protocol-defined unavailable/false value.
|
|
127
|
+
@"supportsSessionPresent": @YES,
|
|
128
|
+
@"supportsDuplicateUserProperties": @YES,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
- (NSString *)invalidSessionOptions:(JS::NativeReactNativeMqtt::WireSessionOptions &)options {
|
|
133
|
+
NSString *host = options.host();
|
|
134
|
+
if (host == nil || host.length == 0) return @"host must be a non-empty string";
|
|
135
|
+
const double port = options.port();
|
|
136
|
+
if (!std::isfinite(port) || std::floor(port) != port || port < 1 || port > 65535) return @"port must be an integer in 1..65535";
|
|
137
|
+
const NSInteger protocol = options.protocolVersion().value_or(4);
|
|
138
|
+
if (protocol != 4 && protocol != 5) return @"protocolVersion must be 4 or 5";
|
|
139
|
+
NSString *transport = options.transport();
|
|
140
|
+
if (transport == nil || (![transport isEqualToString:@"tcp"] && ![transport isEqualToString:@"tls"] && ![transport isEqualToString:@"ws"] && ![transport isEqualToString:@"wss"])) return @"transport is invalid";
|
|
141
|
+
NSString *path = options.path();
|
|
142
|
+
if (path != nil && ![transport isEqualToString:@"ws"] && ![transport isEqualToString:@"wss"]) return @"path is only valid for ws/wss";
|
|
143
|
+
if (path != nil && (path.length == 0 || ![path hasPrefix:@"/"])) return @"path must start with /";
|
|
144
|
+
if ((options.tlsCaBase64() != nil || options.tlsP12Base64() != nil || options.tlsP12Password() != nil || options.tlsInsecure().has_value()) && ![transport isEqualToString:@"tls"] && ![transport isEqualToString:@"wss"]) return @"tls options are only valid for tls/wss";
|
|
145
|
+
if (options.password() != nil && options.username() == nil) return @"password requires username";
|
|
146
|
+
if (options.username() != nil && !RNMqttValidMqttString(options.username(), YES)) return @"username is not valid MQTT UTF-8";
|
|
147
|
+
if (options.password() != nil && !RNMqttValidMqttString(options.password(), YES)) return @"password is not valid MQTT UTF-8";
|
|
148
|
+
if (options.clientId() != nil) {
|
|
149
|
+
if (!RNMqttValidMqttString(options.clientId(), YES)) return @"clientId is not valid MQTT UTF-8";
|
|
150
|
+
NSData *clientIdData = [options.clientId() dataUsingEncoding:NSUTF8StringEncoding];
|
|
151
|
+
if (clientIdData == nil || clientIdData.length > 65535) return @"clientId exceeds the MQTT UTF-8 string limit";
|
|
152
|
+
if (options.clientId().length == 0 && !(protocol == 5 && options.cleanStart().value_or(true))) return @"empty clientId requires MQTT5 cleanStart";
|
|
153
|
+
}
|
|
154
|
+
if (protocol == 4 && (options.cleanStart().has_value() || options.sessionExpiryIntervalSeconds().has_value() || options.receiveMaximum().has_value() || options.userProperties().has_value())) return @"MQTT5 options are not valid for protocolVersion 4";
|
|
155
|
+
if (protocol == 5 && options.cleanSession().has_value()) return @"cleanSession is not valid for protocolVersion 5";
|
|
156
|
+
NSString *willTopic = options.willTopic();
|
|
157
|
+
NSString *willPayload = options.willPayloadBase64();
|
|
158
|
+
if ((willTopic == nil) != (willPayload == nil)) return @"willTopic and willPayloadBase64 must be provided together";
|
|
159
|
+
if (willTopic != nil && !RNMqttValidTopicName(willTopic)) return @"willTopic is invalid";
|
|
160
|
+
if (willPayload != nil && !RNMqttValidBase64(willPayload)) return @"willPayloadBase64 is invalid";
|
|
161
|
+
if (options.willQos().has_value() && (*options.willQos() < 0 || *options.willQos() > 2 || std::floor(*options.willQos()) != *options.willQos())) return @"willQos must be an integer in 0..2";
|
|
162
|
+
auto connectionUserProperties = options.userProperties();
|
|
163
|
+
if (connectionUserProperties.has_value()) {
|
|
164
|
+
for (const auto &property : *connectionUserProperties) {
|
|
165
|
+
NSString *key = property.key();
|
|
166
|
+
NSString *value = property.value();
|
|
167
|
+
if (key == nil || value == nil || !RNMqttValidMqttString(key, YES) || !RNMqttValidMqttString(value, YES)) return @"userProperties entries must contain valid strings";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (options.keepAliveSeconds().has_value() && (*options.keepAliveSeconds() < 0 || *options.keepAliveSeconds() > 65535)) return @"keepAliveSeconds is out of range";
|
|
171
|
+
if (options.sessionExpiryIntervalSeconds().has_value() && (*options.sessionExpiryIntervalSeconds() < 0 || *options.sessionExpiryIntervalSeconds() > 4294967295)) return @"sessionExpiryIntervalSeconds is out of range";
|
|
172
|
+
if (options.receiveMaximum().has_value() && (*options.receiveMaximum() < 1 || *options.receiveMaximum() > 65535)) return @"receiveMaximum is out of range";
|
|
173
|
+
if (options.connectTimeoutMs().has_value() && *options.connectTimeoutMs() < 1) return @"connectTimeoutMs must be positive";
|
|
174
|
+
if (options.operationTimeoutMs().has_value() && *options.operationTimeoutMs() < 1) return @"operationTimeoutMs must be positive";
|
|
175
|
+
if (options.disconnectTimeoutMs().has_value() && *options.disconnectTimeoutMs() < 1) return @"disconnectTimeoutMs must be positive";
|
|
176
|
+
if (options.reconnectInitialDelayMs().has_value() && *options.reconnectInitialDelayMs() < 1) return @"reconnectInitialDelayMs must be positive";
|
|
177
|
+
if (options.reconnectMaxDelayMs().has_value() && *options.reconnectMaxDelayMs() < 1) return @"reconnectMaxDelayMs must be positive";
|
|
178
|
+
if (options.reconnectInitialDelayMs().has_value() && options.reconnectMaxDelayMs().has_value() && *options.reconnectMaxDelayMs() < *options.reconnectInitialDelayMs()) return @"reconnectMaxDelayMs must be >= reconnectInitialDelayMs";
|
|
179
|
+
if (options.reconnectJitterRatio().has_value() && (*options.reconnectJitterRatio() < 0 || *options.reconnectJitterRatio() > 1)) return @"reconnectJitterRatio must be in 0..1";
|
|
180
|
+
if (options.maxQueuedMessages().has_value() && *options.maxQueuedMessages() < 1) return @"maxQueuedMessages must be positive";
|
|
181
|
+
if (options.maxQueuedBytes().has_value() && *options.maxQueuedBytes() < 1) return @"maxQueuedBytes must be positive";
|
|
182
|
+
if (options.maxMessageBytes().has_value() && *options.maxMessageBytes() < 1) return @"maxMessageBytes must be positive";
|
|
183
|
+
if (options.maxMessageBytes().has_value() && options.maxQueuedBytes().has_value() && *options.maxMessageBytes() > *options.maxQueuedBytes()) return @"maxMessageBytes must be <= maxQueuedBytes";
|
|
184
|
+
if (options.batchMaxMessages().has_value() && *options.batchMaxMessages() < 1) return @"batchMaxMessages must be positive";
|
|
185
|
+
if (options.batchMaxBytes().has_value() && *options.batchMaxBytes() < 1) return @"batchMaxBytes must be positive";
|
|
186
|
+
if (options.batchAckTimeoutMs().has_value() && *options.batchAckTimeoutMs() < 1) return @"batchAckTimeoutMs must be positive";
|
|
187
|
+
if (options.overflowPolicy() != nil && ![options.overflowPolicy() isEqualToString:@"disconnect"] && ![options.overflowPolicy() isEqualToString:@"drop-newest"]) return @"overflowPolicy is invalid";
|
|
188
|
+
return nil;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
- (ReactNativeMqttCocoaBridge *)bridgeForSessionOptions:(NSDictionary *)options clientId:(NSString *)clientId {
|
|
192
|
+
return [[ReactNativeMqttCocoaBridge alloc]
|
|
193
|
+
initWithClientId:clientId
|
|
194
|
+
host:options[@"host"]
|
|
195
|
+
port:options[@"port"]
|
|
196
|
+
protocolVersion:options[@"protocolVersion"]
|
|
197
|
+
transport:options[@"transport"]
|
|
198
|
+
path:options[@"path"]
|
|
199
|
+
username:options[@"username"] == [NSNull null] ? nil : options[@"username"]
|
|
200
|
+
password:options[@"password"] == [NSNull null] ? nil : options[@"password"]
|
|
201
|
+
caBase64:options[@"tlsCaBase64"] == [NSNull null] ? nil : options[@"tlsCaBase64"]
|
|
202
|
+
p12Base64:options[@"tlsP12Base64"] == [NSNull null] ? nil : options[@"tlsP12Base64"]
|
|
203
|
+
p12Password:options[@"tlsP12Password"] == [NSNull null] ? nil : options[@"tlsP12Password"]
|
|
204
|
+
insecure:[options[@"tlsInsecure"] boolValue]
|
|
205
|
+
allowInvalidHostnameForTesting:[options[@"tlsAllowInvalidHostnameForTesting"] boolValue]
|
|
206
|
+
keepAlive:options[@"keepAliveSeconds"]
|
|
207
|
+
cleanStart:[options[@"cleanStart"] boolValue]
|
|
208
|
+
cleanSession:[options[@"cleanSession"] boolValue]
|
|
209
|
+
sessionExpiryInterval:options[@"sessionExpiryIntervalSeconds"]
|
|
210
|
+
receiveMaximum:options[@"receiveMaximum"]
|
|
211
|
+
userProperties:options[@"userProperties"]
|
|
212
|
+
willTopic:options[@"willTopic"] == [NSNull null] ? nil : options[@"willTopic"]
|
|
213
|
+
willPayloadBase64:options[@"willPayloadBase64"] == [NSNull null] ? nil : options[@"willPayloadBase64"]
|
|
214
|
+
willQos:options[@"willQos"]
|
|
215
|
+
willRetained:[options[@"willRetained"] boolValue]
|
|
216
|
+
reconnectEnabled:[options[@"reconnectEnabled"] boolValue]
|
|
217
|
+
reconnectInitialDelayMs:options[@"reconnectInitialDelayMs"]
|
|
218
|
+
reconnectMaxDelayMs:options[@"reconnectMaxDelayMs"]
|
|
219
|
+
reconnectJitterRatio:options[@"reconnectJitterRatio"]
|
|
220
|
+
connectTimeoutMs:options[@"connectTimeoutMs"]
|
|
221
|
+
disconnectTimeoutMs:options[@"disconnectTimeoutMs"]];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
- (NSDictionary *)snapshotForInstance:(NSString *)instanceId state:(NSString *)state revision:(NSNumber *)revision generation:(NSNumber *)generation lastConnAck:(id)lastConnAck deliveryStatus:(NSString *)deliveryStatus {
|
|
225
|
+
NSDictionary *record = self->_sessions[instanceId];
|
|
226
|
+
return @{
|
|
227
|
+
@"instanceId": instanceId,
|
|
228
|
+
@"revision": revision,
|
|
229
|
+
@"generation": generation,
|
|
230
|
+
@"state": state,
|
|
231
|
+
@"lastConnAck": lastConnAck ?: [NSNull null],
|
|
232
|
+
@"subscriptionsRevision": record[@"subscriptionsRevision"] ?: @0,
|
|
233
|
+
@"subscriptionsStatus": record[@"subscriptionsStatus"] ?: @"ready",
|
|
234
|
+
@"deliveryStatus": deliveryStatus,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
- (void)openSession:(NSString *)instanceId options:(JS::NativeReactNativeMqtt::WireSessionOptions &)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
239
|
+
NSString *invalid = [self invalidSessionOptions:options];
|
|
240
|
+
if (invalid != nil) {
|
|
241
|
+
reject(@"ERR_INVALID_OPTIONS", invalid, nil);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
NSArray<NSDictionary *> *userProperties = RNMqttUserProperties(options);
|
|
245
|
+
NSDictionary *sessionOptions = @{
|
|
246
|
+
@"host": options.host(),
|
|
247
|
+
@"port": @(options.port()),
|
|
248
|
+
@"protocolVersion": options.protocolVersion().has_value() ? @(*options.protocolVersion()) : @4,
|
|
249
|
+
@"transport": options.transport(),
|
|
250
|
+
@"path": options.path() ?: @"/mqtt",
|
|
251
|
+
@"clientId": options.clientId() ?: [NSNull null],
|
|
252
|
+
@"username": options.username() ?: [NSNull null],
|
|
253
|
+
@"password": options.password() ?: [NSNull null],
|
|
254
|
+
@"tlsCaBase64": options.tlsCaBase64() ?: [NSNull null],
|
|
255
|
+
@"tlsP12Base64": options.tlsP12Base64() ?: [NSNull null],
|
|
256
|
+
@"tlsP12Password": options.tlsP12Password() ?: [NSNull null],
|
|
257
|
+
@"tlsInsecure": @(options.tlsInsecure().value_or(false)),
|
|
258
|
+
@"tlsAllowInvalidHostnameForTesting": @(options.tlsAllowInvalidHostnameForTesting().value_or(false)),
|
|
259
|
+
@"keepAliveSeconds": @(options.keepAliveSeconds().value_or(60)),
|
|
260
|
+
@"cleanStart": @(options.cleanStart().value_or(true)),
|
|
261
|
+
@"cleanSession": @(options.cleanSession().value_or(true)),
|
|
262
|
+
@"sessionExpiryIntervalSeconds": @(options.sessionExpiryIntervalSeconds().value_or(0)),
|
|
263
|
+
@"receiveMaximum": @(options.receiveMaximum().value_or(65535)),
|
|
264
|
+
@"userProperties": userProperties,
|
|
265
|
+
@"willTopic": options.willTopic() ?: [NSNull null],
|
|
266
|
+
@"willPayloadBase64": options.willPayloadBase64() ?: [NSNull null],
|
|
267
|
+
@"willQos": @(options.willQos().value_or(0)),
|
|
268
|
+
@"willRetained": @(options.willRetained().value_or(false)),
|
|
269
|
+
@"maxQueuedMessages": @(options.maxQueuedMessages().value_or(1024)),
|
|
270
|
+
@"maxQueuedBytes": @(options.maxQueuedBytes().value_or(8 * 1024 * 1024)),
|
|
271
|
+
@"maxMessageBytes": @(options.maxMessageBytes().value_or(1024 * 1024)),
|
|
272
|
+
@"batchMaxMessages": @(options.batchMaxMessages().value_or(64)),
|
|
273
|
+
@"batchMaxBytes": @(options.batchMaxBytes().value_or(256 * 1024)),
|
|
274
|
+
@"connectTimeoutMs": @(options.connectTimeoutMs().value_or(30000)),
|
|
275
|
+
@"operationTimeoutMs": @(options.operationTimeoutMs().value_or(30000)),
|
|
276
|
+
@"disconnectTimeoutMs": @(options.disconnectTimeoutMs().value_or(5000)),
|
|
277
|
+
@"reconnectEnabled": @(options.reconnectEnabled().value_or(true)),
|
|
278
|
+
@"reconnectInitialDelayMs": @(options.reconnectInitialDelayMs().value_or(1000)),
|
|
279
|
+
@"reconnectMaxDelayMs": @(options.reconnectMaxDelayMs().value_or(30000)),
|
|
280
|
+
@"reconnectJitterRatio": @(options.reconnectJitterRatio().value_or(0.2)),
|
|
281
|
+
@"overflowPolicy": options.overflowPolicy() ?: @"disconnect",
|
|
282
|
+
};
|
|
283
|
+
NSString *clientId = options.clientId() ?: instanceId;
|
|
284
|
+
dispatch_async(_sessionQueue, ^{
|
|
285
|
+
if (self->_invalidated) {
|
|
286
|
+
reject(@"ERR_DISPOSED", @"Native module is invalidated", nil);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
NSMutableDictionary *existing = [self->_sessions[instanceId] mutableCopy];
|
|
290
|
+
if (existing != nil && ![existing[@"options"] isEqual:sessionOptions]) {
|
|
291
|
+
reject(@"ERR_OPERATION_IN_PROGRESS", @"Session options cannot change", nil);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (existing == nil) {
|
|
295
|
+
NSNumber *streamEpoch = @([self->_iosStreamEpochs[instanceId] unsignedLongLongValue] + 1);
|
|
296
|
+
self->_iosStreamEpochs[instanceId] = streamEpoch;
|
|
297
|
+
existing = [@{
|
|
298
|
+
@"options": sessionOptions,
|
|
299
|
+
@"clientId": clientId,
|
|
300
|
+
@"messageQueue": [NSMutableArray array],
|
|
301
|
+
@"overflowDiagnosticPending": @NO,
|
|
302
|
+
@"revision": @0,
|
|
303
|
+
@"generation": @0,
|
|
304
|
+
@"state": @"idle",
|
|
305
|
+
@"lastConnAck": [NSNull null],
|
|
306
|
+
@"subscriptionsRevision": @0,
|
|
307
|
+
@"subscriptionsStatus": @"ready",
|
|
308
|
+
@"pendingSubscriptionOperations": @0,
|
|
309
|
+
@"droppedMessages": @0,
|
|
310
|
+
@"desiredSubscriptions": [NSMutableDictionary dictionary],
|
|
311
|
+
@"confirmedSubscriptions": [NSMutableDictionary dictionary],
|
|
312
|
+
@"tombstones": [NSMutableDictionary dictionary],
|
|
313
|
+
@"subscriptionWaiters": [NSMutableArray array],
|
|
314
|
+
@"connectWaiters": [NSMutableArray array],
|
|
315
|
+
@"operationWaiters": [NSMutableArray array],
|
|
316
|
+
@"reconnectAttempts": @0,
|
|
317
|
+
@"reconnectToken": @0,
|
|
318
|
+
@"streamEpoch": streamEpoch,
|
|
319
|
+
} mutableCopy];
|
|
320
|
+
existing[@"bridge"] = [self bridgeForSessionOptions:sessionOptions clientId:clientId];
|
|
321
|
+
self->_sessions[instanceId] = existing;
|
|
322
|
+
}
|
|
323
|
+
resolve([self snapshotForInstance:instanceId state:existing[@"state"] revision:existing[@"revision"] generation:existing[@"generation"] lastConnAck:existing[@"lastConnAck"] deliveryStatus:@"active"]);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
- (void)emitNextMessageBatchForInstance:(NSString *)instanceId {
|
|
328
|
+
NSMutableDictionary *record = [self->_sessions[instanceId] mutableCopy];
|
|
329
|
+
if (record == nil || self->_iosBatchLedger[instanceId] != nil) return;
|
|
330
|
+
NSMutableArray *queue = [record[@"messageQueue"] mutableCopy];
|
|
331
|
+
if (queue == nil || queue.count == 0) return;
|
|
332
|
+
NSDictionary *options = record[@"options"];
|
|
333
|
+
NSUInteger maxMessages = [options[@"batchMaxMessages"] unsignedIntegerValue] ?: 64;
|
|
334
|
+
NSUInteger maxBytes = [options[@"batchMaxBytes"] unsignedIntegerValue] ?: (256 * 1024);
|
|
335
|
+
NSMutableArray *messages = [NSMutableArray array];
|
|
336
|
+
NSUInteger bytes = 0;
|
|
337
|
+
while (queue.count > 0 && messages.count < maxMessages) {
|
|
338
|
+
NSDictionary *candidate = queue.firstObject;
|
|
339
|
+
NSUInteger candidateBytes = [candidate[@"payloadBytes"] unsignedIntegerValue];
|
|
340
|
+
if (messages.count > 0 && bytes + candidateBytes > maxBytes) break;
|
|
341
|
+
[messages addObject:candidate];
|
|
342
|
+
[queue removeObjectAtIndex:0];
|
|
343
|
+
bytes += candidateBytes;
|
|
344
|
+
}
|
|
345
|
+
if (messages.count == 0) return;
|
|
346
|
+
self->_iosBatchId += 1;
|
|
347
|
+
NSNumber *batchId = @(self->_iosBatchId);
|
|
348
|
+
NSNumber *streamEpoch = record[@"streamEpoch"] ?: @1;
|
|
349
|
+
self->_iosBatchLedger[instanceId] = @{ @"streamEpoch": streamEpoch, @"batchId": batchId, @"messageCount": @(messages.count), @"bytes": @(bytes) };
|
|
350
|
+
record[@"messageQueue"] = queue;
|
|
351
|
+
self->_sessions[instanceId] = record;
|
|
352
|
+
NSMutableArray *wireMessages = [NSMutableArray arrayWithCapacity:messages.count];
|
|
353
|
+
for (NSDictionary *message in messages) {
|
|
354
|
+
NSMutableDictionary *wire = [message mutableCopy];
|
|
355
|
+
[wire removeObjectForKey:@"payloadBytes"];
|
|
356
|
+
[wireMessages addObject:wire];
|
|
357
|
+
}
|
|
358
|
+
[self emitOnMessageBatch:@{ @"instanceId": instanceId, @"streamEpoch": streamEpoch, @"batchId": batchId, @"messages": wireMessages }];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
- (void)restoreSubscriptionsForInstance:(NSString *)instanceId bridge:(ReactNativeMqttCocoaBridge *)bridge generation:(NSNumber *)generation {
|
|
362
|
+
NSMutableDictionary *record = [self->_sessions[instanceId] mutableCopy];
|
|
363
|
+
NSDictionary *desired = [record[@"desiredSubscriptions"] copy];
|
|
364
|
+
if (record == nil || desired.count == 0) return;
|
|
365
|
+
record[@"restorePending"] = @(desired.count);
|
|
366
|
+
record[@"restoreFailed"] = @NO;
|
|
367
|
+
self->_sessions[instanceId] = record;
|
|
368
|
+
NSMutableArray<NSArray<NSDictionary *> *> *batches = [NSMutableArray array];
|
|
369
|
+
NSMutableArray<NSDictionary *> *batch = [NSMutableArray array];
|
|
370
|
+
NSUInteger batchBytes = 0;
|
|
371
|
+
for (NSString *filter in desired) {
|
|
372
|
+
NSDictionary *intent = desired[filter];
|
|
373
|
+
NSDictionary *request = @{
|
|
374
|
+
@"filter": filter,
|
|
375
|
+
@"qos": intent[@"qos"] ?: @1,
|
|
376
|
+
@"noLocal": intent[@"noLocal"] ?: @NO,
|
|
377
|
+
@"retainAsPublished": intent[@"retainAsPublished"] ?: @NO,
|
|
378
|
+
@"retainHandling": intent[@"retainHandling"] ?: @0,
|
|
379
|
+
@"intentRevision": intent[@"intentRevision"] ?: @0,
|
|
380
|
+
};
|
|
381
|
+
NSUInteger estimatedBytes = [filter lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 8;
|
|
382
|
+
if (estimatedBytes > 16 * 1024) {
|
|
383
|
+
record[@"restoreFailed"] = @YES;
|
|
384
|
+
NSUInteger pending = [record[@"restorePending"] unsignedIntegerValue];
|
|
385
|
+
record[@"restorePending"] = @(pending > 0 ? pending - 1 : 0);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (batch.count > 0 && (batch.count >= 32 || batchBytes + estimatedBytes > 16 * 1024)) {
|
|
389
|
+
[batches addObject:[batch copy]];
|
|
390
|
+
batch = [NSMutableArray array];
|
|
391
|
+
batchBytes = 0;
|
|
392
|
+
}
|
|
393
|
+
[batch addObject:request];
|
|
394
|
+
batchBytes += estimatedBytes;
|
|
395
|
+
}
|
|
396
|
+
if (batch.count > 0) [batches addObject:[batch copy]];
|
|
397
|
+
if (batches.count == 0) {
|
|
398
|
+
record[@"subscriptionsStatus"] = @"failed";
|
|
399
|
+
record[@"revision"] = @([record[@"revision"] integerValue] + 1);
|
|
400
|
+
self->_sessions[instanceId] = record;
|
|
401
|
+
[self settleSubscriptionWaitersForInstance:instanceId];
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
self->_sessions[instanceId] = record;
|
|
405
|
+
[self restoreSubscriptionBatches:batches instanceId:instanceId bridge:bridge generation:generation index:0];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
- (void)restoreSubscriptionBatches:(NSArray<NSArray<NSDictionary *> *> *)batches instanceId:(NSString *)instanceId bridge:(ReactNativeMqttCocoaBridge *)bridge generation:(NSNumber *)generation index:(NSUInteger)index {
|
|
409
|
+
if (index >= batches.count) return;
|
|
410
|
+
NSUInteger waveCount = MIN((NSUInteger)4, batches.count - index);
|
|
411
|
+
NSMutableDictionary *waveRecord = [self->_sessions[instanceId] mutableCopy];
|
|
412
|
+
if (waveRecord == nil) return;
|
|
413
|
+
waveRecord[@"restoreWavePending"] = @(waveCount);
|
|
414
|
+
self->_sessions[instanceId] = waveRecord;
|
|
415
|
+
for (NSUInteger waveOffset = 0; waveOffset < waveCount; waveOffset += 1) {
|
|
416
|
+
NSUInteger batchIndex = index + waveOffset;
|
|
417
|
+
NSArray<NSDictionary *> *batch = batches[batchIndex];
|
|
418
|
+
NSMutableArray<NSDictionary *> *wire = [NSMutableArray arrayWithCapacity:batch.count];
|
|
419
|
+
for (NSDictionary *request in batch) {
|
|
420
|
+
[wire addObject:@{
|
|
421
|
+
@"filter": request[@"filter"],
|
|
422
|
+
@"qos": request[@"qos"],
|
|
423
|
+
@"noLocal": request[@"noLocal"],
|
|
424
|
+
@"retainAsPublished": request[@"retainAsPublished"],
|
|
425
|
+
@"retainHandling": request[@"retainHandling"],
|
|
426
|
+
}];
|
|
427
|
+
}
|
|
428
|
+
[bridge subscribeBatch:wire completion:^(NSArray<NSNumber *> *granted, NSError *error) {
|
|
429
|
+
dispatch_async(self->_sessionQueue, ^{
|
|
430
|
+
NSMutableDictionary *current = [self->_sessions[instanceId] mutableCopy];
|
|
431
|
+
if (self->_invalidated || current == nil || current[@"bridge"] != bridge || ![current[@"state"] isEqual:@"connected"] || ![current[@"generation"] isEqual:generation]) return;
|
|
432
|
+
if (error != nil || granted.count != batch.count) current[@"restoreFailed"] = @YES;
|
|
433
|
+
NSUInteger count = MIN(granted.count, batch.count);
|
|
434
|
+
for (NSUInteger itemIndex = 0; itemIndex < count; itemIndex += 1) {
|
|
435
|
+
NSDictionary *request = batch[itemIndex];
|
|
436
|
+
NSString *filter = request[@"filter"];
|
|
437
|
+
NSDictionary *intent = current[@"desiredSubscriptions"][filter];
|
|
438
|
+
if (intent == nil || ![intent[@"intentRevision"] isEqual:request[@"intentRevision"]]) continue;
|
|
439
|
+
NSNumber *value = granted[itemIndex];
|
|
440
|
+
if (value.integerValue > 2) current[@"restoreFailed"] = @YES;
|
|
441
|
+
else {
|
|
442
|
+
NSMutableDictionary *confirmed = [current[@"confirmedSubscriptions"] mutableCopy] ?: [NSMutableDictionary dictionary];
|
|
443
|
+
confirmed[filter] = @{ @"intentRevision": request[@"intentRevision"], @"grantedQos": value };
|
|
444
|
+
current[@"confirmedSubscriptions"] = confirmed;
|
|
445
|
+
}
|
|
446
|
+
NSUInteger pending = [current[@"restorePending"] unsignedIntegerValue];
|
|
447
|
+
current[@"restorePending"] = @(pending > 0 ? pending - 1 : 0);
|
|
448
|
+
}
|
|
449
|
+
if (error != nil || granted.count != batch.count) {
|
|
450
|
+
NSUInteger unaccounted = batch.count > count ? batch.count - count : batch.count;
|
|
451
|
+
NSUInteger pending = [current[@"restorePending"] unsignedIntegerValue];
|
|
452
|
+
current[@"restorePending"] = @(pending > unaccounted ? pending - unaccounted : 0);
|
|
453
|
+
}
|
|
454
|
+
self->_sessions[instanceId] = current;
|
|
455
|
+
NSUInteger wavePending = [current[@"restoreWavePending"] unsignedIntegerValue];
|
|
456
|
+
current[@"restoreWavePending"] = @(wavePending > 0 ? wavePending - 1 : 0);
|
|
457
|
+
self->_sessions[instanceId] = current;
|
|
458
|
+
if (wavePending <= 1) {
|
|
459
|
+
[self restoreSubscriptionBatches:batches instanceId:instanceId bridge:bridge generation:generation index:index + waveCount];
|
|
460
|
+
NSMutableDictionary *updated = [self->_sessions[instanceId] mutableCopy];
|
|
461
|
+
if ([updated[@"restorePending"] unsignedIntegerValue] == 0) {
|
|
462
|
+
updated[@"subscriptionsStatus"] = [updated[@"restoreFailed"] boolValue] ? @"failed" : @"ready";
|
|
463
|
+
updated[@"revision"] = @([updated[@"revision"] integerValue] + 1);
|
|
464
|
+
self->_sessions[instanceId] = updated;
|
|
465
|
+
[self settleSubscriptionWaitersForInstance:instanceId];
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
}];
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
- (void)restoreTombstonesForInstance:(NSString *)instanceId bridge:(ReactNativeMqttCocoaBridge *)bridge generation:(NSNumber *)generation {
|
|
474
|
+
NSMutableDictionary *record = [self->_sessions[instanceId] mutableCopy];
|
|
475
|
+
NSDictionary *tombstones = [record[@"tombstones"] copy];
|
|
476
|
+
if (record == nil || tombstones.count == 0) return;
|
|
477
|
+
record[@"restorePending"] = @(tombstones.count);
|
|
478
|
+
record[@"restoreFailed"] = @NO;
|
|
479
|
+
record[@"subscriptionsStatus"] = @"pending";
|
|
480
|
+
self->_sessions[instanceId] = record;
|
|
481
|
+
NSMutableArray<NSDictionary *> *requests = [NSMutableArray arrayWithCapacity:tombstones.count];
|
|
482
|
+
for (NSString *filter in tombstones) [requests addObject:@{ @"filter": filter, @"intentRevision": tombstones[filter] }];
|
|
483
|
+
[self restoreTombstoneRequests:requests instanceId:instanceId bridge:bridge generation:generation index:0];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
- (void)restoreTombstoneRequests:(NSArray<NSDictionary *> *)requests instanceId:(NSString *)instanceId bridge:(ReactNativeMqttCocoaBridge *)bridge generation:(NSNumber *)generation index:(NSUInteger)index {
|
|
487
|
+
if (index >= requests.count) return;
|
|
488
|
+
NSString *filter = requests[index][@"filter"];
|
|
489
|
+
NSNumber *intentRevision = requests[index][@"intentRevision"];
|
|
490
|
+
[self enqueueSubscriptionOperation:^(dispatch_block_t finish) {
|
|
491
|
+
[bridge unsubscribeFilter:filter completion:^(NSError *error) {
|
|
492
|
+
dispatch_async(self->_sessionQueue, ^{
|
|
493
|
+
NSMutableDictionary *current = [self->_sessions[instanceId] mutableCopy];
|
|
494
|
+
if (self->_invalidated || current == nil || current[@"bridge"] != bridge || ![current[@"state"] isEqual:@"connected"] || ![current[@"generation"] isEqual:generation]) { finish(); return; }
|
|
495
|
+
if (error != nil) {
|
|
496
|
+
current[@"restoreFailed"] = @YES;
|
|
497
|
+
} else if ([current[@"tombstones"][filter] isEqual:intentRevision]) {
|
|
498
|
+
NSMutableDictionary *currentTombstones = [current[@"tombstones"] mutableCopy] ?: [NSMutableDictionary dictionary];
|
|
499
|
+
[currentTombstones removeObjectForKey:filter];
|
|
500
|
+
current[@"tombstones"] = currentTombstones;
|
|
501
|
+
NSMutableDictionary *confirmed = [current[@"confirmedSubscriptions"] mutableCopy] ?: [NSMutableDictionary dictionary];
|
|
502
|
+
[confirmed removeObjectForKey:filter];
|
|
503
|
+
current[@"confirmedSubscriptions"] = confirmed;
|
|
504
|
+
}
|
|
505
|
+
NSUInteger pending = [current[@"restorePending"] unsignedIntegerValue];
|
|
506
|
+
current[@"restorePending"] = @(pending > 0 ? pending - 1 : 0);
|
|
507
|
+
self->_sessions[instanceId] = current;
|
|
508
|
+
[self restoreTombstoneRequests:requests instanceId:instanceId bridge:bridge generation:generation index:index + 1];
|
|
509
|
+
NSMutableDictionary *updated = [self->_sessions[instanceId] mutableCopy];
|
|
510
|
+
if ([updated[@"restorePending"] unsignedIntegerValue] == 0) {
|
|
511
|
+
updated[@"subscriptionsStatus"] = [updated[@"restoreFailed"] boolValue] ? @"failed" : @"ready";
|
|
512
|
+
updated[@"revision"] = @([updated[@"revision"] integerValue] + 1);
|
|
513
|
+
self->_sessions[instanceId] = updated;
|
|
514
|
+
[self settleSubscriptionWaitersForInstance:instanceId];
|
|
515
|
+
}
|
|
516
|
+
finish();
|
|
517
|
+
});
|
|
518
|
+
}];
|
|
519
|
+
} instanceId:instanceId filter:filter];
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
- (void)settleSubscriptionWaitersForInstance:(NSString *)instanceId {
|
|
523
|
+
NSMutableDictionary *record = [self->_sessions[instanceId] mutableCopy];
|
|
524
|
+
NSMutableArray *waiters = [record[@"subscriptionWaiters"] mutableCopy];
|
|
525
|
+
if (record == nil || waiters.count == 0) return;
|
|
526
|
+
NSMutableArray *remaining = [NSMutableArray array];
|
|
527
|
+
for (NSDictionary *waiter in waiters) {
|
|
528
|
+
NSNumber *waiterGeneration = waiter[@"generation"];
|
|
529
|
+
NSNumber *desiredRevision = waiter[@"desiredRevision"];
|
|
530
|
+
RCTPromiseResolveBlock resolve = waiter[@"resolve"];
|
|
531
|
+
RCTPromiseRejectBlock reject = waiter[@"reject"];
|
|
532
|
+
BOOL generationMatches = [record[@"generation"] isEqual:waiterGeneration];
|
|
533
|
+
if (![record[@"state"] isEqual:@"connected"] || !generationMatches) {
|
|
534
|
+
reject(@"ERR_CONNECTION_LOST", @"Subscription generation changed", nil);
|
|
535
|
+
} else if ([record[@"subscriptionsStatus"] isEqual:@"failed"]) {
|
|
536
|
+
reject(@"ERR_SUBSCRIBE_FAILED", @"Subscription synchronization failed", nil);
|
|
537
|
+
} else if ([record[@"subscriptionsStatus"] isEqual:@"ready"] && [record[@"subscriptionsRevision"] doubleValue] >= [desiredRevision doubleValue]) {
|
|
538
|
+
resolve([self snapshotForInstance:instanceId state:record[@"state"] revision:record[@"revision"] generation:record[@"generation"] lastConnAck:record[@"lastConnAck"] deliveryStatus:@"active"]);
|
|
539
|
+
} else {
|
|
540
|
+
[remaining addObject:waiter];
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
record[@"subscriptionWaiters"] = remaining;
|
|
544
|
+
self->_sessions[instanceId] = record;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
- (void)bindMessageHandler:(ReactNativeMqttCocoaBridge *)bridge instanceId:(NSString *)instanceId token:(NSNumber *)handlerToken {
|
|
548
|
+
__weak ReactNativeMqtt *weakSelf = self;
|
|
549
|
+
__weak ReactNativeMqttCocoaBridge *weakBridge = bridge;
|
|
550
|
+
[bridge setSessionEventHandler:^(NSString *state, NSNumber *revision, NSNumber *generation, NSDictionary *ack) {
|
|
551
|
+
ReactNativeMqtt *strongSelf = weakSelf;
|
|
552
|
+
ReactNativeMqttCocoaBridge *strongBridge = weakBridge;
|
|
553
|
+
if (strongSelf == nil || strongBridge == nil) return;
|
|
554
|
+
dispatch_async(strongSelf->_sessionQueue, ^{
|
|
555
|
+
NSMutableDictionary *record = [strongSelf->_sessions[instanceId] mutableCopy];
|
|
556
|
+
if (strongSelf->_invalidated || record == nil || record[@"bridge"] != strongBridge) return;
|
|
557
|
+
record[@"state"] = state;
|
|
558
|
+
record[@"revision"] = revision;
|
|
559
|
+
record[@"generation"] = generation;
|
|
560
|
+
if ([state isEqualToString:@"reconnecting"]) {
|
|
561
|
+
record[@"reconnectAttempts"] = @([record[@"reconnectAttempts"] unsignedIntegerValue] + 1);
|
|
562
|
+
[strongSelf->_iosSubscriptionQueues removeObjectForKey:instanceId];
|
|
563
|
+
[strongSelf settleSubscriptionWaitersForInstance:instanceId];
|
|
564
|
+
} else if ([state isEqualToString:@"connected"] && ack != nil) {
|
|
565
|
+
record[@"lastConnAck"] = ack;
|
|
566
|
+
record[@"reconnectAttempts"] = @0;
|
|
567
|
+
BOOL sessionPresent = [ack[@"sessionPresent"] boolValue];
|
|
568
|
+
BOOL hasTombstones = [record[@"tombstones"] count] != 0;
|
|
569
|
+
if (!sessionPresent) {
|
|
570
|
+
[record[@"tombstones"] removeAllObjects];
|
|
571
|
+
hasTombstones = NO;
|
|
572
|
+
}
|
|
573
|
+
record[@"subscriptionsStatus"] = (([record[@"desiredSubscriptions"] count] == 0 && !hasTombstones) || (sessionPresent && !hasTombstones)) ? @"ready" : @"pending";
|
|
574
|
+
}
|
|
575
|
+
strongSelf->_sessions[instanceId] = record;
|
|
576
|
+
[strongSelf emitOnSessionSnapshot:[strongSelf snapshotForInstance:instanceId state:state revision:revision generation:generation lastConnAck:record[@"lastConnAck"] deliveryStatus:@"active"]];
|
|
577
|
+
if ([state isEqualToString:@"connected"] && ack != nil) {
|
|
578
|
+
BOOL sessionPresent = [ack[@"sessionPresent"] boolValue];
|
|
579
|
+
BOOL hasTombstones = [record[@"tombstones"] count] != 0;
|
|
580
|
+
if ([record[@"desiredSubscriptions"] count] != 0 && !sessionPresent) {
|
|
581
|
+
[strongSelf restoreSubscriptionsForInstance:instanceId bridge:strongBridge generation:generation];
|
|
582
|
+
} else if (sessionPresent && hasTombstones) {
|
|
583
|
+
[strongSelf restoreTombstonesForInstance:instanceId bridge:strongBridge generation:generation];
|
|
584
|
+
}
|
|
585
|
+
NSArray *pending = [record[@"connectWaiters"] copy] ?: @[];
|
|
586
|
+
record[@"connectWaiters"] = [NSMutableArray array];
|
|
587
|
+
strongSelf->_sessions[instanceId] = record;
|
|
588
|
+
NSDictionary *snapshot = [strongSelf snapshotForInstance:instanceId state:@"connected" revision:revision generation:generation lastConnAck:ack deliveryStatus:@"active"];
|
|
589
|
+
for (NSDictionary *waiter in pending) ((RCTPromiseResolveBlock)waiter[@"resolve"])(@{ @"snapshot": snapshot, @"ack": ack });
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
}];
|
|
593
|
+
[bridge setDisconnectedHandler:^(NSNumber *reasonCode, NSString *reasonString) {
|
|
594
|
+
ReactNativeMqtt *strongSelf = weakSelf;
|
|
595
|
+
ReactNativeMqttCocoaBridge *strongBridge = weakBridge;
|
|
596
|
+
if (strongSelf == nil || strongBridge == nil) return;
|
|
597
|
+
dispatch_async(strongSelf->_sessionQueue, ^{
|
|
598
|
+
NSMutableDictionary *record = [strongSelf->_sessions[instanceId] mutableCopy];
|
|
599
|
+
if (strongSelf->_invalidated || record == nil || record[@"bridge"] != strongBridge || ![record[@"reconnectToken"] isEqual:handlerToken]) return;
|
|
600
|
+
if (reasonCode != nil) {
|
|
601
|
+
[strongSelf emitOnDiagnostic:@{
|
|
602
|
+
@"instanceId": instanceId,
|
|
603
|
+
@"generation": record[@"generation"] ?: @0,
|
|
604
|
+
@"code": @"SERVER_DISCONNECT",
|
|
605
|
+
@"message": @"Broker disconnected the MQTT session",
|
|
606
|
+
@"operation": @"disconnect",
|
|
607
|
+
@"reasonCode": reasonCode,
|
|
608
|
+
@"reasonString": reasonString ?: @"",
|
|
609
|
+
}];
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
}];
|
|
613
|
+
[bridge setMessageHandler:^(NSString *topic, NSData *payload, NSNumber *qos, BOOL retained, NSDictionary *metadata) {
|
|
614
|
+
ReactNativeMqtt *strongSelf = weakSelf;
|
|
615
|
+
if (strongSelf == nil) return;
|
|
616
|
+
dispatch_async(strongSelf->_sessionQueue, ^{
|
|
617
|
+
NSMutableDictionary *record = [strongSelf->_sessions[instanceId] mutableCopy];
|
|
618
|
+
if (strongSelf->_invalidated || record == nil || ![record[@"state"] isEqual:@"connected"]) return;
|
|
619
|
+
NSDictionary *options = record[@"options"];
|
|
620
|
+
NSUInteger payloadBytes = payload.length;
|
|
621
|
+
NSUInteger maxMessageBytes = [options[@"maxMessageBytes"] unsignedIntegerValue] ?: (1024 * 1024);
|
|
622
|
+
NSMutableArray *queue = [record[@"messageQueue"] mutableCopy] ?: [NSMutableArray array];
|
|
623
|
+
NSDictionary *inFlight = strongSelf->_iosBatchLedger[instanceId];
|
|
624
|
+
NSUInteger inFlightMessages = [inFlight[@"messageCount"] unsignedIntegerValue];
|
|
625
|
+
NSUInteger inFlightBytes = [inFlight[@"bytes"] unsignedIntegerValue];
|
|
626
|
+
NSUInteger maxQueuedMessages = [options[@"maxQueuedMessages"] unsignedIntegerValue] ?: 1024;
|
|
627
|
+
NSUInteger maxQueuedBytes = [options[@"maxQueuedBytes"] unsignedIntegerValue] ?: (8 * 1024 * 1024);
|
|
628
|
+
NSNumber *queuedBytesNumber = [queue valueForKeyPath:@"@sum.payloadBytes"];
|
|
629
|
+
NSUInteger queuedBytes = queuedBytesNumber.unsignedIntegerValue;
|
|
630
|
+
BOOL overLimit = payloadBytes > maxMessageBytes || queue.count + inFlightMessages >= maxQueuedMessages || inFlightBytes + queuedBytes + payloadBytes > maxQueuedBytes;
|
|
631
|
+
if (overLimit) {
|
|
632
|
+
NSString *policy = options[@"overflowPolicy"] ?: @"disconnect";
|
|
633
|
+
record[@"droppedMessages"] = @([record[@"droppedMessages"] unsignedIntegerValue] + 1);
|
|
634
|
+
if (![record[@"overflowDiagnosticPending"] boolValue]) {
|
|
635
|
+
record[@"overflowDiagnosticPending"] = @YES;
|
|
636
|
+
[strongSelf emitOnDiagnostic:@{ @"instanceId": instanceId, @"generation": record[@"generation"] ?: @0, @"code": @"QUEUE_OVERFLOW", @"message": @"Message delivery queue limit reached", @"operation": @"onMessageBatch", @"metrics": @{ @"queuedMessages": @(queue.count + inFlightMessages), @"queuedBytes": @(inFlightBytes + queuedBytes), @"pendingOperations": @([record[@"operationWaiters"] count] + [record[@"subscriptionWaiters"] count]), @"droppedMessages": record[@"droppedMessages"], @"reconnectAttempts": record[@"reconnectAttempts"] ?: @0 } }];
|
|
637
|
+
}
|
|
638
|
+
// Preserve the drop counter and coalescing marker for drop-newest;
|
|
639
|
+
// this branch must not lose the mutable session copy before return.
|
|
640
|
+
strongSelf->_sessions[instanceId] = record;
|
|
641
|
+
if ([policy isEqualToString:@"disconnect"]) {
|
|
642
|
+
record[@"reconnectToken"] = @([record[@"reconnectToken"] unsignedIntegerValue] + 1);
|
|
643
|
+
for (NSMutableDictionary *waiter in [record[@"operationWaiters"] copy] ?: @[]) {
|
|
644
|
+
if (![waiter[@"settled"] boolValue]) {
|
|
645
|
+
waiter[@"settled"] = @YES;
|
|
646
|
+
((RCTPromiseRejectBlock)waiter[@"reject"])(@"ERR_CONNECTION_LOST", @"Operation was cancelled by queue overflow", nil);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
record[@"operationWaiters"] = [NSMutableArray array];
|
|
650
|
+
record[@"state"] = @"disconnected";
|
|
651
|
+
record[@"revision"] = @([record[@"revision"] integerValue] + 1);
|
|
652
|
+
strongSelf->_sessions[instanceId] = record;
|
|
653
|
+
[strongSelf->_iosSubscriptionQueues removeObjectForKey:instanceId];
|
|
654
|
+
[strongSelf settleSubscriptionWaitersForInstance:instanceId];
|
|
655
|
+
[weakBridge disconnect];
|
|
656
|
+
[strongSelf emitOnSessionSnapshot:[strongSelf snapshotForInstance:instanceId
|
|
657
|
+
state:record[@"state"]
|
|
658
|
+
revision:record[@"revision"]
|
|
659
|
+
generation:record[@"generation"]
|
|
660
|
+
lastConnAck:record[@"lastConnAck"]
|
|
661
|
+
deliveryStatus:@"active"]];
|
|
662
|
+
}
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
NSMutableDictionary *wireMessage = [@{ @"instanceId": instanceId, @"generation": record[@"generation"] ?: @0, @"topic": topic, @"payloadBase64": [payload base64EncodedStringWithOptions:0] ?: @"", @"qos": qos ?: @0, @"retained": @(retained), @"payloadBytes": @(payloadBytes) } mutableCopy];
|
|
666
|
+
[metadata enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
|
|
667
|
+
(void)stop;
|
|
668
|
+
if (value != [NSNull null]) wireMessage[key] = value;
|
|
669
|
+
}];
|
|
670
|
+
[queue addObject:wireMessage];
|
|
671
|
+
record[@"messageQueue"] = queue;
|
|
672
|
+
strongSelf->_sessions[instanceId] = record;
|
|
673
|
+
[strongSelf emitNextMessageBatchForInstance:instanceId];
|
|
674
|
+
});
|
|
675
|
+
}];
|
|
676
|
+
}
|
|
677
|
+
- (void)connect:(NSString *)instanceId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
678
|
+
dispatch_async(_sessionQueue, ^{
|
|
679
|
+
NSMutableDictionary *record = [self->_sessions[instanceId] mutableCopy];
|
|
680
|
+
ReactNativeMqttCocoaBridge *bridge = record[@"bridge"];
|
|
681
|
+
if (record == nil || bridge == nil) { reject(@"ERR_NATIVE_UNAVAILABLE", @"Session is not open", nil); return; }
|
|
682
|
+
if ([record[@"state"] isEqual:@"closing"]) { reject(@"ERR_DISPOSED", @"Session is closing", nil); return; }
|
|
683
|
+
if ([record[@"state"] isEqual:@"connected"]) {
|
|
684
|
+
NSDictionary *snapshot = [self snapshotForInstance:instanceId state:record[@"state"] revision:record[@"revision"] generation:record[@"generation"] lastConnAck:record[@"lastConnAck"] deliveryStatus:@"active"];
|
|
685
|
+
resolve(@{ @"snapshot": snapshot, @"ack": record[@"lastConnAck"] });
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
NSMutableArray *waiters = [record[@"connectWaiters"] mutableCopy] ?: [NSMutableArray array];
|
|
689
|
+
[waiters addObject:@{ @"resolve": [resolve copy], @"reject": [reject copy] }];
|
|
690
|
+
if ([record[@"state"] isEqual:@"connecting"]) {
|
|
691
|
+
record[@"connectWaiters"] = waiters;
|
|
692
|
+
self->_sessions[instanceId] = record;
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if ([record[@"state"] isEqual:@"reconnecting"]) {
|
|
696
|
+
NSDictionary *waiter = waiters.lastObject;
|
|
697
|
+
record[@"connectWaiters"] = waiters;
|
|
698
|
+
self->_sessions[instanceId] = record;
|
|
699
|
+
NSUInteger timeoutMs = [record[@"options"][@"connectTimeoutMs"] unsignedIntegerValue] ?: 30000;
|
|
700
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeoutMs * NSEC_PER_MSEC), self->_sessionQueue, ^{
|
|
701
|
+
NSMutableDictionary *current = [self->_sessions[instanceId] mutableCopy];
|
|
702
|
+
NSMutableArray *currentWaiters = [current[@"connectWaiters"] mutableCopy];
|
|
703
|
+
if (current == nil || currentWaiters == nil || ![currentWaiters containsObject:waiter]) return;
|
|
704
|
+
[currentWaiters removeObject:waiter];
|
|
705
|
+
current[@"connectWaiters"] = currentWaiters;
|
|
706
|
+
self->_sessions[instanceId] = current;
|
|
707
|
+
NSError *timeoutError = [NSError errorWithDomain:@"ReactNativeMqtt" code:408 userInfo:@{ NSLocalizedDescriptionKey: @"Connection timed out while reconnecting" }];
|
|
708
|
+
((RCTPromiseRejectBlock)waiter[@"reject"])(@"ERR_CONNECT_TIMEOUT", timeoutError.localizedDescription, timeoutError);
|
|
709
|
+
});
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (record[@"lastConnAck"] != [NSNull null]) {
|
|
713
|
+
bridge = [self bridgeForSessionOptions:record[@"options"] clientId:record[@"clientId"] ?: instanceId];
|
|
714
|
+
record[@"bridge"] = bridge;
|
|
715
|
+
}
|
|
716
|
+
record[@"state"] = @"connecting";
|
|
717
|
+
record[@"revision"] = @([record[@"revision"] integerValue] + 1);
|
|
718
|
+
record[@"reconnectToken"] = @([record[@"reconnectToken"] unsignedIntegerValue] + 1);
|
|
719
|
+
record[@"connectWaiters"] = waiters;
|
|
720
|
+
self->_sessions[instanceId] = record;
|
|
721
|
+
[self bindMessageHandler:bridge instanceId:instanceId token:record[@"reconnectToken"]];
|
|
722
|
+
__block BOOL settled = NO;
|
|
723
|
+
[bridge connectWithCompletion:^(NSDictionary *ack, NSError *error) {
|
|
724
|
+
dispatch_async(self->_sessionQueue, ^{
|
|
725
|
+
if (settled) return;
|
|
726
|
+
settled = YES;
|
|
727
|
+
NSMutableDictionary *current = [self->_sessions[instanceId] mutableCopy];
|
|
728
|
+
NSArray *pending = [current[@"connectWaiters"] copy] ?: @[];
|
|
729
|
+
if (self->_invalidated || current == nil || current[@"bridge"] != bridge) {
|
|
730
|
+
for (NSDictionary *waiter in pending) ((RCTPromiseRejectBlock)waiter[@"reject"])(@"ERR_DISPOSED", @"Native module is invalidated", nil);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
if (error != nil) {
|
|
734
|
+
if (![current[@"state"] isEqual:@"connecting"] && ![current[@"state"] isEqual:@"disconnected"]) return;
|
|
735
|
+
current[@"connectWaiters"] = [NSMutableArray array];
|
|
736
|
+
current[@"state"] = @"disconnected";
|
|
737
|
+
current[@"revision"] = @([current[@"revision"] integerValue] + 1);
|
|
738
|
+
self->_sessions[instanceId] = current;
|
|
739
|
+
NSNumber *callbackGeneration = @([current[@"generation"] integerValue] + 1);
|
|
740
|
+
NSError *annotatedError = RNMqttAnnotatedError(error, @"connect", callbackGeneration, @"unknown");
|
|
741
|
+
for (NSDictionary *waiter in pending) ((RCTPromiseRejectBlock)waiter[@"reject"])(@"ERR_CONNECTION_REFUSED", annotatedError.localizedDescription, annotatedError);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
// The Swift Session's connect event owns the successful transition,
|
|
745
|
+
// ACK cache, generation, and connect waiter settlement.
|
|
746
|
+
});
|
|
747
|
+
}];
|
|
748
|
+
NSUInteger timeoutMs = [record[@"options"][@"connectTimeoutMs"] unsignedIntegerValue] ?: 30000;
|
|
749
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeoutMs * NSEC_PER_MSEC), self->_sessionQueue, ^{
|
|
750
|
+
if (settled) return;
|
|
751
|
+
settled = YES;
|
|
752
|
+
NSMutableDictionary *current = [self->_sessions[instanceId] mutableCopy];
|
|
753
|
+
if (self->_invalidated || current == nil || current[@"bridge"] != bridge || ![current[@"state"] isEqual:@"connecting"]) return;
|
|
754
|
+
NSArray *pending = [current[@"connectWaiters"] copy] ?: @[];
|
|
755
|
+
current[@"connectWaiters"] = [NSMutableArray array];
|
|
756
|
+
current[@"state"] = @"disconnected";
|
|
757
|
+
current[@"revision"] = @([current[@"revision"] integerValue] + 1);
|
|
758
|
+
current[@"reconnectToken"] = @([current[@"reconnectToken"] unsignedIntegerValue] + 1);
|
|
759
|
+
self->_sessions[instanceId] = current;
|
|
760
|
+
[bridge disconnect];
|
|
761
|
+
NSError *timeoutError = [NSError errorWithDomain:@"ReactNativeMqtt" code:408 userInfo:@{ NSLocalizedDescriptionKey: @"Connection timed out" }];
|
|
762
|
+
for (NSDictionary *waiter in pending) ((RCTPromiseRejectBlock)waiter[@"reject"])(@"ERR_CONNECT_TIMEOUT", timeoutError.localizedDescription, timeoutError);
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
- (void)disconnect:(NSString *)instanceId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
767
|
+
dispatch_async(_sessionQueue, ^{
|
|
768
|
+
NSMutableDictionary *record = [self->_sessions[instanceId] mutableCopy];
|
|
769
|
+
if (record == nil || [record[@"state"] isEqual:@"closed"]) { resolve([self snapshotForInstance:instanceId state:@"closed" revision:@0 generation:@0 lastConnAck:[NSNull null] deliveryStatus:@"closed"]); return; }
|
|
770
|
+
ReactNativeMqttCocoaBridge *bridge = record[@"bridge"];
|
|
771
|
+
if (bridge == nil) { resolve([self snapshotForInstance:instanceId state:record[@"state"] revision:record[@"revision"] generation:record[@"generation"] lastConnAck:record[@"lastConnAck"] deliveryStatus:@"active"]); return; }
|
|
772
|
+
if ([record[@"state"] isEqual:@"connecting"] || [record[@"state"] isEqual:@"reconnecting"]) {
|
|
773
|
+
NSArray *pending = [record[@"connectWaiters"] copy] ?: @[];
|
|
774
|
+
record[@"connectWaiters"] = [NSMutableArray array];
|
|
775
|
+
for (NSMutableDictionary *waiter in [record[@"operationWaiters"] copy] ?: @[]) {
|
|
776
|
+
if (![waiter[@"settled"] boolValue]) {
|
|
777
|
+
waiter[@"settled"] = @YES;
|
|
778
|
+
((RCTPromiseRejectBlock)waiter[@"reject"])(@"ERR_CONNECTION_LOST", @"Operation was cancelled by disconnect", nil);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
record[@"operationWaiters"] = [NSMutableArray array];
|
|
782
|
+
record[@"state"] = @"disconnected";
|
|
783
|
+
record[@"revision"] = @([record[@"revision"] integerValue] + 1);
|
|
784
|
+
record[@"reconnectToken"] = @([record[@"reconnectToken"] unsignedIntegerValue] + 1);
|
|
785
|
+
self->_sessions[instanceId] = record;
|
|
786
|
+
[self->_iosSubscriptionQueues removeObjectForKey:instanceId];
|
|
787
|
+
[bridge disconnect];
|
|
788
|
+
for (NSDictionary *waiter in pending) ((RCTPromiseRejectBlock)waiter[@"reject"])(@"ERR_CONNECTION_LOST", @"Connection was stopped", nil);
|
|
789
|
+
[self emitOnSessionSnapshot:[self snapshotForInstance:instanceId state:record[@"state"] revision:record[@"revision"] generation:record[@"generation"] lastConnAck:record[@"lastConnAck"] deliveryStatus:@"active"]];
|
|
790
|
+
resolve([self snapshotForInstance:instanceId state:record[@"state"] revision:record[@"revision"] generation:record[@"generation"] lastConnAck:record[@"lastConnAck"] deliveryStatus:@"active"]);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
if (![record[@"state"] isEqual:@"connected"]) { resolve([self snapshotForInstance:instanceId state:record[@"state"] revision:record[@"revision"] generation:record[@"generation"] lastConnAck:record[@"lastConnAck"] deliveryStatus:@"active"]); return; }
|
|
794
|
+
record[@"state"] = @"disconnecting";
|
|
795
|
+
record[@"revision"] = @([record[@"revision"] integerValue] + 1);
|
|
796
|
+
record[@"reconnectToken"] = @([record[@"reconnectToken"] unsignedIntegerValue] + 1);
|
|
797
|
+
for (NSMutableDictionary *waiter in [record[@"operationWaiters"] copy] ?: @[]) {
|
|
798
|
+
if (![waiter[@"settled"] boolValue]) {
|
|
799
|
+
waiter[@"settled"] = @YES;
|
|
800
|
+
((RCTPromiseRejectBlock)waiter[@"reject"])(@"ERR_CONNECTION_LOST", @"Operation was cancelled by disconnect", nil);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
record[@"operationWaiters"] = [NSMutableArray array];
|
|
804
|
+
self->_sessions[instanceId] = record;
|
|
805
|
+
[self->_iosSubscriptionQueues removeObjectForKey:instanceId];
|
|
806
|
+
[self settleSubscriptionWaitersForInstance:instanceId];
|
|
807
|
+
__block BOOL settled = NO;
|
|
808
|
+
void (^finish)(NSError *) = ^(NSError *error) {
|
|
809
|
+
if (settled) return;
|
|
810
|
+
settled = YES;
|
|
811
|
+
NSMutableDictionary *current = [self->_sessions[instanceId] mutableCopy];
|
|
812
|
+
if (current == nil || current[@"bridge"] != bridge) return;
|
|
813
|
+
current[@"state"] = @"disconnected";
|
|
814
|
+
current[@"revision"] = @([current[@"revision"] integerValue] + 1);
|
|
815
|
+
self->_sessions[instanceId] = current;
|
|
816
|
+
if (error != nil) { reject(@"ERR_CLEANUP_FAILED", error.localizedDescription, error); return; }
|
|
817
|
+
resolve([self snapshotForInstance:instanceId state:current[@"state"] revision:current[@"revision"] generation:current[@"generation"] lastConnAck:current[@"lastConnAck"] deliveryStatus:@"active"]);
|
|
818
|
+
};
|
|
819
|
+
[bridge disconnectWithCompletion:^(NSError *error) { dispatch_async(self->_sessionQueue, ^{ finish(error); }); }];
|
|
820
|
+
NSUInteger timeoutMs = [record[@"options"][@"disconnectTimeoutMs"] unsignedIntegerValue] ?: 5000;
|
|
821
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeoutMs * NSEC_PER_MSEC), self->_sessionQueue, ^{
|
|
822
|
+
if (settled) return;
|
|
823
|
+
[bridge disconnect];
|
|
824
|
+
finish([NSError errorWithDomain:@"ReactNativeMqtt" code:408 userInfo:@{ NSLocalizedDescriptionKey: @"Disconnect timed out" }]);
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
- (void)close:(NSString *)instanceId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
829
|
+
dispatch_async(_sessionQueue, ^{
|
|
830
|
+
NSMutableDictionary *existing = [self->_sessions[instanceId] mutableCopy];
|
|
831
|
+
if (existing == nil) { resolve([self snapshotForInstance:instanceId state:@"closed" revision:@0 generation:@0 lastConnAck:[NSNull null] deliveryStatus:@"closed"]); return; }
|
|
832
|
+
NSNumber *revision = existing == nil ? @0 : @([existing[@"revision"] integerValue] + 1);
|
|
833
|
+
for (NSDictionary *waiter in [existing[@"connectWaiters"] copy] ?: @[]) {
|
|
834
|
+
((RCTPromiseRejectBlock)waiter[@"reject"])(@"ERR_DISPOSED", @"Session was closed", nil);
|
|
835
|
+
}
|
|
836
|
+
for (NSDictionary *waiter in [existing[@"subscriptionWaiters"] copy] ?: @[]) {
|
|
837
|
+
((RCTPromiseRejectBlock)waiter[@"reject"])(@"ERR_DISPOSED", @"Session was closed", nil);
|
|
838
|
+
}
|
|
839
|
+
for (NSMutableDictionary *waiter in [existing[@"operationWaiters"] copy] ?: @[]) {
|
|
840
|
+
if (![waiter[@"settled"] boolValue]) {
|
|
841
|
+
waiter[@"settled"] = @YES;
|
|
842
|
+
((RCTPromiseRejectBlock)waiter[@"reject"])(@"ERR_DISPOSED", @"Session was closed", nil);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
ReactNativeMqttCocoaBridge *bridge = existing[@"bridge"];
|
|
846
|
+
if (bridge == nil || [existing[@"state"] isEqual:@"idle"] || [existing[@"state"] isEqual:@"disconnected"] || [existing[@"state"] isEqual:@"closed"]) {
|
|
847
|
+
[bridge disconnect];
|
|
848
|
+
[self->_sessions removeObjectForKey:instanceId];
|
|
849
|
+
[self->_iosBatchLedger removeObjectForKey:instanceId];
|
|
850
|
+
[self->_iosSubscriptionQueues removeObjectForKey:instanceId];
|
|
851
|
+
self->_iosStreamEpochs[instanceId] = @([self->_iosStreamEpochs[instanceId] unsignedLongLongValue] + 1);
|
|
852
|
+
resolve([self snapshotForInstance:instanceId state:@"closed" revision:revision generation:existing[@"generation"] ?: @0 lastConnAck:existing[@"lastConnAck"] deliveryStatus:@"closed"]);
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
existing[@"state"] = @"closing";
|
|
856
|
+
existing[@"revision"] = revision;
|
|
857
|
+
existing[@"reconnectToken"] = @([existing[@"reconnectToken"] unsignedIntegerValue] + 1);
|
|
858
|
+
existing[@"connectWaiters"] = [NSMutableArray array];
|
|
859
|
+
existing[@"subscriptionWaiters"] = [NSMutableArray array];
|
|
860
|
+
self->_sessions[instanceId] = existing;
|
|
861
|
+
__block BOOL settled = NO;
|
|
862
|
+
void (^finish)(NSError *) = ^(NSError *error) {
|
|
863
|
+
if (settled) return;
|
|
864
|
+
settled = YES;
|
|
865
|
+
NSMutableDictionary *current = [self->_sessions[instanceId] mutableCopy];
|
|
866
|
+
if (current == nil || current[@"bridge"] != bridge) return;
|
|
867
|
+
NSNumber *closedRevision = @([current[@"revision"] integerValue] + 1);
|
|
868
|
+
[self->_sessions removeObjectForKey:instanceId];
|
|
869
|
+
[self->_iosBatchLedger removeObjectForKey:instanceId];
|
|
870
|
+
[self->_iosSubscriptionQueues removeObjectForKey:instanceId];
|
|
871
|
+
self->_iosStreamEpochs[instanceId] = @([self->_iosStreamEpochs[instanceId] unsignedLongLongValue] + 1);
|
|
872
|
+
if (error != nil) { reject(@"ERR_CLEANUP_FAILED", error.localizedDescription, error); return; }
|
|
873
|
+
resolve([self snapshotForInstance:instanceId state:@"closed" revision:closedRevision generation:current[@"generation"] ?: @0 lastConnAck:current[@"lastConnAck"] deliveryStatus:@"closed"]);
|
|
874
|
+
};
|
|
875
|
+
[bridge disposeWithCompletion:^(NSError *error) { dispatch_async(self->_sessionQueue, ^{ finish(error); }); }];
|
|
876
|
+
NSUInteger timeoutMs = [existing[@"options"][@"disconnectTimeoutMs"] unsignedIntegerValue] ?: 5000;
|
|
877
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeoutMs * NSEC_PER_MSEC), self->_sessionQueue, ^{
|
|
878
|
+
if (settled) return;
|
|
879
|
+
[bridge disconnect];
|
|
880
|
+
finish([NSError errorWithDomain:@"ReactNativeMqtt" code:408 userInfo:@{ NSLocalizedDescriptionKey: @"Close timed out while disconnecting" }]);
|
|
881
|
+
});
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
- (void)getSnapshot:(NSString *)instanceId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { (void)reject; dispatch_async(_sessionQueue, ^{ NSDictionary *existing = self->_sessions[instanceId]; if (existing == nil) { resolve([self snapshotForInstance:instanceId state:@"closed" revision:@0 generation:@0 lastConnAck:[NSNull null] deliveryStatus:@"closed"]); } else { resolve([self snapshotForInstance:instanceId state:existing[@"state"] revision:existing[@"revision"] generation:existing[@"generation"] lastConnAck:existing[@"lastConnAck"] deliveryStatus:@"active"]); } }); }
|
|
885
|
+
- (void)setSubscription:(NSString *)instanceId filter:(NSString *)filter options:(JS::NativeReactNativeMqtt::WireSubscribeOptions &)options intentRevision:(double)intentRevision resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
886
|
+
if (!RNMqttValidTopicFilter(filter)) { reject(@"ERR_INVALID_OPTIONS", @"topic filter is invalid", nil); return; }
|
|
887
|
+
NSNumber *requestedQos = @(options.qos().value_or(1));
|
|
888
|
+
BOOL requestedNoLocal = options.noLocal().value_or(false);
|
|
889
|
+
BOOL requestedRetainAsPublished = options.retainAsPublished().value_or(false);
|
|
890
|
+
NSNumber *requestedRetainHandling = @(options.retainHandling().value_or(0));
|
|
891
|
+
dispatch_async(_sessionQueue, ^{
|
|
892
|
+
NSMutableDictionary *record = [self->_sessions[instanceId] mutableCopy];
|
|
893
|
+
ReactNativeMqttCocoaBridge *bridge = record[@"bridge"];
|
|
894
|
+
if (record == nil || bridge == nil || ![record[@"state"] isEqual:@"connected"]) { reject(@"ERR_NOT_CONNECTED", @"Session is not connected", nil); return; }
|
|
895
|
+
NSMutableDictionary *desired = [record[@"desiredSubscriptions"] mutableCopy] ?: [NSMutableDictionary dictionary];
|
|
896
|
+
desired[filter] = @{ @"qos": requestedQos, @"noLocal": @(requestedNoLocal), @"retainAsPublished": @(requestedRetainAsPublished), @"retainHandling": requestedRetainHandling, @"intentRevision": @(intentRevision) };
|
|
897
|
+
record[@"desiredSubscriptions"] = desired;
|
|
898
|
+
NSMutableDictionary *tombstones = [record[@"tombstones"] mutableCopy] ?: [NSMutableDictionary dictionary];
|
|
899
|
+
[tombstones removeObjectForKey:filter];
|
|
900
|
+
record[@"tombstones"] = tombstones;
|
|
901
|
+
record[@"subscriptionsRevision"] = @(MAX([record[@"subscriptionsRevision"] doubleValue], intentRevision));
|
|
902
|
+
record[@"subscriptionsStatus"] = @"pending";
|
|
903
|
+
record[@"pendingSubscriptionOperations"] = @([record[@"pendingSubscriptionOperations"] unsignedIntegerValue] + 1);
|
|
904
|
+
NSMutableDictionary *operationWaiter = [self operationWaiterWithReject:reject operation:@"subscribe" topic:filter generation:record[@"generation"]];
|
|
905
|
+
NSMutableArray *operationWaiters = [record[@"operationWaiters"] mutableCopy] ?: [NSMutableArray array];
|
|
906
|
+
[operationWaiters addObject:operationWaiter];
|
|
907
|
+
record[@"operationWaiters"] = operationWaiters;
|
|
908
|
+
self->_sessions[instanceId] = record;
|
|
909
|
+
__block BOOL settled = NO;
|
|
910
|
+
__block dispatch_block_t finishOperation;
|
|
911
|
+
[self enqueueSubscriptionOperation:^(dispatch_block_t finish) {
|
|
912
|
+
finishOperation = [finish copy];
|
|
913
|
+
[bridge subscribeFilter:filter qos:requestedQos noLocal:requestedNoLocal retainAsPublished:requestedRetainAsPublished retainHandling:requestedRetainHandling completion:^(NSNumber *granted, NSError *error) {
|
|
914
|
+
dispatch_async(self->_sessionQueue, ^{
|
|
915
|
+
if (settled || [operationWaiter[@"settled"] boolValue]) { finish(); return; }
|
|
916
|
+
settled = YES;
|
|
917
|
+
operationWaiter[@"settled"] = @YES;
|
|
918
|
+
NSMutableDictionary *current = [self->_sessions[instanceId] mutableCopy];
|
|
919
|
+
NSMutableArray *remainingOperations = [current[@"operationWaiters"] mutableCopy] ?: [NSMutableArray array];
|
|
920
|
+
[remainingOperations removeObject:operationWaiter];
|
|
921
|
+
current[@"operationWaiters"] = remainingOperations;
|
|
922
|
+
NSDictionary *intent = current[@"desiredSubscriptions"][filter];
|
|
923
|
+
if (error != nil || granted == nil || granted.integerValue >= 0x80) {
|
|
924
|
+
NSError *subscribeError = error;
|
|
925
|
+
if (subscribeError == nil) {
|
|
926
|
+
NSNumber *reasonCode = granted ?: @0x80;
|
|
927
|
+
subscribeError = [NSError errorWithDomain:@"ReactNativeMqtt" code:reasonCode.integerValue userInfo:@{
|
|
928
|
+
NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Subscription rejected (reason 0x%02lX)", (long)reasonCode.integerValue],
|
|
929
|
+
@"reasonCode": reasonCode
|
|
930
|
+
}];
|
|
931
|
+
}
|
|
932
|
+
subscribeError = RNMqttAnnotatedError(subscribeError, @"subscribe", current[@"generation"], @"rejected");
|
|
933
|
+
current[@"pendingSubscriptionOperations"] = @([current[@"pendingSubscriptionOperations"] unsignedIntegerValue] > 0 ? [current[@"pendingSubscriptionOperations"] unsignedIntegerValue] - 1 : 0);
|
|
934
|
+
if ([intent[@"intentRevision"] doubleValue] == intentRevision) current[@"subscriptionsStatus"] = @"failed";
|
|
935
|
+
self->_sessions[instanceId] = current;
|
|
936
|
+
[self settleSubscriptionWaitersForInstance:instanceId];
|
|
937
|
+
reject(@"ERR_SUBSCRIBE_FAILED", subscribeError.localizedDescription, subscribeError);
|
|
938
|
+
} else if ([intent[@"intentRevision"] doubleValue] != intentRevision) {
|
|
939
|
+
[self settleSubscriptionWaitersForInstance:instanceId];
|
|
940
|
+
reject(@"ERR_OPERATION_SUPERSEDED", @"Subscription intent was superseded", nil);
|
|
941
|
+
} else {
|
|
942
|
+
current[@"pendingSubscriptionOperations"] = @([current[@"pendingSubscriptionOperations"] unsignedIntegerValue] > 0 ? [current[@"pendingSubscriptionOperations"] unsignedIntegerValue] - 1 : 0);
|
|
943
|
+
current[@"subscriptionsStatus"] = [current[@"pendingSubscriptionOperations"] unsignedIntegerValue] == 0 ? @"ready" : @"pending";
|
|
944
|
+
NSMutableDictionary *confirmed = [current[@"confirmedSubscriptions"] mutableCopy] ?: [NSMutableDictionary dictionary];
|
|
945
|
+
confirmed[filter] = @{ @"intentRevision": @(intentRevision), @"grantedQos": granted ?: @0 };
|
|
946
|
+
current[@"confirmedSubscriptions"] = confirmed;
|
|
947
|
+
current[@"revision"] = @([current[@"revision"] integerValue] + 1);
|
|
948
|
+
self->_sessions[instanceId] = current;
|
|
949
|
+
[self settleSubscriptionWaitersForInstance:instanceId];
|
|
950
|
+
resolve(@{ @"generation": current[@"generation"] ?: @0, @"intentRevision": @(intentRevision), @"grantedQos": granted ?: @0, @"reasonCode": granted ?: @0 });
|
|
951
|
+
}
|
|
952
|
+
finish();
|
|
953
|
+
});
|
|
954
|
+
}];
|
|
955
|
+
} instanceId:instanceId filter:filter];
|
|
956
|
+
NSUInteger timeoutMs = [record[@"options"][@"operationTimeoutMs"] unsignedIntegerValue] ?: 30000;
|
|
957
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeoutMs * NSEC_PER_MSEC), self->_sessionQueue, ^{
|
|
958
|
+
if (settled) return;
|
|
959
|
+
settled = YES;
|
|
960
|
+
operationWaiter[@"settled"] = @YES;
|
|
961
|
+
NSMutableDictionary *current = [self->_sessions[instanceId] mutableCopy];
|
|
962
|
+
if (current != nil) {
|
|
963
|
+
NSMutableArray *remainingOperations = [current[@"operationWaiters"] mutableCopy] ?: [NSMutableArray array];
|
|
964
|
+
[remainingOperations removeObject:operationWaiter];
|
|
965
|
+
current[@"operationWaiters"] = remainingOperations;
|
|
966
|
+
current[@"pendingSubscriptionOperations"] = @([current[@"pendingSubscriptionOperations"] unsignedIntegerValue] > 0 ? [current[@"pendingSubscriptionOperations"] unsignedIntegerValue] - 1 : 0);
|
|
967
|
+
current[@"subscriptionsStatus"] = @"failed";
|
|
968
|
+
self->_sessions[instanceId] = current;
|
|
969
|
+
[self settleSubscriptionWaitersForInstance:instanceId];
|
|
970
|
+
}
|
|
971
|
+
reject(@"ERR_OPERATION_TIMEOUT", @"Subscription operation timed out", nil);
|
|
972
|
+
if (finishOperation != nil) finishOperation();
|
|
973
|
+
});
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
- (void)removeSubscription:(NSString *)instanceId filter:(NSString *)filter intentRevision:(double)intentRevision resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
977
|
+
if (!RNMqttValidTopicFilter(filter)) { reject(@"ERR_INVALID_OPTIONS", @"topic filter is invalid", nil); return; }
|
|
978
|
+
dispatch_async(_sessionQueue, ^{
|
|
979
|
+
NSMutableDictionary *record = [self->_sessions[instanceId] mutableCopy];
|
|
980
|
+
ReactNativeMqttCocoaBridge *bridge = record[@"bridge"];
|
|
981
|
+
if (record == nil || bridge == nil) { reject(@"ERR_NOT_CONNECTED", @"Session is not open", nil); return; }
|
|
982
|
+
NSMutableDictionary *desired = [record[@"desiredSubscriptions"] mutableCopy] ?: [NSMutableDictionary dictionary];
|
|
983
|
+
NSDictionary *previous = desired[filter];
|
|
984
|
+
[desired removeObjectForKey:filter];
|
|
985
|
+
record[@"desiredSubscriptions"] = desired;
|
|
986
|
+
NSMutableDictionary *tombstones = [record[@"tombstones"] mutableCopy] ?: [NSMutableDictionary dictionary];
|
|
987
|
+
tombstones[filter] = @(intentRevision);
|
|
988
|
+
record[@"tombstones"] = tombstones;
|
|
989
|
+
record[@"subscriptionsRevision"] = @(MAX([record[@"subscriptionsRevision"] doubleValue], intentRevision));
|
|
990
|
+
if (![record[@"state"] isEqual:@"connected"]) {
|
|
991
|
+
record[@"subscriptionsStatus"] = @"pending";
|
|
992
|
+
self->_sessions[instanceId] = record;
|
|
993
|
+
resolve(@{ @"intentRevision": @(intentRevision) });
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
record[@"subscriptionsStatus"] = @"pending";
|
|
997
|
+
record[@"pendingSubscriptionOperations"] = @([record[@"pendingSubscriptionOperations"] unsignedIntegerValue] + 1);
|
|
998
|
+
NSMutableDictionary *operationWaiter = [self operationWaiterWithReject:reject operation:@"unsubscribe" topic:filter generation:record[@"generation"]];
|
|
999
|
+
NSMutableArray *operationWaiters = [record[@"operationWaiters"] mutableCopy] ?: [NSMutableArray array];
|
|
1000
|
+
[operationWaiters addObject:operationWaiter];
|
|
1001
|
+
record[@"operationWaiters"] = operationWaiters;
|
|
1002
|
+
self->_sessions[instanceId] = record;
|
|
1003
|
+
__block BOOL settled = NO;
|
|
1004
|
+
__block dispatch_block_t finishOperation;
|
|
1005
|
+
[self enqueueSubscriptionOperation:^(dispatch_block_t finish) {
|
|
1006
|
+
finishOperation = [finish copy];
|
|
1007
|
+
[bridge unsubscribeFilter:filter completion:^(NSError *error) {
|
|
1008
|
+
dispatch_async(self->_sessionQueue, ^{
|
|
1009
|
+
if (settled || [operationWaiter[@"settled"] boolValue]) { finish(); return; }
|
|
1010
|
+
settled = YES;
|
|
1011
|
+
operationWaiter[@"settled"] = @YES;
|
|
1012
|
+
NSMutableDictionary *current = [self->_sessions[instanceId] mutableCopy];
|
|
1013
|
+
NSMutableArray *remainingOperations = [current[@"operationWaiters"] mutableCopy] ?: [NSMutableArray array];
|
|
1014
|
+
[remainingOperations removeObject:operationWaiter];
|
|
1015
|
+
current[@"operationWaiters"] = remainingOperations;
|
|
1016
|
+
if (error != nil) {
|
|
1017
|
+
if (previous != nil) current[@"desiredSubscriptions"][filter] = previous;
|
|
1018
|
+
NSError *annotatedError = RNMqttAnnotatedError(error, @"unsubscribe", current[@"generation"], @"rejected");
|
|
1019
|
+
current[@"pendingSubscriptionOperations"] = @([current[@"pendingSubscriptionOperations"] unsignedIntegerValue] > 0 ? [current[@"pendingSubscriptionOperations"] unsignedIntegerValue] - 1 : 0);
|
|
1020
|
+
current[@"subscriptionsStatus"] = @"failed";
|
|
1021
|
+
self->_sessions[instanceId] = current;
|
|
1022
|
+
[self settleSubscriptionWaitersForInstance:instanceId];
|
|
1023
|
+
reject(@"ERR_UNSUBSCRIBE_FAILED", annotatedError.localizedDescription, annotatedError);
|
|
1024
|
+
} else {
|
|
1025
|
+
current[@"pendingSubscriptionOperations"] = @([current[@"pendingSubscriptionOperations"] unsignedIntegerValue] > 0 ? [current[@"pendingSubscriptionOperations"] unsignedIntegerValue] - 1 : 0);
|
|
1026
|
+
current[@"subscriptionsStatus"] = [current[@"pendingSubscriptionOperations"] unsignedIntegerValue] == 0 ? @"ready" : @"pending";
|
|
1027
|
+
NSMutableDictionary *confirmed = [current[@"confirmedSubscriptions"] mutableCopy] ?: [NSMutableDictionary dictionary];
|
|
1028
|
+
[confirmed removeObjectForKey:filter];
|
|
1029
|
+
current[@"confirmedSubscriptions"] = confirmed;
|
|
1030
|
+
NSMutableDictionary *currentTombstones = [current[@"tombstones"] mutableCopy] ?: [NSMutableDictionary dictionary];
|
|
1031
|
+
[currentTombstones removeObjectForKey:filter];
|
|
1032
|
+
current[@"tombstones"] = currentTombstones;
|
|
1033
|
+
current[@"revision"] = @([current[@"revision"] integerValue] + 1);
|
|
1034
|
+
self->_sessions[instanceId] = current;
|
|
1035
|
+
[self settleSubscriptionWaitersForInstance:instanceId];
|
|
1036
|
+
resolve(@{ @"intentRevision": @(intentRevision) });
|
|
1037
|
+
}
|
|
1038
|
+
finish();
|
|
1039
|
+
});
|
|
1040
|
+
}];
|
|
1041
|
+
} instanceId:instanceId filter:filter];
|
|
1042
|
+
NSUInteger timeoutMs = [record[@"options"][@"operationTimeoutMs"] unsignedIntegerValue] ?: 30000;
|
|
1043
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeoutMs * NSEC_PER_MSEC), self->_sessionQueue, ^{
|
|
1044
|
+
if (settled) return;
|
|
1045
|
+
settled = YES;
|
|
1046
|
+
operationWaiter[@"settled"] = @YES;
|
|
1047
|
+
NSMutableDictionary *current = [self->_sessions[instanceId] mutableCopy];
|
|
1048
|
+
if (current != nil) {
|
|
1049
|
+
NSMutableArray *remainingOperations = [current[@"operationWaiters"] mutableCopy] ?: [NSMutableArray array];
|
|
1050
|
+
[remainingOperations removeObject:operationWaiter];
|
|
1051
|
+
current[@"operationWaiters"] = remainingOperations;
|
|
1052
|
+
if (previous != nil) {
|
|
1053
|
+
NSMutableDictionary *desiredAfterTimeout = [current[@"desiredSubscriptions"] mutableCopy] ?: [NSMutableDictionary dictionary];
|
|
1054
|
+
desiredAfterTimeout[filter] = previous;
|
|
1055
|
+
current[@"desiredSubscriptions"] = desiredAfterTimeout;
|
|
1056
|
+
}
|
|
1057
|
+
current[@"pendingSubscriptionOperations"] = @([current[@"pendingSubscriptionOperations"] unsignedIntegerValue] > 0 ? [current[@"pendingSubscriptionOperations"] unsignedIntegerValue] - 1 : 0);
|
|
1058
|
+
current[@"subscriptionsStatus"] = @"failed";
|
|
1059
|
+
self->_sessions[instanceId] = current;
|
|
1060
|
+
[self settleSubscriptionWaitersForInstance:instanceId];
|
|
1061
|
+
}
|
|
1062
|
+
reject(@"ERR_OPERATION_TIMEOUT", @"Unsubscription operation timed out", nil);
|
|
1063
|
+
if (finishOperation != nil) finishOperation();
|
|
1064
|
+
});
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
- (void)awaitSubscriptions:(NSString *)instanceId generation:(double)generation desiredRevision:(double)desiredRevision resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { dispatch_async(_sessionQueue, ^{ NSMutableDictionary *record = [self->_sessions[instanceId] mutableCopy]; if (record == nil || ![record[@"state"] isEqual:@"connected"]) { reject(@"ERR_NOT_CONNECTED", @"Session is not connected", nil); return; } if ([record[@"generation"] doubleValue] != generation) { reject(@"ERR_CONNECTION_LOST", @"Connection generation changed", nil); return; } if ([record[@"subscriptionsStatus"] isEqual:@"failed"]) { reject(@"ERR_SUBSCRIBE_FAILED", @"Subscription synchronization failed", nil); return; } if ([record[@"subscriptionsRevision"] doubleValue] >= desiredRevision && [record[@"subscriptionsStatus"] isEqual:@"ready"]) { resolve([self snapshotForInstance:instanceId state:record[@"state"] revision:record[@"revision"] generation:record[@"generation"] lastConnAck:record[@"lastConnAck"] deliveryStatus:@"active"]); return; } NSMutableArray *waiters = [record[@"subscriptionWaiters"] mutableCopy] ?: [NSMutableArray array]; NSDictionary *waiter = @{ @"generation": @(generation), @"desiredRevision": @(desiredRevision), @"resolve": [resolve copy], @"reject": [reject copy] }; [waiters addObject:waiter]; record[@"subscriptionWaiters"] = waiters; self->_sessions[instanceId] = record; NSUInteger timeoutMs = [record[@"options"][@"operationTimeoutMs"] unsignedIntegerValue] ?: 30000; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeoutMs * NSEC_PER_MSEC), self->_sessionQueue, ^{ NSMutableDictionary *current = [self->_sessions[instanceId] mutableCopy]; NSMutableArray *currentWaiters = [current[@"subscriptionWaiters"] mutableCopy]; if (current == nil || currentWaiters == nil || ![currentWaiters containsObject:waiter]) return; [currentWaiters removeObject:waiter]; current[@"subscriptionWaiters"] = currentWaiters; self->_sessions[instanceId] = current; reject(@"ERR_OPERATION_TIMEOUT", @"Subscription synchronization is not ready", nil); }); }); }
|
|
1068
|
+
- (void)publish:(NSString *)instanceId request:(JS::NativeReactNativeMqtt::WirePublishRequest &)request resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
1069
|
+
NSString *topic = [request.topic() copy];
|
|
1070
|
+
NSString *responseTopic = [request.responseTopic() copy];
|
|
1071
|
+
NSString *payloadBase64 = [request.payloadBase64() copy];
|
|
1072
|
+
NSString *contentType = [request.contentType() copy];
|
|
1073
|
+
NSString *correlationDataBase64 = [request.correlationDataBase64() copy];
|
|
1074
|
+
NSNumber *messageExpiry = request.messageExpiryIntervalSeconds().has_value() ? @(*request.messageExpiryIntervalSeconds()) : nil;
|
|
1075
|
+
NSNumber *payloadFormatIndicator = request.payloadFormatIndicator().has_value() ? @(*request.payloadFormatIndicator()) : nil;
|
|
1076
|
+
NSNumber *qos = @(request.qos().value_or(1));
|
|
1077
|
+
BOOL retained = request.retained().value_or(false);
|
|
1078
|
+
NSArray<NSDictionary *> *userProperties = nil;
|
|
1079
|
+
if (auto values = request.userProperties(); values.has_value()) {
|
|
1080
|
+
NSMutableArray<NSDictionary *> *pairs = [NSMutableArray arrayWithCapacity:values->size()];
|
|
1081
|
+
for (const auto &property : *values) [pairs addObject:@{ @"key": property.key(), @"value": property.value() }];
|
|
1082
|
+
userProperties = pairs;
|
|
1083
|
+
}
|
|
1084
|
+
if (!RNMqttValidTopicName(topic)) { reject(@"ERR_INVALID_OPTIONS", @"topic name is invalid", nil); return; }
|
|
1085
|
+
if (responseTopic != nil && !RNMqttValidTopicName(responseTopic)) { reject(@"ERR_INVALID_OPTIONS", @"responseTopic is invalid", nil); return; }
|
|
1086
|
+
dispatch_async(_sessionQueue, ^{
|
|
1087
|
+
NSMutableDictionary *record = [self->_sessions[instanceId] mutableCopy];
|
|
1088
|
+
ReactNativeMqttCocoaBridge *bridge = record[@"bridge"];
|
|
1089
|
+
if (record == nil || bridge == nil || ![record[@"state"] isEqual:@"connected"]) { reject(@"ERR_NOT_CONNECTED", @"Session is not connected", nil); return; }
|
|
1090
|
+
NSData *payload = [[NSData alloc] initWithBase64EncodedString:payloadBase64 options:0];
|
|
1091
|
+
if (payload == nil) { reject(@"ERR_INVALID_OPTIONS", @"payloadBase64 is invalid", nil); return; }
|
|
1092
|
+
NSMutableDictionary *properties = [NSMutableDictionary dictionary];
|
|
1093
|
+
if (contentType != nil) properties[@"contentType"] = contentType;
|
|
1094
|
+
if (responseTopic != nil) properties[@"responseTopic"] = responseTopic;
|
|
1095
|
+
if (correlationDataBase64 != nil) {
|
|
1096
|
+
NSData *correlation = [[NSData alloc] initWithBase64EncodedString:correlationDataBase64 options:0];
|
|
1097
|
+
if (correlation == nil) { reject(@"ERR_INVALID_PAYLOAD", @"correlationDataBase64 is invalid", nil); return; }
|
|
1098
|
+
properties[@"correlationData"] = correlation;
|
|
1099
|
+
}
|
|
1100
|
+
if (messageExpiry != nil) properties[@"messageExpiryIntervalSeconds"] = messageExpiry;
|
|
1101
|
+
if (payloadFormatIndicator != nil) properties[@"payloadFormatIndicator"] = payloadFormatIndicator;
|
|
1102
|
+
if (userProperties != nil) properties[@"userProperties"] = userProperties;
|
|
1103
|
+
NSMutableDictionary *operationWaiter = [self operationWaiterWithReject:reject operation:@"publish" topic:topic generation:record[@"generation"]];
|
|
1104
|
+
NSMutableArray *operationWaiters = [record[@"operationWaiters"] mutableCopy] ?: [NSMutableArray array];
|
|
1105
|
+
[operationWaiters addObject:operationWaiter];
|
|
1106
|
+
record[@"operationWaiters"] = operationWaiters;
|
|
1107
|
+
self->_sessions[instanceId] = record;
|
|
1108
|
+
__block BOOL settled = NO;
|
|
1109
|
+
[bridge publishTopic:topic payload:payload qos:qos retained:retained properties:properties completion:^(NSError *error) {
|
|
1110
|
+
dispatch_async(self->_sessionQueue, ^{
|
|
1111
|
+
if (settled || [operationWaiter[@"settled"] boolValue]) return;
|
|
1112
|
+
settled = YES;
|
|
1113
|
+
operationWaiter[@"settled"] = @YES;
|
|
1114
|
+
NSMutableDictionary *current = [self->_sessions[instanceId] mutableCopy];
|
|
1115
|
+
NSMutableArray *remainingOperations = [current[@"operationWaiters"] mutableCopy] ?: [NSMutableArray array];
|
|
1116
|
+
[remainingOperations removeObject:operationWaiter];
|
|
1117
|
+
current[@"operationWaiters"] = remainingOperations;
|
|
1118
|
+
self->_sessions[instanceId] = current;
|
|
1119
|
+
if (error != nil) {
|
|
1120
|
+
NSError *annotatedError = RNMqttAnnotatedError(error, @"publish", current[@"generation"], @"unknown");
|
|
1121
|
+
reject(@"ERR_PUBLISH_FAILED", annotatedError.localizedDescription, annotatedError);
|
|
1122
|
+
} else {
|
|
1123
|
+
resolve(nil);
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
}];
|
|
1127
|
+
NSUInteger timeoutMs = [record[@"options"][@"operationTimeoutMs"] unsignedIntegerValue] ?: 30000;
|
|
1128
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)timeoutMs * NSEC_PER_MSEC), self->_sessionQueue, ^{
|
|
1129
|
+
if (settled) return;
|
|
1130
|
+
settled = YES;
|
|
1131
|
+
operationWaiter[@"settled"] = @YES;
|
|
1132
|
+
NSMutableDictionary *current = [self->_sessions[instanceId] mutableCopy];
|
|
1133
|
+
if (current != nil) {
|
|
1134
|
+
NSMutableArray *remainingOperations = [current[@"operationWaiters"] mutableCopy] ?: [NSMutableArray array];
|
|
1135
|
+
[remainingOperations removeObject:operationWaiter];
|
|
1136
|
+
current[@"operationWaiters"] = remainingOperations;
|
|
1137
|
+
self->_sessions[instanceId] = current;
|
|
1138
|
+
}
|
|
1139
|
+
NSError *timeoutError = [NSError errorWithDomain:@"ReactNativeMqtt" code:408 userInfo:@{ NSLocalizedDescriptionKey: @"Publish operation timed out", @"operation": @"publish", @"outcome": @"unknown" }];
|
|
1140
|
+
reject(@"ERR_OPERATION_TIMEOUT", timeoutError.localizedDescription, timeoutError);
|
|
1141
|
+
});
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
- (void)ackMessageBatch:(NSString *)instanceId streamEpoch:(double)streamEpoch batchId:(double)batchId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { if (!std::isfinite(streamEpoch) || !std::isfinite(batchId) || streamEpoch < 0 || batchId < 0 || std::floor(streamEpoch) != streamEpoch || std::floor(batchId) != batchId) { reject(@"ERR_INVALID_OPTIONS", @"ACK token must contain non-negative integers", nil); return; } dispatch_async(_sessionQueue, ^{ NSDictionary *token = self->_iosBatchLedger[instanceId]; if (token != nil && [token[@"streamEpoch"] doubleValue] == streamEpoch && [token[@"batchId"] doubleValue] == batchId) { [self->_iosBatchLedger removeObjectForKey:instanceId]; NSMutableDictionary *record = [self->_sessions[instanceId] mutableCopy]; if (record != nil) { record[@"overflowDiagnosticPending"] = @NO; self->_sessions[instanceId] = record; } [self emitNextMessageBatchForInstance:instanceId]; } resolve(nil); }); }
|
|
1145
|
+
|
|
1146
|
+
- (void)invalidate {
|
|
1147
|
+
dispatch_sync(_sessionQueue, ^{
|
|
1148
|
+
self->_invalidated = YES;
|
|
1149
|
+
for (NSString *instanceId in [self->_sessions allKeys]) {
|
|
1150
|
+
NSDictionary *record = self->_sessions[instanceId];
|
|
1151
|
+
ReactNativeMqttCocoaBridge *bridge = record[@"bridge"];
|
|
1152
|
+
for (NSDictionary *waiter in [record[@"connectWaiters"] copy] ?: @[]) {
|
|
1153
|
+
((RCTPromiseRejectBlock)waiter[@"reject"])(@"ERR_DISPOSED", @"Native module was invalidated", nil);
|
|
1154
|
+
}
|
|
1155
|
+
for (NSDictionary *waiter in [record[@"subscriptionWaiters"] copy] ?: @[]) {
|
|
1156
|
+
((RCTPromiseRejectBlock)waiter[@"reject"])(@"ERR_DISPOSED", @"Native module was invalidated", nil);
|
|
1157
|
+
}
|
|
1158
|
+
for (NSMutableDictionary *waiter in [record[@"operationWaiters"] copy] ?: @[]) {
|
|
1159
|
+
if (![waiter[@"settled"] boolValue]) {
|
|
1160
|
+
waiter[@"settled"] = @YES;
|
|
1161
|
+
((RCTPromiseRejectBlock)waiter[@"reject"])(@"ERR_DISPOSED", @"Native module was invalidated", nil);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
[bridge disconnect];
|
|
1165
|
+
}
|
|
1166
|
+
[self->_sessions removeAllObjects];
|
|
1167
|
+
[self->_iosBatchLedger removeAllObjects];
|
|
1168
|
+
[self->_iosStreamEpochs removeAllObjects];
|
|
1169
|
+
[self->_iosSubscriptionQueues removeAllObjects];
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
1174
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
1175
|
+
{
|
|
1176
|
+
return std::make_shared<facebook::react::NativeReactNativeMqttSpecJSI>(params);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
+ (NSString *)moduleName
|
|
1180
|
+
{
|
|
1181
|
+
return @"ReactNativeMqtt";
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
@end
|